RenderProps and SpanFactories

This commit is contained in:
Dimitry Ivanov 2018-12-22 15:38:29 +03:00
parent 69f9d0ebb8
commit 9dd3d4a94d
70 changed files with 1144 additions and 1661 deletions

View File

@ -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);

View File

@ -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

View File

@ -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
);
}
}

View File

@ -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 {

View File

@ -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);

View File

@ -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() {
}
}

View File

@ -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 {

View File

@ -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)
);
}
}

View File

@ -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
// );
}
}

View File

@ -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) {

View File

@ -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();

View File

@ -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)
);
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}

View File

@ -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));
}
}

View 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 + '\'' +
'}';
}
}

View 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);
}

View 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();
}
}

View 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);
}

View File

@ -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);
}
});
}
}

View 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() {
}
}

View File

@ -10,6 +10,7 @@ import ru.noties.markwon.core.spans.LinkSpan;
*
* @since 1.1.0
*/
@Deprecated
public interface MarkwonSpannableFactory {
@Nullable

View File

@ -16,6 +16,7 @@ import ru.noties.markwon.core.spans.ThematicBreakSpan;
/**
* @since 1.1.0
*/
@Deprecated
public class MarkwonSpannableFactoryDef implements MarkwonSpannableFactory {
@NonNull

View File

@ -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) {

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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)
);
}
}

View File

@ -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()
);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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()));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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(' ');
}
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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() {
}
}

View File

@ -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)
);
}
}

View File

@ -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
);
}
}

View File

@ -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() {

View File

@ -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()));

View 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 = "![alt](#href)";
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);
}
}

View File

@ -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();
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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 +
'}';
}
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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
);
}
}
}

View File

@ -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;

View File

@ -1,7 +0,0 @@
input: "![image](#href)"
output:
- img: "image"
src: "#href"
imageSize: null
replacementTextIsLink: false

View File

@ -24,6 +24,7 @@ public class IconPlugin extends AbstractMarkwonPlugin {
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.customDelimiterProcessor(IconProcessor.create());
builder.postProcessor()
}
@Override

View File

@ -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};

View File

@ -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);