RenderProps and SpanFactories
This commit is contained in:
parent
69f9d0ebb8
commit
9dd3d4a94d
@ -85,17 +85,17 @@ public class MarkdownRenderer {
|
||||
: prism4JThemeDarkula;
|
||||
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.use(CorePlugin.create())
|
||||
.use(ImagesPlugin.createWithAssets(context))
|
||||
.use(SvgPlugin.create(context.getResources()))
|
||||
.use(GifPlugin.create(false))
|
||||
.use(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
|
||||
.use(GifAwarePlugin.create(context))
|
||||
.use(TablePlugin.create(context))
|
||||
.use(TaskListPlugin.create(context))
|
||||
.use(StrikethroughPlugin.create())
|
||||
.use(HtmlPlugin.create())
|
||||
.use(new AbstractMarkwonPlugin() {
|
||||
.usePlugin(CorePlugin.create())
|
||||
.usePlugin(ImagesPlugin.createWithAssets(context))
|
||||
.usePlugin(SvgPlugin.create(context.getResources()))
|
||||
.usePlugin(GifPlugin.create(false))
|
||||
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
|
||||
.usePlugin(GifAwarePlugin.create(context))
|
||||
.usePlugin(TablePlugin.create(context))
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.urlProcessor(urlProcessor);
|
||||
|
@ -4,9 +4,16 @@ import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.Image;
|
||||
|
||||
import ru.noties.markwon.AbstractMarkwonPlugin;
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.MarkwonSpansFactory;
|
||||
import ru.noties.markwon.R;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.image.AsyncDrawableSpan;
|
||||
import ru.noties.markwon.image.ImageProps;
|
||||
|
||||
public class GifAwarePlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@ -18,18 +25,36 @@ public class GifAwarePlugin extends AbstractMarkwonPlugin {
|
||||
private final Context context;
|
||||
private final GifProcessor processor;
|
||||
|
||||
public GifAwarePlugin(@NonNull Context context) {
|
||||
GifAwarePlugin(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.processor = GifProcessor.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
|
||||
final GifPlaceholder gifPlaceholder = new GifPlaceholder(
|
||||
context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white),
|
||||
0x20000000
|
||||
);
|
||||
builder.factory(new GifAwareSpannableFactory(gifPlaceholder));
|
||||
|
||||
builder.setFactory(Image.class, new SpanFactory() {
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new AsyncDrawableSpan(
|
||||
configuration.theme(),
|
||||
new GifAwareAsyncDrawable(
|
||||
gifPlaceholder,
|
||||
ImageProps.DESTINATION.require(context),
|
||||
configuration.asyncDrawableLoader(),
|
||||
configuration.imageSizeResolver(),
|
||||
ImageProps.IMAGE_SIZE.get(context)
|
||||
),
|
||||
AsyncDrawableSpan.ALIGN_BOTTOM,
|
||||
ImageProps.REPLACEMENT_TEXT_IS_LINK.get(context, false)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,37 +0,0 @@
|
||||
package ru.noties.markwon.gif;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.core.MarkwonSpannableFactoryDef;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.image.ImageSize;
|
||||
import ru.noties.markwon.image.ImageSizeResolver;
|
||||
import ru.noties.markwon.core.spans.AsyncDrawableSpan;
|
||||
|
||||
public class GifAwareSpannableFactory extends MarkwonSpannableFactoryDef {
|
||||
|
||||
private final GifPlaceholder gifPlaceholder;
|
||||
|
||||
public GifAwareSpannableFactory(@NonNull GifPlaceholder gifPlaceholder) {
|
||||
this.gifPlaceholder = gifPlaceholder;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
|
||||
return new AsyncDrawableSpan(
|
||||
theme,
|
||||
new GifAwareAsyncDrawable(
|
||||
gifPlaceholder,
|
||||
destination,
|
||||
loader,
|
||||
imageSizeResolver,
|
||||
imageSize
|
||||
),
|
||||
AsyncDrawableSpan.ALIGN_BOTTOM,
|
||||
replacementTextIsLink
|
||||
);
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import pl.droidsonroids.gif.GifDrawable;
|
||||
import ru.noties.markwon.core.spans.AsyncDrawableSpan;
|
||||
import ru.noties.markwon.image.AsyncDrawableSpan;
|
||||
|
||||
public abstract class GifProcessor {
|
||||
|
||||
|
@ -12,8 +12,13 @@ import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import ru.noties.markwon.AbstractMarkwonPlugin;
|
||||
import ru.noties.markwon.MarkwonSpansFactory;
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class TaskListPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
/**
|
||||
@ -62,6 +67,11 @@ public class TaskListPlugin extends AbstractMarkwonPlugin {
|
||||
builder.customBlockParserFactory(new TaskListBlockParser.Factory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.setFactory(TaskListItem.class, new TaskListSpanFactory(drawable));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder
|
||||
@ -86,11 +96,13 @@ public class TaskListPlugin extends AbstractMarkwonPlugin {
|
||||
final int length = visitor.length();
|
||||
|
||||
visitor.visitChildren(taskListItem);
|
||||
visitor.setSpans(length, new TaskListSpan(
|
||||
visitor.theme(),
|
||||
drawable,
|
||||
indent(taskListItem) + taskListItem.indent(),
|
||||
taskListItem.done()));
|
||||
|
||||
final RenderProps context = visitor.renderProps();
|
||||
|
||||
TaskListProps.BLOCK_INDENT.set(context, indent(taskListItem) + taskListItem.indent());
|
||||
TaskListProps.DONE.set(context, taskListItem.done());
|
||||
|
||||
visitor.setSpansForNode(taskListItem, length);
|
||||
|
||||
if (visitor.hasNext(taskListItem)) {
|
||||
visitor.ensureNewLine();
|
||||
@ -99,7 +111,7 @@ public class TaskListPlugin extends AbstractMarkwonPlugin {
|
||||
});
|
||||
}
|
||||
|
||||
private static int resolve(Context context, @AttrRes int attr) {
|
||||
private static int resolve(@NonNull Context context, @AttrRes int attr) {
|
||||
final TypedValue typedValue = new TypedValue();
|
||||
final int attrs[] = new int[]{attr};
|
||||
final TypedArray typedArray = context.obtainStyledAttributes(typedValue.data, attrs);
|
||||
|
@ -0,0 +1,16 @@
|
||||
package ru.noties.markwon.ext.tasklist;
|
||||
|
||||
import ru.noties.markwon.Prop;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public abstract class TaskListProps {
|
||||
|
||||
public static final Prop<Integer> BLOCK_INDENT = Prop.of("task-list-block-indent");
|
||||
|
||||
public static final Prop<Boolean> DONE = Prop.of("task-list-done");
|
||||
|
||||
private TaskListProps() {
|
||||
}
|
||||
}
|
@ -26,13 +26,6 @@ public class TaskListSpan implements LeadingMarginSpan {
|
||||
// @since 2.0.1 field is NOT final (to allow mutation)
|
||||
private boolean isDone;
|
||||
|
||||
@Deprecated
|
||||
public TaskListSpan(@NonNull MarkwonTheme theme, int blockIndent, boolean isDone) {
|
||||
this.theme = theme;
|
||||
this.drawable = null;
|
||||
this.blockIndent = blockIndent;
|
||||
this.isDone = isDone;
|
||||
}
|
||||
|
||||
public TaskListSpan(@NonNull MarkwonTheme theme, @NonNull Drawable drawable, int blockIndent, boolean isDone) {
|
||||
this.theme = theme;
|
||||
@ -71,11 +64,6 @@ public class TaskListSpan implements LeadingMarginSpan {
|
||||
return;
|
||||
}
|
||||
|
||||
// final Drawable drawable = theme.getTaskListDrawable();
|
||||
// if (drawable == null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
final int save = c.save();
|
||||
try {
|
||||
|
||||
|
@ -0,0 +1,29 @@
|
||||
package ru.noties.markwon.ext.tasklist;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
|
||||
public class TaskListSpanFactory implements SpanFactory {
|
||||
|
||||
private final Drawable drawable;
|
||||
|
||||
public TaskListSpanFactory(@NonNull Drawable drawable) {
|
||||
this.drawable = drawable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new TaskListSpan(
|
||||
configuration.theme(),
|
||||
drawable,
|
||||
TaskListProps.BLOCK_INDENT.get(context, 0),
|
||||
TaskListProps.DONE.get(context, false)
|
||||
);
|
||||
}
|
||||
}
|
@ -47,13 +47,15 @@ public class ImageHandler extends SimpleTagHandler {
|
||||
// but we can look and see if we are inside a LinkSpan (will have to extend TagHandler
|
||||
// to obtain an instance SpannableBuilder for inspection)
|
||||
|
||||
return configuration.factory().image(
|
||||
configuration.theme(),
|
||||
destination,
|
||||
configuration.asyncDrawableLoader(),
|
||||
configuration.imageSizeResolver(),
|
||||
imageSizeParser.parse(tag.attributes()),
|
||||
false
|
||||
);
|
||||
return null;
|
||||
|
||||
// return configuration.factory().image(
|
||||
// configuration.theme(),
|
||||
// destination,
|
||||
// configuration.asyncDrawableLoader(),
|
||||
// configuration.imageSizeResolver(),
|
||||
// imageSizeParser.parse(tag.attributes()),
|
||||
// false
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,16 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureRenderProps(@NonNull RenderProps renderProps) {
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String processMarkdown(@NonNull String markdown) {
|
||||
|
@ -9,6 +9,9 @@ import org.commonmark.node.Node;
|
||||
|
||||
public abstract class Markwon {
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder(@NonNull Context context) {
|
||||
return new MarkwonBuilderImpl(context);
|
||||
@ -28,6 +31,9 @@ public abstract class Markwon {
|
||||
|
||||
public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown);
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface Builder {
|
||||
|
||||
/**
|
||||
@ -40,7 +46,10 @@ public abstract class Markwon {
|
||||
Builder bufferType(@NonNull TextView.BufferType bufferType);
|
||||
|
||||
@NonNull
|
||||
Builder use(@NonNull MarkwonPlugin plugin);
|
||||
Builder usePlugin(@NonNull MarkwonPlugin plugin);
|
||||
|
||||
@NonNull
|
||||
Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins);
|
||||
|
||||
@NonNull
|
||||
Markwon build();
|
||||
|
@ -8,11 +8,15 @@ import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
|
||||
private final Context context;
|
||||
@ -33,11 +37,30 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Markwon.Builder use(@NonNull MarkwonPlugin plugin) {
|
||||
public Markwon.Builder usePlugin(@NonNull MarkwonPlugin plugin) {
|
||||
plugins.add(plugin);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Markwon.Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins) {
|
||||
|
||||
final Iterator<? extends MarkwonPlugin> iterator = plugins.iterator();
|
||||
|
||||
MarkwonPlugin plugin;
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
plugin = iterator.next();
|
||||
if (plugin == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
this.plugins.add(plugin);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Markwon build() {
|
||||
@ -45,8 +68,10 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
final Parser.Builder parserBuilder = new Parser.Builder();
|
||||
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
|
||||
final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder();
|
||||
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder(context);
|
||||
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder();
|
||||
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
|
||||
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
|
||||
final RenderProps renderProps = new RenderPropsImpl();
|
||||
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
plugin.configureParser(parserBuilder);
|
||||
@ -54,16 +79,19 @@ class MarkwonBuilderImpl implements Markwon.Builder {
|
||||
plugin.configureImages(asyncDrawableLoaderBuilder);
|
||||
plugin.configureConfiguration(configurationBuilder);
|
||||
plugin.configureVisitor(visitorBuilder);
|
||||
plugin.configureSpansFactory(spanFactoryBuilder);
|
||||
plugin.configureRenderProps(renderProps);
|
||||
}
|
||||
|
||||
final MarkwonConfiguration configuration = configurationBuilder.build(
|
||||
themeBuilder.build(),
|
||||
asyncDrawableLoaderBuilder.build());
|
||||
asyncDrawableLoaderBuilder.build(),
|
||||
spanFactoryBuilder.build());
|
||||
|
||||
return new MarkwonImpl(
|
||||
bufferType,
|
||||
parserBuilder.build(),
|
||||
visitorBuilder.build(configuration),
|
||||
visitorBuilder.build(configuration, renderProps),
|
||||
Collections.unmodifiableList(plugins)
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.core.MarkwonSpannableFactory;
|
||||
import ru.noties.markwon.core.MarkwonSpannableFactoryDef;
|
||||
import ru.noties.markwon.core.spans.LinkSpan;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.image.ImageSizeResolver;
|
||||
@ -21,16 +18,9 @@ import ru.noties.markwon.urlprocessor.UrlProcessorNoOp;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class MarkwonConfiguration {
|
||||
|
||||
// creates default configuration
|
||||
@NonNull
|
||||
@Deprecated
|
||||
public static MarkwonConfiguration create(@NonNull Context context) {
|
||||
return new Builder(context).build(MarkwonTheme.create(context), AsyncDrawableLoader.noOp());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Builder builder(@NonNull Context context) {
|
||||
return new Builder(context);
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
private final MarkwonTheme theme;
|
||||
@ -39,7 +29,9 @@ public class MarkwonConfiguration {
|
||||
private final LinkSpan.Resolver linkResolver;
|
||||
private final UrlProcessor urlProcessor;
|
||||
private final ImageSizeResolver imageSizeResolver;
|
||||
private final MarkwonSpannableFactory factory; // @since 1.1.0
|
||||
|
||||
// @since 3.0.0
|
||||
private final MarkwonSpansFactory spansFactory;
|
||||
|
||||
private MarkwonConfiguration(@NonNull Builder builder) {
|
||||
this.theme = builder.theme;
|
||||
@ -48,7 +40,7 @@ public class MarkwonConfiguration {
|
||||
this.linkResolver = builder.linkResolver;
|
||||
this.urlProcessor = builder.urlProcessor;
|
||||
this.imageSizeResolver = builder.imageSizeResolver;
|
||||
this.factory = builder.factory;
|
||||
this.spansFactory = builder.spansFactory;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -81,26 +73,26 @@ public class MarkwonConfiguration {
|
||||
return imageSizeResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public MarkwonSpannableFactory factory() {
|
||||
return factory;
|
||||
public MarkwonSpansFactory spansFactory() {
|
||||
return spansFactory;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressWarnings({"unused", "UnusedReturnValue"})
|
||||
public static class Builder {
|
||||
|
||||
private final Context context;
|
||||
|
||||
private MarkwonTheme theme;
|
||||
private AsyncDrawableLoader asyncDrawableLoader;
|
||||
private SyntaxHighlight syntaxHighlight;
|
||||
private LinkSpan.Resolver linkResolver;
|
||||
private UrlProcessor urlProcessor;
|
||||
private ImageSizeResolver imageSizeResolver;
|
||||
private MarkwonSpannableFactory factory; // @since 1.1.0
|
||||
private MarkwonSpansFactory spansFactory;
|
||||
|
||||
Builder(@NonNull Context context) {
|
||||
this.context = context;
|
||||
Builder() {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -130,20 +122,15 @@ public class MarkwonConfiguration {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@NonNull
|
||||
public Builder factory(@NonNull MarkwonSpannableFactory factory) {
|
||||
this.factory = factory;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonConfiguration build(@NonNull MarkwonTheme theme, @NonNull AsyncDrawableLoader asyncDrawableLoader) {
|
||||
public MarkwonConfiguration build(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull AsyncDrawableLoader asyncDrawableLoader,
|
||||
@NonNull MarkwonSpansFactory spansFactory) {
|
||||
|
||||
this.theme = theme;
|
||||
this.asyncDrawableLoader = asyncDrawableLoader;
|
||||
this.spansFactory = spansFactory;
|
||||
|
||||
if (syntaxHighlight == null) {
|
||||
syntaxHighlight = new SyntaxHighlightNoOp();
|
||||
@ -161,11 +148,6 @@ public class MarkwonConfiguration {
|
||||
imageSizeResolver = new ImageSizeResolverDef();
|
||||
}
|
||||
|
||||
// @since 1.1.0
|
||||
if (factory == null) {
|
||||
factory = MarkwonSpannableFactoryDef.create();
|
||||
}
|
||||
|
||||
return new MarkwonConfiguration(this);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,9 @@ import org.commonmark.parser.Parser;
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface MarkwonPlugin {
|
||||
|
||||
void configureParser(@NonNull Parser.Builder builder);
|
||||
@ -21,6 +24,11 @@ public interface MarkwonPlugin {
|
||||
|
||||
void configureVisitor(@NonNull MarkwonVisitor.Builder builder);
|
||||
|
||||
void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder);
|
||||
|
||||
// can be used to configure own properties and use between plugins
|
||||
void configureRenderProps(@NonNull RenderProps renderProps);
|
||||
|
||||
@NonNull
|
||||
String processMarkdown(@NonNull String markdown);
|
||||
|
||||
|
@ -0,0 +1,34 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface MarkwonSpansFactory {
|
||||
|
||||
@Nullable
|
||||
<N extends Node, F extends SpanFactory> F get(@NonNull Class<N> node);
|
||||
|
||||
@Nullable
|
||||
<N extends Node, F extends SpanFactory> F get(@NonNull N node);
|
||||
|
||||
@NonNull
|
||||
<N extends Node, F extends SpanFactory> F require(@NonNull Class<N> node);
|
||||
|
||||
@NonNull
|
||||
<N extends Node, F extends SpanFactory> F require(@NonNull N node);
|
||||
|
||||
|
||||
interface Builder {
|
||||
|
||||
@NonNull
|
||||
<N extends Node, F extends SpanFactory> Builder setFactory(@NonNull Class<N> node, @NonNull F factory);
|
||||
|
||||
@NonNull
|
||||
MarkwonSpansFactory build();
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class MarkwonSpansFactoryImpl implements MarkwonSpansFactory {
|
||||
|
||||
private final Map<Class<? extends Node>, SpanFactory> factories;
|
||||
|
||||
private MarkwonSpansFactoryImpl(@NonNull Map<Class<? extends Node>, SpanFactory> factories) {
|
||||
this.factories = factories;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <N extends Node, F extends SpanFactory> F get(@NonNull Class<N> node) {
|
||||
//noinspection unchecked
|
||||
return (F) factories.get(node);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <N extends Node, F extends SpanFactory> F get(@NonNull N node) {
|
||||
return get(node.getClass());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <N extends Node, F extends SpanFactory> F require(@NonNull Class<N> node) {
|
||||
final F f = get(node);
|
||||
if (f == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <N extends Node, F extends SpanFactory> F require(@NonNull N node) {
|
||||
final F f = get(node);
|
||||
if (f == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
static class BuilderImpl implements Builder {
|
||||
|
||||
private final Map<Class<? extends Node>, SpanFactory> factories =
|
||||
new HashMap<>(3);
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <N extends Node, F extends SpanFactory> Builder setFactory(@NonNull Class<N> node, @NonNull F factory) {
|
||||
factories.put(node, factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MarkwonSpansFactory build() {
|
||||
return new MarkwonSpansFactoryImpl(Collections.unmodifiableMap(factories));
|
||||
}
|
||||
}
|
||||
}
|
@ -6,9 +6,9 @@ import android.support.annotation.Nullable;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.Visitor;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.core.MarkwonSpannableFactory;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface MarkwonVisitor extends Visitor {
|
||||
|
||||
interface NodeVisitor<N extends Node> {
|
||||
@ -21,17 +21,14 @@ public interface MarkwonVisitor extends Visitor {
|
||||
<N extends Node> Builder on(@NonNull Class<N> node, @Nullable NodeVisitor<? super N> nodeVisitor);
|
||||
|
||||
@NonNull
|
||||
MarkwonVisitor build(@NonNull MarkwonConfiguration configuration);
|
||||
MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
MarkwonConfiguration configuration();
|
||||
|
||||
@NonNull
|
||||
MarkwonTheme theme();
|
||||
|
||||
@NonNull
|
||||
MarkwonSpannableFactory factory();
|
||||
RenderProps renderProps();
|
||||
|
||||
@NonNull
|
||||
SpannableBuilder builder();
|
||||
@ -48,6 +45,10 @@ public interface MarkwonVisitor extends Visitor {
|
||||
|
||||
void setSpans(int start, @Nullable Object spans);
|
||||
|
||||
@Nullable
|
||||
<N extends Node> NodeVisitor<N> nodeVisitor(@NonNull Class<N> node);
|
||||
// will automatically obtain SpanFactory instance and use it, it no SpanFactory is registered,
|
||||
// will throw, if not desired use setSpansForNodeOptional
|
||||
<N extends Node> void setSpansForNode(@NonNull N node, int start);
|
||||
|
||||
// does not throw if there is no SpanFactory registered for this node
|
||||
<N extends Node> void setSpansForNodeOptional(@NonNull N node, int start);
|
||||
}
|
||||
|
@ -31,25 +31,25 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.core.MarkwonSpannableFactory;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
class MarkwonVisitorImpl implements MarkwonVisitor {
|
||||
|
||||
private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes;
|
||||
|
||||
private final MarkwonConfiguration configuration;
|
||||
private final MarkwonTheme theme;
|
||||
private final MarkwonSpannableFactory factory;
|
||||
|
||||
private final RenderProps renderProps;
|
||||
|
||||
private final SpannableBuilder builder = new SpannableBuilder();
|
||||
|
||||
MarkwonVisitorImpl(
|
||||
@NonNull MarkwonConfiguration configuration,
|
||||
@NonNull RenderProps renderProps,
|
||||
@NonNull Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes) {
|
||||
this.configuration = configuration;
|
||||
this.theme = configuration.theme();
|
||||
this.factory = configuration.factory();
|
||||
this.renderProps = renderProps;
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
@ -181,14 +181,8 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MarkwonTheme theme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MarkwonSpannableFactory factory() {
|
||||
return factory;
|
||||
public RenderProps renderProps() {
|
||||
return renderProps;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -237,17 +231,22 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
|
||||
SpannableBuilder.setSpans(builder, spans, start, builder.length());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <N extends Node> NodeVisitor<N> nodeVisitor(@NonNull Class<N> node) {
|
||||
//noinspection unchecked
|
||||
return (NodeVisitor<N>) nodes.get(node);
|
||||
public <N extends Node> void setSpansForNode(@NonNull N node, int start) {
|
||||
setSpans(start, configuration.spansFactory().require(node).getSpans(configuration, renderProps));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <N extends Node> void setSpansForNodeOptional(@NonNull N node, int start) {
|
||||
final SpanFactory factory = configuration.spansFactory().get(node);
|
||||
if (factory != null) {
|
||||
setSpans(start, factory.getSpans(configuration, renderProps));
|
||||
}
|
||||
}
|
||||
|
||||
static class BuilderImpl implements Builder {
|
||||
|
||||
private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes =
|
||||
new HashMap<>(3);
|
||||
private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes = new HashMap<>();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@ -264,9 +263,10 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration) {
|
||||
public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) {
|
||||
return new MarkwonVisitorImpl(
|
||||
configuration,
|
||||
renderProps,
|
||||
Collections.unmodifiableMap(nodes));
|
||||
}
|
||||
}
|
||||
|
84
markwon/src/main/java/ru/noties/markwon/Prop.java
Normal file
84
markwon/src/main/java/ru/noties/markwon/Prop.java
Normal file
@ -0,0 +1,84 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class to hold data in {@link RenderProps}
|
||||
*
|
||||
* @param <T> represents the type that this instance holds
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public final class Prop<T> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@NonNull
|
||||
public static <T> Prop<T> of(@NonNull Class<T> type, @NonNull String name) {
|
||||
return new Prop<>(name);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static <T> Prop<T> of(@NonNull String name) {
|
||||
return new Prop<>(name);
|
||||
}
|
||||
|
||||
private final String name;
|
||||
|
||||
private Prop(@NonNull String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T get(@NonNull RenderProps context) {
|
||||
return context.get(this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public T get(@NonNull RenderProps context, @NonNull T defValue) {
|
||||
return context.get(this, defValue);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public T require(@NonNull RenderProps context) {
|
||||
final T t = get(context);
|
||||
if (t == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
public void set(@NonNull RenderProps context, @Nullable T value) {
|
||||
context.set(this, value);
|
||||
}
|
||||
|
||||
public void clear(@NonNull RenderProps context) {
|
||||
context.clear(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Prop<?> prop = (Prop<?>) o;
|
||||
|
||||
return name.equals(prop.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Prop{" +
|
||||
"name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
20
markwon/src/main/java/ru/noties/markwon/RenderProps.java
Normal file
20
markwon/src/main/java/ru/noties/markwon/RenderProps.java
Normal file
@ -0,0 +1,20 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface RenderProps {
|
||||
|
||||
@Nullable
|
||||
<T> T get(@NonNull Prop<T> prop);
|
||||
|
||||
@NonNull
|
||||
<T> T get(@NonNull Prop<T> prop, @NonNull T defValue);
|
||||
|
||||
<T> void set(@NonNull Prop<T> prop, @Nullable T value);
|
||||
|
||||
<T> void clear(@NonNull Prop<T> prop);
|
||||
}
|
51
markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java
Normal file
51
markwon/src/main/java/ru/noties/markwon/RenderPropsImpl.java
Normal file
@ -0,0 +1,51 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class RenderPropsImpl implements RenderProps {
|
||||
|
||||
private final Map<Prop, Object> values = new HashMap<>(3);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <T> T get(@NonNull Prop<T> prop) {
|
||||
//noinspection unchecked
|
||||
return (T) values.get(prop);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <T> T get(@NonNull Prop<T> prop, @NonNull T defValue) {
|
||||
Object value = values.get(prop);
|
||||
if (value != null) {
|
||||
//noinspection unchecked
|
||||
return (T) value;
|
||||
}
|
||||
return defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void set(@NonNull Prop<T> prop, @Nullable T value) {
|
||||
if (value == null) {
|
||||
values.remove(prop);
|
||||
} else {
|
||||
values.put(prop, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void clear(@NonNull Prop<T> prop) {
|
||||
values.remove(prop);
|
||||
}
|
||||
|
||||
public void clearAll() {
|
||||
values.clear();
|
||||
}
|
||||
}
|
15
markwon/src/main/java/ru/noties/markwon/SpanFactory.java
Normal file
15
markwon/src/main/java/ru/noties/markwon/SpanFactory.java
Normal file
@ -0,0 +1,15 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public interface SpanFactory {
|
||||
|
||||
@Nullable
|
||||
Object getSpans(
|
||||
@NonNull MarkwonConfiguration configuration,
|
||||
@NonNull RenderProps context);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package ru.noties.markwon.core;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.BlockQuote;
|
||||
@ -12,7 +13,9 @@ import org.commonmark.node.HardLineBreak;
|
||||
import org.commonmark.node.Heading;
|
||||
import org.commonmark.node.IndentedCodeBlock;
|
||||
import org.commonmark.node.Link;
|
||||
import org.commonmark.node.ListBlock;
|
||||
import org.commonmark.node.ListItem;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.OrderedList;
|
||||
import org.commonmark.node.Paragraph;
|
||||
import org.commonmark.node.SoftLineBreak;
|
||||
@ -21,23 +24,23 @@ import org.commonmark.node.Text;
|
||||
import org.commonmark.node.ThematicBreak;
|
||||
|
||||
import ru.noties.markwon.AbstractMarkwonPlugin;
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.MarkwonSpansFactory;
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
import ru.noties.markwon.core.visitor.BlockQuoteNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.CodeBlockNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.CodeNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.EmphasisNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.HardLineBreakNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.HeadingNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.LinkNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.ListBlockNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.ListItemNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.ParagraphNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.SoftLineBreakNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.StrongEmphasisNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.TextNodeVisitor;
|
||||
import ru.noties.markwon.core.visitor.ThematicBreakNodeVisitor;
|
||||
import ru.noties.markwon.core.factory.BlockQuoteSpanFactory;
|
||||
import ru.noties.markwon.core.factory.CodeBlockSpanFactory;
|
||||
import ru.noties.markwon.core.factory.CodeSpanFactory;
|
||||
import ru.noties.markwon.core.factory.EmphasisSpanFactory;
|
||||
import ru.noties.markwon.core.factory.HeadingSpanFactory;
|
||||
import ru.noties.markwon.core.factory.LinkSpanFactory;
|
||||
import ru.noties.markwon.core.factory.ListItemSpanFactory;
|
||||
import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory;
|
||||
import ru.noties.markwon.core.factory.ThematicBreakSpanFactory;
|
||||
import ru.noties.markwon.core.spans.OrderedListItemSpan;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class CorePlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
@ -50,8 +53,12 @@ public class CorePlugin extends AbstractMarkwonPlugin {
|
||||
return new CorePlugin(softBreakAddsNewLine);
|
||||
}
|
||||
|
||||
// todo: can we make it configurable somewhere else?
|
||||
// even possibility of options that require creating factory method for each configuration... meh
|
||||
private final boolean softBreakAddsNewLine;
|
||||
|
||||
// todo: test that visitors are registered for all expected nodes
|
||||
|
||||
protected CorePlugin(boolean softBreakAddsNewLine) {
|
||||
this.softBreakAddsNewLine = softBreakAddsNewLine;
|
||||
}
|
||||
@ -70,78 +77,315 @@ public class CorePlugin extends AbstractMarkwonPlugin {
|
||||
listItem(builder);
|
||||
thematicBreak(builder);
|
||||
heading(builder);
|
||||
softLineBreak(builder);
|
||||
softLineBreak(builder, softBreakAddsNewLine);
|
||||
hardLineBreak(builder);
|
||||
paragraph(builder);
|
||||
link(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
|
||||
// reuse this one for both code-blocks (indent & fenced)
|
||||
final CodeBlockSpanFactory codeBlockSpanFactory = new CodeBlockSpanFactory();
|
||||
|
||||
builder
|
||||
.setFactory(StrongEmphasis.class, new StrongEmphasisSpanFactory())
|
||||
.setFactory(Emphasis.class, new EmphasisSpanFactory())
|
||||
.setFactory(BlockQuote.class, new BlockQuoteSpanFactory())
|
||||
.setFactory(Code.class, new CodeSpanFactory())
|
||||
.setFactory(FencedCodeBlock.class, codeBlockSpanFactory)
|
||||
.setFactory(IndentedCodeBlock.class, codeBlockSpanFactory)
|
||||
.setFactory(ListItem.class, new ListItemSpanFactory())
|
||||
.setFactory(Heading.class, new HeadingSpanFactory())
|
||||
.setFactory(Link.class, new LinkSpanFactory())
|
||||
.setFactory(ThematicBreak.class, new ThematicBreakSpanFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) {
|
||||
OrderedListItemSpan.measure(textView, markdown);
|
||||
}
|
||||
|
||||
protected void text(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Text.class, new TextNodeVisitor());
|
||||
private static void text(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Text.class, new MarkwonVisitor.NodeVisitor<Text>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) {
|
||||
visitor.builder().append(text.getLiteral());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void strongEmphasis(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(StrongEmphasis.class, new StrongEmphasisNodeVisitor());
|
||||
private static void strongEmphasis(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(StrongEmphasis.class, new MarkwonVisitor.NodeVisitor<StrongEmphasis>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) {
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(strongEmphasis);
|
||||
visitor.setSpansForNode(strongEmphasis, length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void emphasis(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Emphasis.class, new EmphasisNodeVisitor());
|
||||
private static void emphasis(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Emphasis.class, new MarkwonVisitor.NodeVisitor<Emphasis>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) {
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(emphasis);
|
||||
visitor.setSpansForNode(emphasis, length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void blockQuote(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(BlockQuote.class, new BlockQuoteNodeVisitor());
|
||||
private static void blockQuote(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(BlockQuote.class, new MarkwonVisitor.NodeVisitor<BlockQuote>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) {
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(blockQuote);
|
||||
visitor.setSpansForNode(blockQuote, length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void code(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Code.class, new CodeNodeVisitor());
|
||||
private static void code(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Code.class, new MarkwonVisitor.NodeVisitor<Code>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Code code) {
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
// NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces
|
||||
// unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted
|
||||
visitor.builder()
|
||||
.append('\u00a0')
|
||||
.append(code.getLiteral())
|
||||
.append('\u00a0');
|
||||
|
||||
visitor.setSpansForNode(code, length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void fencedCodeBlock(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(FencedCodeBlock.class, new CodeBlockNodeVisitor.Fenced());
|
||||
private static void fencedCodeBlock(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(FencedCodeBlock.class, new MarkwonVisitor.NodeVisitor<FencedCodeBlock>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull FencedCodeBlock fencedCodeBlock) {
|
||||
visitCodeBlock(visitor, fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void indentedCodeBlock(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(IndentedCodeBlock.class, new CodeBlockNodeVisitor.Indented());
|
||||
private static void indentedCodeBlock(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(IndentedCodeBlock.class, new MarkwonVisitor.NodeVisitor<IndentedCodeBlock>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull IndentedCodeBlock indentedCodeBlock) {
|
||||
visitCodeBlock(visitor, null, indentedCodeBlock.getLiteral(), indentedCodeBlock);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void bulletList(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(BulletList.class, new ListBlockNodeVisitor());
|
||||
private static void visitCodeBlock(
|
||||
@NonNull MarkwonVisitor visitor,
|
||||
@Nullable String info,
|
||||
@NonNull String code,
|
||||
@NonNull Node node) {
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
visitor.builder()
|
||||
.append('\u00a0').append('\n')
|
||||
.append(visitor.configuration().syntaxHighlight().highlight(info, code));
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
visitor.builder().append('\u00a0');
|
||||
|
||||
visitor.setSpansForNode(node, length);
|
||||
|
||||
if (visitor.hasNext(node)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
protected void orderedList(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(OrderedList.class, new ListBlockNodeVisitor());
|
||||
private static void bulletList(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(BulletList.class, new SimpleBlockNodeVisitor());
|
||||
}
|
||||
|
||||
protected void listItem(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(ListItem.class, new ListItemNodeVisitor());
|
||||
private static void orderedList(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(OrderedList.class, new SimpleBlockNodeVisitor());
|
||||
}
|
||||
|
||||
protected void thematicBreak(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(ThematicBreak.class, new ThematicBreakNodeVisitor());
|
||||
private static void listItem(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(ListItem.class, new MarkwonVisitor.NodeVisitor<ListItem>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull ListItem listItem) {
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
final Node parent = listItem.getParent();
|
||||
if (parent instanceof OrderedList) {
|
||||
|
||||
final int start = ((OrderedList) parent).getStartNumber();
|
||||
|
||||
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED);
|
||||
CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start);
|
||||
|
||||
// after we have visited the children increment start number
|
||||
final OrderedList orderedList = (OrderedList) parent;
|
||||
orderedList.setStartNumber(orderedList.getStartNumber() + 1);
|
||||
|
||||
} else {
|
||||
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET);
|
||||
CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem));
|
||||
}
|
||||
|
||||
protected void heading(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Heading.class, new HeadingNodeVisitor());
|
||||
visitor.visitChildren(listItem);
|
||||
visitor.setSpansForNode(listItem, length);
|
||||
|
||||
if (visitor.hasNext(listItem)) {
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void softLineBreak(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(SoftLineBreak.class, new SoftLineBreakNodeVisitor(softBreakAddsNewLine));
|
||||
private static int listLevel(@NonNull Node node) {
|
||||
int level = 0;
|
||||
Node parent = node.getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof ListItem) {
|
||||
level += 1;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
protected void hardLineBreak(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(HardLineBreak.class, new HardLineBreakNodeVisitor());
|
||||
private static void thematicBreak(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(ThematicBreak.class, new MarkwonVisitor.NodeVisitor<ThematicBreak>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull ThematicBreak thematicBreak) {
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
// without space it won't render
|
||||
visitor.builder().append('\u00a0');
|
||||
|
||||
visitor.setSpansForNode(thematicBreak, length);
|
||||
|
||||
if (visitor.hasNext(thematicBreak)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void paragraph(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Paragraph.class, new ParagraphNodeVisitor());
|
||||
private static void heading(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Heading.class, new MarkwonVisitor.NodeVisitor<Heading>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(heading);
|
||||
|
||||
CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
|
||||
|
||||
visitor.setSpansForNode(heading, length);
|
||||
|
||||
if (visitor.hasNext(heading)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void link(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Link.class, new LinkNodeVisitor());
|
||||
private static void softLineBreak(@NonNull MarkwonVisitor.Builder builder, final boolean softBreakAddsNewLine) {
|
||||
builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor<SoftLineBreak>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) {
|
||||
if (softBreakAddsNewLine) {
|
||||
visitor.ensureNewLine();
|
||||
} else {
|
||||
visitor.builder().append(' ');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void hardLineBreak(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(HardLineBreak.class, new MarkwonVisitor.NodeVisitor<HardLineBreak>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull HardLineBreak hardLineBreak) {
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void paragraph(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Paragraph.class, new MarkwonVisitor.NodeVisitor<Paragraph>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Paragraph paragraph) {
|
||||
|
||||
final boolean inTightList = isInTightList(paragraph);
|
||||
|
||||
if (!inTightList) {
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(paragraph);
|
||||
|
||||
CoreProps.PARAGRAPH_IS_IN_TIGHT_LIST.set(visitor.renderProps(), inTightList);
|
||||
|
||||
// @since 1.1.1 apply paragraph span
|
||||
visitor.setSpansForNodeOptional(paragraph, length);
|
||||
|
||||
if (!inTightList && visitor.hasNext(paragraph)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isInTightList(@NonNull Paragraph paragraph) {
|
||||
final Node parent = paragraph.getParent();
|
||||
if (parent != null) {
|
||||
final Node gramps = parent.getParent();
|
||||
if (gramps instanceof ListBlock) {
|
||||
ListBlock list = (ListBlock) gramps;
|
||||
return list.isTight();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void link(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Link.class, new MarkwonVisitor.NodeVisitor<Link>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Link link) {
|
||||
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(link);
|
||||
|
||||
final MarkwonConfiguration configuration = visitor.configuration();
|
||||
final String destination = configuration.urlProcessor().process(link.getDestination());
|
||||
|
||||
CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination);
|
||||
|
||||
visitor.setSpansForNode(link, length);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
26
markwon/src/main/java/ru/noties/markwon/core/CoreProps.java
Normal file
26
markwon/src/main/java/ru/noties/markwon/core/CoreProps.java
Normal file
@ -0,0 +1,26 @@
|
||||
package ru.noties.markwon.core;
|
||||
|
||||
import ru.noties.markwon.Prop;
|
||||
|
||||
public abstract class CoreProps {
|
||||
|
||||
public static final Prop<ListItemType> LIST_ITEM_TYPE = Prop.of("list-item-type");
|
||||
|
||||
public static final Prop<Integer> BULLET_LIST_ITEM_LEVEL = Prop.of("bullet-list-item-level");
|
||||
|
||||
public static final Prop<Integer> ORDERED_LIST_ITEM_NUMBER = Prop.of("ordered-list-item-number");
|
||||
|
||||
public static final Prop<Integer> HEADING_LEVEL = Prop.of("heading-level");
|
||||
|
||||
public static final Prop<String> LINK_DESTINATION = Prop.of("link-destination");
|
||||
|
||||
public static final Prop<Boolean> PARAGRAPH_IS_IN_TIGHT_LIST = Prop.of("paragraph-is-in-tight-list");
|
||||
|
||||
public enum ListItemType {
|
||||
BULLET,
|
||||
ORDERED
|
||||
}
|
||||
|
||||
private CoreProps() {
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import ru.noties.markwon.core.spans.LinkSpan;
|
||||
*
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@Deprecated
|
||||
public interface MarkwonSpannableFactory {
|
||||
|
||||
@Nullable
|
||||
|
@ -16,6 +16,7 @@ import ru.noties.markwon.core.spans.ThematicBreakSpan;
|
||||
/**
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@Deprecated
|
||||
public class MarkwonSpannableFactoryDef implements MarkwonSpannableFactory {
|
||||
|
||||
@NonNull
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
package ru.noties.markwon.core;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
@ -6,7 +6,14 @@ import org.commonmark.node.Node;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class ListBlockNodeVisitor implements MarkwonVisitor.NodeVisitor<Node> {
|
||||
/**
|
||||
* A {@link ru.noties.markwon.MarkwonVisitor.NodeVisitor} that ensures that a markdown
|
||||
* block starts with a new line, all children are visited and if further content available
|
||||
* ensures a new line after self. Does not render any spans
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class SimpleBlockNodeVisitor implements MarkwonVisitor.NodeVisitor<Node> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
|
||||
|
@ -0,0 +1,17 @@
|
||||
package ru.noties.markwon.core.factory;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.core.spans.BlockQuoteSpan;
|
||||
|
||||
public class BlockQuoteSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new BlockQuoteSpan(configuration.theme());
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package ru.noties.markwon.core.factory;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.core.spans.CodeSpan;
|
||||
|
||||
public class CodeBlockSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new CodeSpan(configuration.theme(), true);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package ru.noties.markwon.core.factory;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.core.spans.CodeSpan;
|
||||
|
||||
public class CodeSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new CodeSpan(configuration.theme(), false);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package ru.noties.markwon.core.factory;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.core.spans.EmphasisSpan;
|
||||
|
||||
public class EmphasisSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new EmphasisSpan();
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package ru.noties.markwon.core.factory;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.core.CoreProps;
|
||||
import ru.noties.markwon.core.spans.HeadingSpan;
|
||||
|
||||
public class HeadingSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new HeadingSpan(
|
||||
configuration.theme(),
|
||||
CoreProps.HEADING_LEVEL.require(context)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package ru.noties.markwon.core.factory;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.core.CoreProps;
|
||||
import ru.noties.markwon.core.spans.LinkSpan;
|
||||
|
||||
public class LinkSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new LinkSpan(
|
||||
configuration.theme(),
|
||||
CoreProps.LINK_DESTINATION.require(context),
|
||||
configuration.linkResolver()
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package ru.noties.markwon.core.factory;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.core.CoreProps;
|
||||
import ru.noties.markwon.core.spans.BulletListItemSpan;
|
||||
import ru.noties.markwon.core.spans.OrderedListItemSpan;
|
||||
|
||||
public class ListItemSpanFactory implements SpanFactory {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
|
||||
// type of list item
|
||||
// bullet : level
|
||||
// ordered: number
|
||||
final Object spans;
|
||||
|
||||
if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(context)) {
|
||||
spans = new BulletListItemSpan(
|
||||
configuration.theme(),
|
||||
CoreProps.BULLET_LIST_ITEM_LEVEL.require(context)
|
||||
);
|
||||
} else {
|
||||
|
||||
// todo| in order to provide real RTL experience there must be a way to provide this string
|
||||
final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(context))
|
||||
+ "." + '\u00a0';
|
||||
|
||||
spans = new OrderedListItemSpan(
|
||||
configuration.theme(),
|
||||
number
|
||||
);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package ru.noties.markwon.core.factory;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.core.spans.StrongEmphasisSpan;
|
||||
|
||||
public class StrongEmphasisSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new StrongEmphasisSpan();
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package ru.noties.markwon.core.factory;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
import ru.noties.markwon.core.spans.ThematicBreakSpan;
|
||||
|
||||
public class ThematicBreakSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new ThematicBreakSpan(configuration.theme());
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.BlockQuote;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class BlockQuoteNodeVisitor implements MarkwonVisitor.NodeVisitor<BlockQuote> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) {
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
visitor.visitChildren(blockQuote);
|
||||
visitor.setSpans(length, visitor.factory().blockQuote(visitor.theme()));
|
||||
|
||||
if (visitor.hasNext(blockQuote)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.node.IndentedCodeBlock;
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public abstract class CodeBlockNodeVisitor {
|
||||
|
||||
public static class Fenced implements MarkwonVisitor.NodeVisitor<FencedCodeBlock> {
|
||||
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull FencedCodeBlock fencedCodeBlock) {
|
||||
visitCodeBlock(visitor, fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Indented implements MarkwonVisitor.NodeVisitor<IndentedCodeBlock> {
|
||||
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull IndentedCodeBlock indentedCodeBlock) {
|
||||
visitCodeBlock(visitor, null, indentedCodeBlock.getLiteral(), indentedCodeBlock);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void visitCodeBlock(
|
||||
@NonNull MarkwonVisitor visitor,
|
||||
@Nullable String info,
|
||||
@NonNull String code,
|
||||
@NonNull Node node) {
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
visitor.builder()
|
||||
.append('\u00a0').append('\n')
|
||||
.append(visitor.configuration().syntaxHighlight().highlight(info, code));
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
visitor.builder().append('\u00a0');
|
||||
|
||||
visitor.setSpans(length, visitor.factory().code(visitor.theme(), true));
|
||||
|
||||
if (visitor.hasNext(node)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Code;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class CodeNodeVisitor implements MarkwonVisitor.NodeVisitor<Code> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Code code) {
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
// NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces
|
||||
// unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted
|
||||
visitor.builder()
|
||||
.append('\u00a0')
|
||||
.append(code.getLiteral())
|
||||
.append('\u00a0');
|
||||
|
||||
visitor.setSpans(length, visitor.factory().code(visitor.theme(), false));
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Emphasis;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class EmphasisNodeVisitor implements MarkwonVisitor.NodeVisitor<Emphasis> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) {
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(emphasis);
|
||||
visitor.setSpans(length, visitor.factory().emphasis());
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.HardLineBreak;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class HardLineBreakNodeVisitor implements MarkwonVisitor.NodeVisitor<HardLineBreak> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull HardLineBreak hardLineBreak) {
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Heading;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class HeadingNodeVisitor implements MarkwonVisitor.NodeVisitor<Heading> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Heading heading) {
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(heading);
|
||||
visitor.setSpans(length, visitor.factory().heading(visitor.theme(), heading.getLevel()));
|
||||
|
||||
if (visitor.hasNext(heading)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Link;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class LinkNodeVisitor implements MarkwonVisitor.NodeVisitor<Link> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Link link) {
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(link);
|
||||
final MarkwonConfiguration configuration = visitor.configuration();
|
||||
final String destination = configuration.urlProcessor().process(link.getDestination());
|
||||
visitor.setSpans(length, visitor.factory().link(visitor.theme(), destination, configuration.linkResolver()));
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.ListItem;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.OrderedList;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class ListItemNodeVisitor implements MarkwonVisitor.NodeVisitor<ListItem> {
|
||||
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull ListItem listItem) {
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
final Node parent = listItem.getParent();
|
||||
if (parent instanceof OrderedList) {
|
||||
|
||||
final int start = ((OrderedList) parent).getStartNumber();
|
||||
|
||||
visitor.visitChildren(listItem);
|
||||
visitor.setSpans(length, visitor.factory().orderedListItem(visitor.theme(), start));
|
||||
|
||||
|
||||
// after we have visited the children increment start number
|
||||
final OrderedList orderedList = (OrderedList) parent;
|
||||
orderedList.setStartNumber(orderedList.getStartNumber() + 1);
|
||||
|
||||
} else {
|
||||
|
||||
visitor.visitChildren(listItem);
|
||||
visitor.setSpans(length, visitor.factory().bulletListItem(visitor.theme(), listLevel(listItem)));
|
||||
|
||||
}
|
||||
|
||||
if (visitor.hasNext(listItem)) {
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
private static int listLevel(@NonNull Node node) {
|
||||
int level = 0;
|
||||
Node parent = node.getParent();
|
||||
while (parent != null) {
|
||||
if (parent instanceof ListItem) {
|
||||
level += 1;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
return level;
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.ListBlock;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.Paragraph;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class ParagraphNodeVisitor implements MarkwonVisitor.NodeVisitor<Paragraph> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Paragraph paragraph) {
|
||||
|
||||
final boolean inTightList = isInTightList(paragraph);
|
||||
|
||||
if (!inTightList) {
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(paragraph);
|
||||
|
||||
// @since 1.1.1 apply paragraph span
|
||||
visitor.setSpans(length, visitor.factory().paragraph(inTightList));
|
||||
|
||||
if (!inTightList && visitor.hasNext(paragraph)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInTightList(@NonNull Paragraph paragraph) {
|
||||
final Node parent = paragraph.getParent();
|
||||
if (parent != null) {
|
||||
final Node gramps = parent.getParent();
|
||||
if (gramps instanceof ListBlock) {
|
||||
ListBlock list = (ListBlock) gramps;
|
||||
return list.isTight();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.SoftLineBreak;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class SoftLineBreakNodeVisitor implements MarkwonVisitor.NodeVisitor<SoftLineBreak> {
|
||||
|
||||
private final boolean softBreakAddsNewLine;
|
||||
|
||||
public SoftLineBreakNodeVisitor(boolean softBreakAddsNewLine) {
|
||||
this.softBreakAddsNewLine = softBreakAddsNewLine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) {
|
||||
if (softBreakAddsNewLine) {
|
||||
visitor.ensureNewLine();
|
||||
} else {
|
||||
visitor.builder().append(' ');
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.StrongEmphasis;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class StrongEmphasisNodeVisitor implements MarkwonVisitor.NodeVisitor<StrongEmphasis> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) {
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(strongEmphasis);
|
||||
visitor.setSpans(length, visitor.factory().strongEmphasis());
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Text;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class TextNodeVisitor implements MarkwonVisitor.NodeVisitor<Text> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) {
|
||||
visitor.builder().append(text.getLiteral());
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package ru.noties.markwon.core.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.ThematicBreak;
|
||||
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
|
||||
public class ThematicBreakNodeVisitor implements MarkwonVisitor.NodeVisitor<ThematicBreak> {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull ThematicBreak thematicBreak) {
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
// without space it won't render
|
||||
visitor.builder().append('\u00a0');
|
||||
|
||||
visitor.setSpans(length, visitor.factory().thematicBreak(visitor.theme()));
|
||||
|
||||
if (visitor.hasNext(thematicBreak)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.renderer.R;
|
||||
import ru.noties.markwon.core.spans.AsyncDrawableSpan;
|
||||
|
||||
public abstract class AsyncDrawableScheduler {
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ru.noties.markwon.core.spans;
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
@ -13,7 +13,6 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.image.AsyncDrawable;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class AsyncDrawableSpan extends ReplacementSpan {
|
||||
@ -32,17 +31,6 @@ public class AsyncDrawableSpan extends ReplacementSpan {
|
||||
private final int alignment;
|
||||
private final boolean replacementTextIsLink;
|
||||
|
||||
// public AsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable) {
|
||||
// this(theme, drawable, ALIGN_BOTTOM);
|
||||
// }
|
||||
|
||||
// public AsyncDrawableSpan(
|
||||
// @NonNull MarkwonTheme theme,
|
||||
// @NonNull AsyncDrawable drawable,
|
||||
// @Alignment int alignment) {
|
||||
// this(theme, drawable, alignment, false);
|
||||
// }
|
||||
|
||||
public AsyncDrawableSpan(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull AsyncDrawable drawable,
|
||||
@ -150,6 +138,7 @@ public class AsyncDrawableSpan extends ReplacementSpan {
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public AsyncDrawable getDrawable() {
|
||||
return drawable;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import ru.noties.markwon.Prop;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public abstract class ImageProps {
|
||||
|
||||
public static final Prop<String> DESTINATION = Prop.of("image-destination");
|
||||
|
||||
public static final Prop<Boolean> REPLACEMENT_TEXT_IS_LINK =
|
||||
Prop.of("image-replacement-text-is-link");
|
||||
|
||||
public static final Prop<ImageSize> IMAGE_SIZE = Prop.of("image-size");
|
||||
|
||||
|
||||
private ImageProps() {
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.SpanFactory;
|
||||
|
||||
public class ImageSpanFactory implements SpanFactory {
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps context) {
|
||||
return new AsyncDrawableSpan(
|
||||
configuration.theme(),
|
||||
new AsyncDrawable(
|
||||
ImageProps.DESTINATION.require(context),
|
||||
configuration.asyncDrawableLoader(),
|
||||
configuration.imageSizeResolver(),
|
||||
ImageProps.IMAGE_SIZE.get(context)
|
||||
),
|
||||
AsyncDrawableSpan.ALIGN_BOTTOM,
|
||||
ImageProps.REPLACEMENT_TEXT_IS_LINK.get(context, false)
|
||||
);
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package ru.noties.markwon.image;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.node.Image;
|
||||
@ -13,9 +12,9 @@ import java.util.Arrays;
|
||||
|
||||
import ru.noties.markwon.AbstractMarkwonPlugin;
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.MarkwonSpansFactory;
|
||||
import ru.noties.markwon.MarkwonVisitor;
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.core.spans.AsyncDrawableSpan;
|
||||
import ru.noties.markwon.RenderProps;
|
||||
import ru.noties.markwon.image.data.DataUriSchemeHandler;
|
||||
import ru.noties.markwon.image.file.FileSchemeHandler;
|
||||
import ru.noties.markwon.image.network.NetworkSchemeHandler;
|
||||
@ -35,7 +34,7 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
|
||||
private final Context context;
|
||||
private final boolean useAssets;
|
||||
|
||||
private ImagesPlugin(Context context, boolean useAssets) {
|
||||
protected ImagesPlugin(Context context, boolean useAssets) {
|
||||
this.context = context;
|
||||
this.useAssets = useAssets;
|
||||
}
|
||||
@ -58,6 +57,11 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
|
||||
.defaultMediaDecoder(ImageMediaDecoder.create(context.getResources()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
builder.setFactory(Image.class, new ImageSpanFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(Image.class, new MarkwonVisitor.NodeVisitor<Image>() {
|
||||
@ -77,18 +81,21 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
final Node parent = image.getParent();
|
||||
final boolean link = parent instanceof Link;
|
||||
|
||||
final String destination = configuration
|
||||
.urlProcessor()
|
||||
.process(image.getDestination());
|
||||
|
||||
final Object spans = imageSpan(
|
||||
visitor.theme(),
|
||||
destination,
|
||||
configuration.asyncDrawableLoader(),
|
||||
configuration.imageSizeResolver(),
|
||||
link);
|
||||
final RenderProps context = visitor.renderProps();
|
||||
|
||||
visitor.setSpans(length, spans);
|
||||
// apply image properties
|
||||
// Please note that we explicitly set IMAGE_SIZE to null as we do not clear
|
||||
// properties after we applied span (we could though)
|
||||
ImageProps.DESTINATION.set(context, destination);
|
||||
ImageProps.REPLACEMENT_TEXT_IS_LINK.set(context, link);
|
||||
ImageProps.IMAGE_SIZE.set(context, null);
|
||||
|
||||
visitor.setSpansForNode(image, length);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -102,24 +109,4 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
AsyncDrawableScheduler.schedule(textView);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected Object imageSpan(
|
||||
@NonNull MarkwonTheme theme,
|
||||
@NonNull String destination,
|
||||
@NonNull AsyncDrawableLoader loader,
|
||||
@NonNull ImageSizeResolver imageSizeResolver,
|
||||
boolean replacementTextIsLink) {
|
||||
return new AsyncDrawableSpan(
|
||||
theme,
|
||||
new AsyncDrawable(
|
||||
destination,
|
||||
loader,
|
||||
imageSizeResolver,
|
||||
null
|
||||
),
|
||||
AsyncDrawableSpan.ALIGN_BOTTOM,
|
||||
replacementTextIsLink
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ public class CoreTest {
|
||||
span("italic", text("bold italic"))));
|
||||
|
||||
final Spanned spanned = (Spanned) Markwon.builder(RuntimeEnvironment.application)
|
||||
.use(CorePlugin.create())
|
||||
.use(new AbstractMarkwonPlugin() {
|
||||
.usePlugin(CorePlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.factory(new MarkwonSpannableFactoryDef() {
|
||||
|
@ -33,8 +33,8 @@ abstract class BaseSuiteTest {
|
||||
@NonNull
|
||||
Markwon markwon() {
|
||||
return Markwon.builder(RuntimeEnvironment.application)
|
||||
.use(CorePlugin.create(softBreakAddsNewLine()))
|
||||
.use(new AbstractMarkwonPlugin() {
|
||||
.usePlugin(CorePlugin.create(softBreakAddsNewLine()))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.factory(new TestFactory(useParagraphs()));
|
||||
|
49
markwon/src/test/java/ru/noties/markwon/image/ImageTest.java
Normal file
49
markwon/src/test/java/ru/noties/markwon/image/ImageTest.java
Normal file
@ -0,0 +1,49 @@
|
||||
package ru.noties.markwon.image;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import ru.noties.markwon.Markwon;
|
||||
import ru.noties.markwon.core.CorePlugin;
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.test.TestSpan.Document;
|
||||
import ru.noties.markwon.test.TestSpanMatcher;
|
||||
|
||||
import static ru.noties.markwon.test.TestSpan.args;
|
||||
import static ru.noties.markwon.test.TestSpan.document;
|
||||
import static ru.noties.markwon.test.TestSpan.span;
|
||||
import static ru.noties.markwon.test.TestSpan.text;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE)
|
||||
public class ImageTest {
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
|
||||
final String markdown = "";
|
||||
|
||||
final Context context = RuntimeEnvironment.application;
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(CorePlugin.create())
|
||||
.usePlugin(new ImagesPlugin(context, false) {
|
||||
@Override
|
||||
protected Object imageSpan(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, boolean replacementTextIsLink) {
|
||||
return span("image", args("href", destination));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
final Document document = document(
|
||||
span("image", args("href", "#href"), text("alt"))
|
||||
);
|
||||
|
||||
TestSpanMatcher.matches(markwon.toMarkdown(markdown), document);
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package ru.noties.markwon.renderer.visitor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import ru.noties.markwon.AbstractMarkwonPlugin;
|
||||
import ru.noties.markwon.Markwon;
|
||||
import ru.noties.markwon.MarkwonConfiguration;
|
||||
import ru.noties.markwon.core.CorePlugin;
|
||||
import ru.noties.markwon.image.ImagesPlugin;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@RunWith(ParameterizedRobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE)
|
||||
public class SpannableMarkdownVisitorTest {
|
||||
|
||||
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
|
||||
public static Collection<Object> parameters() {
|
||||
return TestDataReader.testFiles();
|
||||
}
|
||||
|
||||
private final String file;
|
||||
|
||||
public SpannableMarkdownVisitorTest(@NonNull String file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
|
||||
final TestData data = TestDataReader.readTest(file);
|
||||
|
||||
final Markwon markwon = markwon(data.config());
|
||||
|
||||
// okay we must thing about it... casting?
|
||||
final SpannableStringBuilder stringBuilder = (SpannableStringBuilder) markwon.toMarkdown(data.input());
|
||||
|
||||
final TestValidator validator = TestValidator.create(file);
|
||||
|
||||
int index = 0;
|
||||
|
||||
for (TestNode testNode : data.output()) {
|
||||
index = validator.validate(stringBuilder, index, testNode);
|
||||
}
|
||||
|
||||
// assert that the whole thing is processed
|
||||
assertEquals("`" + stringBuilder + "`", stringBuilder.length(), index);
|
||||
|
||||
final Object[] spans = stringBuilder.getSpans(0, stringBuilder.length(), Object.class);
|
||||
final int length = spans != null
|
||||
? spans.length
|
||||
: 0;
|
||||
|
||||
assertEquals(Arrays.toString(spans), validator.processedSpanNodesCount(), length);
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
private Markwon markwon(@NonNull final TestConfig config) {
|
||||
return Markwon.builder(RuntimeEnvironment.application)
|
||||
.use(CorePlugin.create(config.hasOption(TestConfig.SOFT_BREAK_ADDS_NEW_LINE)))
|
||||
.use(ImagesPlugin.create(mock(Context.class)))
|
||||
.use(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
|
||||
builder.factory(new TestFactory(config.hasOption(TestConfig.USE_PARAGRAPHS)));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package ru.noties.markwon.renderer.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
class TestConfig {
|
||||
|
||||
static final String USE_PARAGRAPHS = "use-paragraphs";
|
||||
// static final String USE_HTML = "use-html";
|
||||
static final String SOFT_BREAK_ADDS_NEW_LINE = "soft-break-adds-new-line";
|
||||
// static final String HTML_ALLOW_NON_CLOSED_TAGS = "html-allow-non-closed-tags";
|
||||
|
||||
private final Map<String, Boolean> map;
|
||||
|
||||
TestConfig(@NonNull Map<String, Boolean> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
boolean hasOption(@NonNull String option) {
|
||||
final Boolean value = map.get(option);
|
||||
return value != null && value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TestConfig{" +
|
||||
"map=" + map +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package ru.noties.markwon.renderer.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class TestData {
|
||||
|
||||
private final String description;
|
||||
private final String input;
|
||||
private final TestConfig config;
|
||||
private final List<TestNode> output;
|
||||
|
||||
TestData(
|
||||
@Nullable String description,
|
||||
@NonNull String input,
|
||||
@NonNull TestConfig config,
|
||||
@NonNull List<TestNode> output) {
|
||||
this.description = description;
|
||||
this.input = input;
|
||||
this.config = config;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String description() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String input() {
|
||||
return input;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public TestConfig config() {
|
||||
return config;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<TestNode> output() {
|
||||
return output;
|
||||
}
|
||||
}
|
@ -1,341 +0,0 @@
|
||||
package ru.noties.markwon.renderer.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import ix.Ix;
|
||||
import ix.IxFunction;
|
||||
import ix.IxPredicate;
|
||||
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.CODE;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.CODE_BLOCK;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.EMPHASIS;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.HEADING;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.IMAGE;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.LINK;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.ORDERED_LIST;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.PARAGRAPH;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.STRONG_EMPHASIS;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.THEMATIC_BREAK;
|
||||
|
||||
abstract class TestDataReader {
|
||||
|
||||
private static final String FOLDER = "tests/";
|
||||
|
||||
@NonNull
|
||||
static Collection<Object> testFiles() {
|
||||
|
||||
final InputStream in = TestDataReader.class.getClassLoader().getResourceAsStream(FOLDER);
|
||||
if (in == null) {
|
||||
throw new RuntimeException("Cannot access test cases folder");
|
||||
}
|
||||
|
||||
try {
|
||||
//noinspection unchecked
|
||||
return (Collection) Ix.from(IOUtils.readLines(in, StandardCharsets.UTF_8))
|
||||
.filter(new IxPredicate<String>() {
|
||||
@Override
|
||||
public boolean test(String s) {
|
||||
return s.endsWith(".yaml");
|
||||
}
|
||||
})
|
||||
.map(new IxFunction<String, String>() {
|
||||
@Override
|
||||
public String apply(String s) {
|
||||
return FOLDER + s;
|
||||
}
|
||||
})
|
||||
.map(new IxFunction<String, Object[]>() {
|
||||
@Override
|
||||
public Object[] apply(String s) {
|
||||
return new Object[]{
|
||||
s
|
||||
};
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static TestData readTest(@NonNull String file) {
|
||||
return new Reader(file).read();
|
||||
}
|
||||
|
||||
private TestDataReader() {
|
||||
}
|
||||
|
||||
static class Reader {
|
||||
|
||||
private static final String TEXT = "text";
|
||||
// private static final String CELLS = "cells";
|
||||
|
||||
private static final Set<String> TAGS;
|
||||
|
||||
static {
|
||||
TAGS = new HashSet<>(Arrays.asList(
|
||||
STRONG_EMPHASIS,
|
||||
EMPHASIS,
|
||||
BLOCK_QUOTE,
|
||||
CODE,
|
||||
CODE_BLOCK,
|
||||
ORDERED_LIST,
|
||||
BULLET_LIST,
|
||||
THEMATIC_BREAK,
|
||||
HEADING,
|
||||
PARAGRAPH,
|
||||
IMAGE,
|
||||
LINK,
|
||||
HEADING + "1",
|
||||
HEADING + "2",
|
||||
HEADING + "3",
|
||||
HEADING + "4",
|
||||
HEADING + "5",
|
||||
HEADING + "6",
|
||||
TEXT
|
||||
));
|
||||
}
|
||||
|
||||
private final String file;
|
||||
|
||||
Reader(@NonNull String file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
TestData read() {
|
||||
return testData(jsonObject());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private JsonObject jsonObject() {
|
||||
try {
|
||||
final String input = IOUtils.resourceToString(file, StandardCharsets.UTF_8, TestDataReader.class.getClassLoader());
|
||||
final ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
|
||||
final Object object = objectMapper.readValue(input, Object.class);
|
||||
final ObjectMapper jsonWriter = new ObjectMapper();
|
||||
final String json = jsonWriter.writeValueAsString(object);
|
||||
return new Gson().fromJson(json, JsonObject.class);
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TestData testData(@NonNull JsonObject jsonObject) {
|
||||
|
||||
final String description;
|
||||
{
|
||||
final JsonElement element = jsonObject.get("description");
|
||||
if (element != null
|
||||
&& element.isJsonPrimitive()) {
|
||||
description = element.getAsString();
|
||||
} else {
|
||||
description = null;
|
||||
}
|
||||
}
|
||||
|
||||
final String input = jsonObject.get("input").getAsString();
|
||||
if (TextUtils.isEmpty(input)) {
|
||||
throw new RuntimeException(String.format("Test case file `%s` is missing " +
|
||||
"input parameter", file));
|
||||
}
|
||||
|
||||
final TestConfig testConfig = testConfig(jsonObject.get("config"));
|
||||
|
||||
final List<TestNode> testNodes = testNodes(jsonObject.get("output").getAsJsonArray());
|
||||
if (testNodes.size() == 0) {
|
||||
throw new RuntimeException(String.format("Test case file `%s` has no " +
|
||||
"output specified", file));
|
||||
}
|
||||
|
||||
return new TestData(
|
||||
description,
|
||||
input,
|
||||
testConfig,
|
||||
testNodes
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<TestNode> testNodes(@NonNull JsonArray array) {
|
||||
return testNodes(null, array);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<TestNode> testNodes(@Nullable TestNode parent, @NonNull JsonArray array) {
|
||||
|
||||
// an item in array is a JsonObject
|
||||
|
||||
// it can be "b": "bold" -> means Span(name="b", children=[Text(bold)]
|
||||
// or b:
|
||||
// - text: "bold" -> which is the same as above
|
||||
|
||||
// it can additionally contain "attrs" key which is the attributes
|
||||
// b:
|
||||
// - text: "bold"
|
||||
// href: "my-href"
|
||||
|
||||
final int size = array.size();
|
||||
|
||||
final List<TestNode> testNodes = new ArrayList<>(size);
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
|
||||
// if element is a string (or a json primitive) let's just add a text node
|
||||
// right away, this way we will not have to provide text with `text: "my-text"`
|
||||
// (we still can though)
|
||||
final JsonElement jsonElement = array.get(i);
|
||||
if (jsonElement.isJsonPrimitive()) {
|
||||
testNodes.add(new TestNode.Text(parent, jsonElement.getAsString()));
|
||||
continue;
|
||||
}
|
||||
|
||||
final JsonObject object = jsonElement.getAsJsonObject();
|
||||
|
||||
String name = null;
|
||||
Map<String, String> attributes = new HashMap<>(0);
|
||||
|
||||
for (String key : object.keySet()) {
|
||||
if (TAGS.contains(key)) {
|
||||
if (name == null) {
|
||||
name = key;
|
||||
} else {
|
||||
throw new RuntimeException("Unexpected key in object: " + object);
|
||||
}
|
||||
} else {
|
||||
// fill attribute map with it
|
||||
final String value;
|
||||
final JsonElement valueElement = object.get(key);
|
||||
if (valueElement.isJsonNull()) {
|
||||
value = null;
|
||||
} else {
|
||||
value = valueElement.getAsString();
|
||||
}
|
||||
// else {
|
||||
// // another special case: table cell
|
||||
// // this is not so good
|
||||
// if (CELLS.equals(key)) {
|
||||
// final JsonArray cells = valueElement.getAsJsonArray();
|
||||
// final int length = cells.size();
|
||||
// final List<TableRowSpan.Cell> list = new ArrayList<>(length);
|
||||
// for (int k = 0; k < length; k++) {
|
||||
// final JsonObject cell = cells.get(k).getAsJsonObject();
|
||||
// list.add(new TableRowSpan.Cell(
|
||||
// cell.get("alignment").getAsInt(),
|
||||
// cell.get("text").getAsString()
|
||||
// ));
|
||||
// }
|
||||
// value = list.toString();
|
||||
// } else {
|
||||
// value = valueElement.getAsString();
|
||||
// }
|
||||
// }
|
||||
attributes.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
throw new RuntimeException("Object is missing tag name: " + object);
|
||||
}
|
||||
|
||||
final JsonElement element = object.get(name);
|
||||
|
||||
if (TEXT.equals(name)) {
|
||||
testNodes.add(new TestNode.Text(parent, element.getAsString()));
|
||||
} else {
|
||||
|
||||
final List<TestNode> children = new ArrayList<>(1);
|
||||
final TestNode.Span span = new TestNode.Span(parent, name, children, attributes);
|
||||
|
||||
// if it's primitive string -> just append text node
|
||||
if (element.isJsonPrimitive()) {
|
||||
children.add(new TestNode.Text(span, element.getAsString()));
|
||||
} else if (element.isJsonArray()) {
|
||||
children.addAll(testNodes(span, element.getAsJsonArray()));
|
||||
} else {
|
||||
throw new RuntimeException("Unexpected element: " + object);
|
||||
}
|
||||
|
||||
testNodes.add(span);
|
||||
}
|
||||
}
|
||||
|
||||
return testNodes;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TestConfig testConfig(@Nullable JsonElement element) {
|
||||
|
||||
final JsonObject object = element != null && element.isJsonObject()
|
||||
? element.getAsJsonObject()
|
||||
: null;
|
||||
|
||||
final Map<String, Boolean> map;
|
||||
|
||||
if (object != null) {
|
||||
|
||||
map = new HashMap<>(object.size());
|
||||
|
||||
for (String key : object.keySet()) {
|
||||
|
||||
final JsonElement value = object.get(key);
|
||||
|
||||
if (value.isJsonPrimitive()) {
|
||||
|
||||
final JsonPrimitive jsonPrimitive = value.getAsJsonPrimitive();
|
||||
|
||||
Boolean b = null;
|
||||
|
||||
if (jsonPrimitive.isBoolean()) {
|
||||
b = jsonPrimitive.getAsBoolean();
|
||||
} else if (jsonPrimitive.isString()) {
|
||||
final String s = jsonPrimitive.getAsString();
|
||||
if ("true".equalsIgnoreCase(s)) {
|
||||
b = Boolean.TRUE;
|
||||
} else if ("false".equalsIgnoreCase(s)) {
|
||||
b = Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
if (b != null) {
|
||||
map.put(key, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
map = Collections.emptyMap();
|
||||
}
|
||||
|
||||
return new TestConfig(map);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
package ru.noties.markwon.renderer.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.core.MarkwonSpannableFactory;
|
||||
import ru.noties.markwon.core.spans.LinkSpan;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
import ru.noties.markwon.image.ImageSize;
|
||||
import ru.noties.markwon.image.ImageSizeResolver;
|
||||
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.BLOCK_QUOTE;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.BULLET_LIST;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.CODE;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.CODE_BLOCK;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.EMPHASIS;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.HEADING;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.IMAGE;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.LINK;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.ORDERED_LIST;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.PARAGRAPH;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.STRONG_EMPHASIS;
|
||||
import static ru.noties.markwon.renderer.visitor.TestSpan.THEMATIC_BREAK;
|
||||
|
||||
class TestFactory implements MarkwonSpannableFactory {
|
||||
|
||||
private final boolean useParagraphs;
|
||||
|
||||
TestFactory(boolean useParagraphs) {
|
||||
this.useParagraphs = useParagraphs;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object strongEmphasis() {
|
||||
return new TestSpan(STRONG_EMPHASIS);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object emphasis() {
|
||||
return new TestSpan(EMPHASIS);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object blockQuote(@NonNull MarkwonTheme theme) {
|
||||
return new TestSpan(BLOCK_QUOTE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object code(@NonNull MarkwonTheme theme, boolean multiline) {
|
||||
final String name = multiline
|
||||
? CODE_BLOCK
|
||||
: CODE;
|
||||
return new TestSpan(name);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object orderedListItem(@NonNull MarkwonTheme theme, int startNumber) {
|
||||
return new TestSpan(ORDERED_LIST, map("start", startNumber));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object bulletListItem(@NonNull MarkwonTheme theme, int level) {
|
||||
return new TestSpan(BULLET_LIST, map("level", level));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object thematicBreak(@NonNull MarkwonTheme theme) {
|
||||
return new TestSpan(THEMATIC_BREAK);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object heading(@NonNull MarkwonTheme theme, int level) {
|
||||
return new TestSpan(HEADING + level);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object paragraph(boolean inTightList) {
|
||||
return !useParagraphs
|
||||
? null
|
||||
: new TestSpan(PARAGRAPH);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object image(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull AsyncDrawableLoader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) {
|
||||
return new TestSpan(IMAGE, map(
|
||||
Pair.of("src", destination),
|
||||
Pair.of("imageSize", imageSize),
|
||||
Pair.of("replacementTextIsLink", replacementTextIsLink)
|
||||
));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object link(@NonNull MarkwonTheme theme, @NonNull String destination, @NonNull LinkSpan.Resolver resolver) {
|
||||
return new TestSpan(LINK, map("href", destination));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Map<String, String> map(@NonNull String key, @Nullable Object value) {
|
||||
return Collections.singletonMap(key, String.valueOf(value));
|
||||
}
|
||||
|
||||
private static class Pair {
|
||||
|
||||
static Pair of(@NonNull String key, @Nullable Object value) {
|
||||
return new Pair(key, value);
|
||||
}
|
||||
|
||||
final String key;
|
||||
final Object value;
|
||||
|
||||
Pair(@NonNull String key, @Nullable Object value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Map<String, String> map(Pair... pairs) {
|
||||
final int length = pairs.length;
|
||||
final Map<String, String> map = new HashMap<>(length);
|
||||
for (Pair pair : pairs) {
|
||||
map.put(pair.key, pair.value == null ? null : String.valueOf(pair.value));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
package ru.noties.markwon.renderer.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
abstract class TestNode {
|
||||
|
||||
private final TestNode parent;
|
||||
|
||||
TestNode(@Nullable TestNode parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TestNode parent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
abstract boolean isText();
|
||||
|
||||
abstract boolean isSpan();
|
||||
|
||||
@NonNull
|
||||
abstract Text getAsText();
|
||||
|
||||
@NonNull
|
||||
abstract Span getAsSpan();
|
||||
|
||||
|
||||
static class Text extends TestNode {
|
||||
|
||||
private final String text;
|
||||
|
||||
Text(@Nullable TestNode parent, @NonNull String text) {
|
||||
super(parent);
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String text() {
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isText() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isSpan() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
Text getAsText() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
Span getAsSpan() {
|
||||
throw new ClassCastException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Text{" +
|
||||
"text='" + text + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
static class Span extends TestNode {
|
||||
|
||||
private final String name;
|
||||
private final List<TestNode> children;
|
||||
private final Map<String, String> attributes;
|
||||
|
||||
Span(
|
||||
@Nullable TestNode parent,
|
||||
@NonNull String name,
|
||||
@NonNull List<TestNode> children,
|
||||
@NonNull Map<String, String> attributes) {
|
||||
super(parent);
|
||||
this.name = name;
|
||||
this.children = children;
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<TestNode> children() {
|
||||
return children;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Map<String, String> attributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isText() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isSpan() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
Text getAsText() {
|
||||
throw new ClassCastException();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
Span getAsSpan() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Span{" +
|
||||
"name='" + name + '\'' +
|
||||
", children=" + children +
|
||||
", attributes=" + attributes +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package ru.noties.markwon.renderer.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
class TestSpan {
|
||||
|
||||
static final String STRONG_EMPHASIS = "b";
|
||||
static final String EMPHASIS = "i";
|
||||
static final String BLOCK_QUOTE = "blockquote";
|
||||
static final String CODE = "code";
|
||||
static final String CODE_BLOCK = "code-block";
|
||||
static final String ORDERED_LIST = "ol";
|
||||
static final String BULLET_LIST = "ul";
|
||||
static final String THEMATIC_BREAK = "hr";
|
||||
static final String HEADING = "h";
|
||||
// static final String STRIKE_THROUGH = "s";
|
||||
// static final String TASK_LIST = "task-list";
|
||||
// static final String TABLE_ROW = "tr";
|
||||
static final String PARAGRAPH = "p";
|
||||
static final String IMAGE = "img";
|
||||
static final String LINK = "a";
|
||||
// static final String SUPER_SCRIPT = "sup";
|
||||
// static final String SUB_SCRIPT = "sub";
|
||||
// static final String UNDERLINE = "u";
|
||||
|
||||
|
||||
private final String name;
|
||||
private final Map<String, String> attributes;
|
||||
|
||||
TestSpan(@NonNull String name) {
|
||||
this(name, Collections.<String, String>emptyMap());
|
||||
}
|
||||
|
||||
TestSpan(@NonNull String name, @NonNull Map<String, String> attributes) {
|
||||
this.name = name;
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Map<String, String> attributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TestSpan{" +
|
||||
"name='" + name + '\'' +
|
||||
", attributes=" + attributes +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -1,199 +0,0 @@
|
||||
package ru.noties.markwon.renderer.visitor;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import ix.Ix;
|
||||
import ix.IxPredicate;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
abstract class TestValidator {
|
||||
|
||||
abstract int validate(
|
||||
@NonNull SpannableStringBuilder builder,
|
||||
int index,
|
||||
@NonNull TestNode node);
|
||||
|
||||
abstract int processedSpanNodesCount();
|
||||
|
||||
|
||||
@NonNull
|
||||
static TestValidator create(@NonNull String id) {
|
||||
return new Impl(id);
|
||||
}
|
||||
|
||||
static class Impl extends TestValidator {
|
||||
|
||||
private final String id;
|
||||
|
||||
private int processedCount;
|
||||
|
||||
Impl(@NonNull String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
int validate(
|
||||
@NonNull final SpannableStringBuilder builder,
|
||||
final int index,
|
||||
@NonNull TestNode node) {
|
||||
|
||||
if (node.isText()) {
|
||||
|
||||
final String text;
|
||||
{
|
||||
final String content = node.getAsText().text();
|
||||
|
||||
// code is a special case as we wrap it around non-breakable spaces
|
||||
final TestNode parent = node.parent();
|
||||
if (parent != null) {
|
||||
final TestNode.Span span = parent.getAsSpan();
|
||||
if (TestSpan.CODE.equals(span.name())) {
|
||||
text = "\u00a0" + content + "\u00a0";
|
||||
} else if (TestSpan.CODE_BLOCK.equals(span.name())) {
|
||||
text = "\u00a0\n" + content + "\n\u00a0";
|
||||
} else {
|
||||
text = content;
|
||||
}
|
||||
} else {
|
||||
text = content;
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
String.format("text: %s, position: {%d-%d}", text, index, index + text.length()),
|
||||
text,
|
||||
builder.subSequence(index, index + text.length()).toString());
|
||||
|
||||
return index + text.length();
|
||||
}
|
||||
|
||||
final TestNode.Span span = node.getAsSpan();
|
||||
processedCount += 1;
|
||||
|
||||
int out = index;
|
||||
|
||||
for (TestNode child : span.children()) {
|
||||
out = validate(builder, out, child);
|
||||
}
|
||||
|
||||
final int end = out;
|
||||
|
||||
// we can possibly have parent spans here, should filter them
|
||||
final Object[] spans = builder.getSpans(index, out, Object.class);
|
||||
|
||||
// expected span{name, attributes} at position{start-end}, with text: `%s`, spans: []
|
||||
|
||||
|
||||
assertTrue(
|
||||
message(span, index, end, builder, spans),
|
||||
spans != null
|
||||
);
|
||||
|
||||
final TestSpan testSpan = Ix.fromArray(spans)
|
||||
.filter(new IxPredicate<Object>() {
|
||||
@Override
|
||||
public boolean test(Object o) {
|
||||
return o instanceof TestSpan;
|
||||
}
|
||||
})
|
||||
.cast(TestSpan.class)
|
||||
.filter(new IxPredicate<TestSpan>() {
|
||||
@Override
|
||||
public boolean test(TestSpan testSpan) {
|
||||
|
||||
// in case of nested spans with the same name (lists)
|
||||
// we also must validate attributes
|
||||
// and thus we are moving most of assertions to this filter method
|
||||
return span.name().equals(testSpan.name())
|
||||
&& index == builder.getSpanStart(testSpan)
|
||||
&& end == builder.getSpanEnd(testSpan)
|
||||
&& mapEquals(span.attributes(), testSpan.attributes());
|
||||
}
|
||||
})
|
||||
.first(null);
|
||||
|
||||
assertNotNull(
|
||||
message(span, index, end, builder, spans),
|
||||
testSpan
|
||||
);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
int processedSpanNodesCount() {
|
||||
return processedCount;
|
||||
}
|
||||
|
||||
private static boolean mapEquals(
|
||||
@NonNull Map<String, String> expected,
|
||||
@NonNull Map<String, String> actual) {
|
||||
|
||||
if (expected.size() != actual.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result = true;
|
||||
|
||||
for (Map.Entry<String, String> entry : expected.entrySet()) {
|
||||
if (!actual.containsKey(entry.getKey())
|
||||
|| !equals(entry.getValue(), actual.get(entry.getKey()))) {
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean equals(@Nullable Object o1, @Nullable Object o2) {
|
||||
return o1 != null
|
||||
? o1.equals(o2)
|
||||
: o2 == null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String message(
|
||||
@NonNull TestNode.Span span,
|
||||
int start,
|
||||
int end,
|
||||
@NonNull Spanned text,
|
||||
@Nullable Object[] spans) {
|
||||
final String spansText;
|
||||
if (spans == null
|
||||
|| spans.length == 0) {
|
||||
spansText = "[]";
|
||||
} else {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (Object o : spans) {
|
||||
final TestSpan testSpan = (TestSpan) o;
|
||||
if (builder.length() > 0) {
|
||||
builder.append(", ");
|
||||
}
|
||||
|
||||
builder
|
||||
.append("{name: '").append(testSpan.name()).append('\'')
|
||||
.append(", position{").append(start).append(", ").append(end).append('}');
|
||||
|
||||
if (testSpan.attributes().size() > 0) {
|
||||
builder.append(", attributes: ").append(testSpan.attributes());
|
||||
}
|
||||
|
||||
builder.append('}');
|
||||
}
|
||||
spansText = builder.toString();
|
||||
}
|
||||
return String.format("Expected span: %s at position{%d-%d} with text `%s`, spans: %s",
|
||||
span, start, end, text.subSequence(start, end), spansText
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,6 @@ import ru.noties.markwon.MarkwonVisitor;
|
||||
import ru.noties.markwon.SpannableBuilder;
|
||||
import ru.noties.markwon.core.MarkwonTheme;
|
||||
import ru.noties.markwon.core.MarkwonSpannableFactory;
|
||||
import ru.noties.markwon.core.visitor.CodeBlockNodeVisitor;
|
||||
import ru.noties.markwon.image.AsyncDrawableLoader;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
@ -1,7 +0,0 @@
|
||||
input: ""
|
||||
|
||||
output:
|
||||
- img: "image"
|
||||
src: "#href"
|
||||
imageSize: null
|
||||
replacementTextIsLink: false
|
@ -24,6 +24,7 @@ public class IconPlugin extends AbstractMarkwonPlugin {
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.customDelimiterProcessor(IconProcessor.create());
|
||||
builder.postProcessor()
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -21,8 +21,8 @@ public class MainActivity extends Activity {
|
||||
final TextView textView = findViewById(R.id.text_view);
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.use(IconPlugin.create(IconSpanProvider.create(this, 0)))
|
||||
.use(new AbstractMarkwonPlugin() {
|
||||
.usePlugin(IconPlugin.create(IconSpanProvider.create(this, 0)))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
final float[] textSizeMultipliers = new float[]{3f, 2f, 1.5f, 1f, .5f, .25f};
|
||||
|
@ -46,11 +46,11 @@ public class MainActivity extends Activity {
|
||||
+ latex + "$$\n\n something like **this**";
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.use(CorePlugin.create())
|
||||
.usePlugin(CorePlugin.create())
|
||||
// strictly speaking this one is not required as long as JLatexMathPlugin schedules
|
||||
// drawables on it's own
|
||||
.use(ImagesPlugin.create(this))
|
||||
.use(JLatexMathPlugin.create(config))
|
||||
.usePlugin(ImagesPlugin.create(this))
|
||||
.usePlugin(JLatexMathPlugin.create(config))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, markdown);
|
||||
|
Loading…
x
Reference in New Issue
Block a user