Core functionality plugin

This commit is contained in:
Dimitry Ivanov 2018-11-24 16:59:20 +03:00
parent 498c811987
commit 3526e16565
17 changed files with 260 additions and 68 deletions

View File

@ -41,17 +41,17 @@
<!--<data-->
<!--android:host="*"-->
<!--android:scheme="http"-->
<!--android:mimeType="text/markdown"/>-->
<!--android:mimeType="text/toMarkdown"/>-->
<!--<data-->
<!--android:host="*"-->
<!--android:scheme="file"-->
<!--android:mimeType="text/markdown"/>-->
<!--android:mimeType="text/toMarkdown"/>-->
<!--<data-->
<!--android:host="*"-->
<!--android:scheme="https"-->
<!--android:mimeType="text/markdown"/>-->
<!--android:mimeType="text/toMarkdown"/>-->
<data android:pathPattern=".*\\.markdown" />
<data android:pathPattern=".*\\.mdown" />

View File

@ -71,7 +71,7 @@ public class MainActivity extends Activity {
.use(new CorePlugin())
.use(TaskListPlugin.create(new TaskListDrawable(0xffff0000, 0xffff0000, -1)))
.build();
final CharSequence markdown = markwon2.markdown("**hello _dear_** `code`\n\n- [ ] first\n- [x] second");
final CharSequence markdown = markwon2.toMarkdown("**hello _dear_** `code`\n\n- [ ] first\n- [x] second");
textView.setText(markdown);
return;
}

View File

@ -104,7 +104,7 @@ public class MarkdownRenderer {
final long end = SystemClock.uptimeMillis();
Debug.i("markdown rendered: %d ms", end - start);
Debug.i("toMarkdown rendered: %d ms", end - start);
if (!isCancelled()) {
handler.post(new Runnable() {

View File

@ -60,7 +60,7 @@ public class MarkwonViewHelper implements IMarkwonView {
this.provider = provider;
this.configuration = provider.provide(textView.getContext());
if (!TextUtils.isEmpty(markdown)) {
// invalidate rendered markdown
// invalidate rendered toMarkdown
setMarkdown(markdown);
}
}

View File

@ -35,12 +35,12 @@ public abstract class AbstractMarkwonPlugin implements MarkwonPlugin {
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder) {
public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) {
}
@Override
public void afterSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder) {
public void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown) {
}
}

View File

@ -48,12 +48,12 @@ public abstract class Markwon {
}
/**
* Parses submitted raw markdown, converts it to CharSequence (with Spannables)
* Parses submitted raw toMarkdown, converts it to CharSequence (with Spannables)
* and applies it to view
*
* @param view {@link TextView} to set markdown into
* @param view {@link TextView} to set toMarkdown into
* @param configuration a {@link MarkwonConfiguration} instance
* @param markdown raw markdown String (for example: {@code `**Hello**`})
* @param markdown raw toMarkdown String (for example: {@code `**Hello**`})
* @see #markdown(MarkwonConfiguration, String)
* @see #setText(TextView, CharSequence)
* @see MarkwonConfiguration
@ -69,13 +69,13 @@ public abstract class Markwon {
}
/**
* Helper method to apply parsed markdown.
* Helper method to apply parsed toMarkdown.
* <p>
* Since 1.0.6 redirects it\'s call to {@link #setText(TextView, CharSequence, MovementMethod)}
* with LinkMovementMethod as an argument to preserve current API.
*
* @param view {@link TextView} to set markdown into
* @param text parsed markdown
* @param view {@link TextView} to set toMarkdown into
* @param text parsed toMarkdown
* @see #setText(TextView, CharSequence, MovementMethod)
* @since 1.0.0
*/
@ -84,13 +84,13 @@ public abstract class Markwon {
}
/**
* Helper method to apply parsed markdown with additional argument of a MovementMethod. Used
* Helper method to apply parsed toMarkdown with additional argument of a MovementMethod. Used
* to workaround problems that occur when using system LinkMovementMethod (for example:
* https://issuetracker.google.com/issues/37068143). As a better alternative to it consider
* using: https://github.com/saket/Better-Link-Movement-Method
*
* @param view TextView to set markdown into
* @param text parsed markdown
* @param view TextView to set toMarkdown into
* @param text parsed toMarkdown
* @param movementMethod an implementation if MovementMethod or null
* @see #scheduleDrawables(TextView)
* @see #scheduleTableRows(TextView)
@ -102,7 +102,7 @@ public abstract class Markwon {
unscheduleTableRows(view);
// @since 2.0.1 we must measure ordered-list-item-spans before applying text to a TextView.
// if markdown has a lot of ordered list items (or text size is relatively big, or block-margin
// if toMarkdown has a lot of ordered list items (or text size is relatively big, or block-margin
// is relatively small) then this list won't be rendered properly: it will take correct
// layout (width and margin) but will be clipped if margin is not _consistent_ between calls.
OrderedListItemSpan.measure(view, text);
@ -117,11 +117,11 @@ public abstract class Markwon {
}
/**
* Returns parsed markdown with default {@link MarkwonConfiguration} obtained from {@link Context}
* Returns parsed toMarkdown with default {@link MarkwonConfiguration} obtained from {@link Context}
*
* @param context {@link Context}
* @param markdown raw markdown
* @return parsed markdown
* @param markdown raw toMarkdown
* @return parsed toMarkdown
* @since 1.0.0
*/
@NonNull
@ -131,11 +131,11 @@ public abstract class Markwon {
}
/**
* Returns parsed markdown with provided {@link MarkwonConfiguration}
* Returns parsed toMarkdown with provided {@link MarkwonConfiguration}
*
* @param configuration a {@link MarkwonConfiguration}
* @param markdown raw markdown
* @return parsed markdown
* @param markdown raw toMarkdown
* @return parsed toMarkdown
* @see MarkwonConfiguration
* @since 1.0.0
*/

View File

@ -2,6 +2,7 @@ package ru.noties.markwon;
import android.content.Context;
import android.support.annotation.NonNull;
import android.widget.TextView;
import org.commonmark.node.Node;
@ -18,8 +19,13 @@ public abstract class Markwon2 {
@NonNull
public abstract CharSequence render(@NonNull Node node);
// parse + render
@NonNull
public abstract CharSequence markdown(@NonNull String input);
public abstract CharSequence toMarkdown(@NonNull String input);
public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown);
public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown);
public interface Builder {

View File

@ -46,7 +46,7 @@ class MarkwonBuilderImpl implements Markwon2.Builder {
return new MarkwonImpl(
parserBuilder.build(),
visitorBuilder.build(themeBuilder.build(), configurationBuilder.build()),
visitorBuilder.build(configurationBuilder.build(themeBuilder.build())),
Collections.unmodifiableList(plugins)
);
}

View File

@ -20,7 +20,7 @@ public class MarkwonConfiguration {
// creates default configuration
@NonNull
public static MarkwonConfiguration create(@NonNull Context context) {
return new Builder(context).build();
return new Builder(context).build(MarkwonTheme.create(context));
}
@NonNull
@ -28,9 +28,8 @@ public class MarkwonConfiguration {
return new Builder(context);
}
@Deprecated
private final MarkwonTheme theme;
private final MarkwonTheme theme;
private final AsyncDrawable.Loader asyncDrawableLoader;
private final SyntaxHighlight syntaxHighlight;
private final LinkSpan.Resolver linkResolver;
@ -65,7 +64,6 @@ public class MarkwonConfiguration {
}
@NonNull
@Deprecated
public MarkwonTheme theme() {
return theme;
}
@ -137,7 +135,6 @@ public class MarkwonConfiguration {
private final Context context;
@Deprecated
private MarkwonTheme theme;
private AsyncDrawable.Loader asyncDrawableLoader;
private SyntaxHighlight syntaxHighlight;
@ -169,12 +166,12 @@ public class MarkwonConfiguration {
this.htmlAllowNonClosedTags = configuration.htmlAllowNonClosedTags;
}
@NonNull
@Deprecated
public Builder theme(@NonNull MarkwonTheme theme) {
this.theme = theme;
return this;
}
// @NonNull
// @Deprecated
// public Builder theme(@NonNull MarkwonTheme theme) {
// this.theme = theme;
// return this;
// }
@NonNull
public Builder asyncDrawableLoader(@NonNull AsyncDrawable.Loader asyncDrawableLoader) {
@ -263,11 +260,9 @@ public class MarkwonConfiguration {
}
@NonNull
public MarkwonConfiguration build() {
public MarkwonConfiguration build(@NonNull MarkwonTheme theme) {
if (theme == null) {
theme = MarkwonTheme.create(context);
}
this.theme = theme;
if (asyncDrawableLoader == null) {
asyncDrawableLoader = new AsyncDrawableLoaderNoOp();

View File

@ -1,6 +1,7 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
import android.widget.TextView;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
@ -42,7 +43,26 @@ class MarkwonImpl extends Markwon2 {
@NonNull
@Override
public CharSequence markdown(@NonNull String input) {
public CharSequence toMarkdown(@NonNull String input) {
return render(parse(input));
}
@Override
public void setMarkdown(@NonNull TextView textView, @NonNull String markdown) {
setParsedMarkdown(textView, toMarkdown(markdown));
}
@Override
public void setParsedMarkdown(@NonNull TextView textView, @NonNull CharSequence markdown) {
for (MarkwonPlugin plugin : plugins) {
plugin.beforeSetText(textView, markdown);
}
textView.setText(markdown);
for (MarkwonPlugin plugin : plugins) {
plugin.afterSetText(textView, markdown);
}
}
}

View File

@ -23,7 +23,7 @@ public interface MarkwonPlugin {
@NonNull
String processMarkdown(@NonNull String markdown);
void beforeSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder);
void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown);
void afterSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder);
void afterSetText(@NonNull TextView textView, @NonNull CharSequence markdown);
}

View File

@ -20,14 +20,17 @@ public interface MarkwonVisitor extends Visitor {
<N extends Node> Builder on(@NonNull Class<N> node, @NonNull NodeVisitor<N> nodeVisitor);
@NonNull
MarkwonVisitor build(@NonNull MarkwonTheme theme, @NonNull MarkwonConfiguration configuration);
MarkwonVisitor build(@NonNull MarkwonConfiguration configuration);
}
@NonNull
MarkwonConfiguration configuration();
@NonNull
MarkwonTheme theme();
@NonNull
MarkwonConfiguration configuration();
SpannableFactory factory();
@NonNull
SpannableBuilder builder();

View File

@ -37,19 +37,21 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
private final Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes;
private final MarkwonTheme theme;
private final MarkwonConfiguration configuration;
private final MarkwonTheme theme;
private final SpannableFactory factory;
private final SpannableBuilder builder = new SpannableBuilder();
private int blockQuoteIndent;
private int listLevel;
private MarkwonVisitorImpl(
@NonNull MarkwonTheme theme,
@NonNull MarkwonConfiguration configuration,
@NonNull Map<Class<? extends Node>, NodeVisitor<? extends Node>> nodes) {
this.theme = theme;
this.configuration = configuration;
this.theme = configuration.theme();
this.factory = configuration.factory();
this.nodes = nodes;
}
@ -173,6 +175,12 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
}
}
@NonNull
@Override
public MarkwonConfiguration configuration() {
return configuration;
}
@NonNull
@Override
public MarkwonTheme theme() {
@ -181,8 +189,8 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
@NonNull
@Override
public MarkwonConfiguration configuration() {
return configuration;
public SpannableFactory factory() {
return factory;
}
@NonNull
@ -280,9 +288,8 @@ class MarkwonVisitorImpl implements MarkwonVisitor {
@NonNull
@Override
public MarkwonVisitor build(@NonNull MarkwonTheme theme, @NonNull MarkwonConfiguration configuration) {
public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration) {
return new MarkwonVisitorImpl(
theme,
configuration,
Collections.unmodifiableMap(nodes));
}

View File

@ -9,17 +9,24 @@ import org.commonmark.node.BulletList;
import org.commonmark.node.Code;
import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading;
import org.commonmark.node.Image;
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;
import org.commonmark.node.StrongEmphasis;
import org.commonmark.node.Text;
import org.commonmark.node.ThematicBreak;
import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.SpannableBuilder;
import ru.noties.markwon.spans.OrderedListItemSpan;
public class CorePlugin extends AbstractMarkwonPlugin {
@ -30,6 +37,25 @@ public class CorePlugin extends AbstractMarkwonPlugin {
// todo: softBreak adds new line should be here (or maybe removed even?)
// todo: add a simple HTML handler
// todo: configure primitive images (without okhttp -> just HttpUrlConnection and simple types (static, data)
@NonNull
public static CorePlugin create() {
return create(false);
}
@NonNull
public static CorePlugin create(boolean softBreakAddsNewLine) {
return new CorePlugin(softBreakAddsNewLine);
}
private final boolean softBreakAddsNewLine;
protected CorePlugin(boolean softBreakAddsNewLine) {
this.softBreakAddsNewLine = softBreakAddsNewLine;
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
text(builder);
@ -43,11 +69,17 @@ public class CorePlugin extends AbstractMarkwonPlugin {
orderedList(builder);
listItem(builder);
thematicBreak(builder);
heading(builder);
softLineBreak(builder);
hardLineBreak(builder);
paragraph(builder);
image(builder);
link(builder);
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull SpannableBuilder builder) {
OrderedListItemSpan.measure(textView, builder);
public void beforeSetText(@NonNull TextView textView, @NonNull CharSequence markdown) {
OrderedListItemSpan.measure(textView, markdown);
}
protected void text(@NonNull MarkwonVisitor.Builder builder) {
@ -65,7 +97,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) {
final int length = visitor.length();
visitor.visitChildren(strongEmphasis);
visitor.setSpans(length, visitor.configuration().factory().strongEmphasis());
visitor.setSpans(length, visitor.factory().strongEmphasis());
}
});
}
@ -76,7 +108,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) {
final int length = visitor.length();
visitor.visitChildren(emphasis);
visitor.setSpans(length, visitor.configuration().factory().emphasis());
visitor.setSpans(length, visitor.factory().emphasis());
}
});
}
@ -95,7 +127,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
final int length = visitor.length();
visitor.incrementBlockQuoteIndent();
visitor.visitChildren(blockQuote);
visitor.setSpans(length, visitor.configuration().factory().blockQuote(visitor.theme()));
visitor.setSpans(length, visitor.factory().blockQuote(visitor.theme()));
visitor.decrementBlockQuoteIndent();
if (visitor.hasNext(blockQuote)) {
@ -122,7 +154,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
.append(code.getLiteral())
.append('\u00a0');
visitor.setSpans(length, visitor.configuration().factory().code(visitor.theme(), false));
visitor.setSpans(length, visitor.factory().code(visitor.theme(), false));
}
});
}
@ -163,7 +195,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
visitor.builder().append('\u00a0');
visitor.setSpans(length, visitor.configuration().factory().code(visitor.theme(), true));
visitor.setSpans(length, visitor.factory().code(visitor.theme(), true));
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
@ -220,7 +252,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
final int start = ((OrderedList) parent).getStartNumber();
visitor.visitChildren(listItem);
visitor.setSpans(length, visitor.configuration().factory().orderedListItem(visitor.theme(), start));
visitor.setSpans(length, visitor.factory().orderedListItem(visitor.theme(), start));
// after we have visited the children increment start number
@ -230,7 +262,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
} else {
visitor.visitChildren(listItem);
visitor.setSpans(length, visitor.configuration().factory().bulletListItem(visitor.theme(), visitor.listLevel() - 1));
visitor.setSpans(length, visitor.factory().bulletListItem(visitor.theme(), visitor.listLevel() - 1));
}
@ -256,7 +288,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
// without space it won't render
visitor.builder().append('\u00a0');
visitor.setSpans(length, visitor.configuration().factory().thematicBreak(visitor.theme()));
visitor.setSpans(length, visitor.factory().thematicBreak(visitor.theme()));
if (visitor.hasNext(thematicBreak)) {
visitor.ensureNewLine();
@ -265,4 +297,132 @@ public class CorePlugin extends AbstractMarkwonPlugin {
}
});
}
protected 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);
visitor.setSpans(length, visitor.factory().heading(visitor.theme(), heading.getLevel()));
if (visitor.hasNext(heading)) {
visitor.ensureNewLine();
visitor.forceNewLine();
}
}
});
}
protected void softLineBreak(@NonNull MarkwonVisitor.Builder builder) {
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(' ');
}
}
});
}
protected 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();
}
});
}
protected 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);
// @since 1.1.1 apply paragraph span
visitor.setSpans(length, visitor.factory().paragraph(inTightList));
if (!inTightList && visitor.hasNext(paragraph)) {
visitor.ensureNewLine();
if (visitor.blockQuoteIndent() == 0) {
visitor.forceNewLine();
}
}
}
});
}
protected void image(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Image.class, new MarkwonVisitor.NodeVisitor<Image>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) {
final int length = visitor.length();
visitor.visitChildren(image);
// we must check if anything _was_ added, as we need at least one char to render
if (length == visitor.length()) {
visitor.builder().append('\uFFFC');
}
final MarkwonConfiguration configuration = visitor.configuration();
final Node parent = image.getParent();
final boolean link = parent instanceof Link;
final String destination = configuration
.urlProcessor()
.process(image.getDestination());
final Object spans = visitor.factory().image(
visitor.theme(),
destination,
configuration.asyncDrawableLoader(),
configuration.imageSizeResolver(),
null,
link);
visitor.setSpans(length, spans);
}
});
}
protected 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());
visitor.setSpans(length, visitor.factory().link(visitor.theme(), destination, configuration.linkResolver()));
}
});
}
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

@ -20,8 +20,8 @@ public class OrderedListItemSpan implements LeadingMarginSpan {
* NB, this method must be called <em>before</em> setting text to a TextView (`TextView#setText`
* internally can trigger new Layout creation which will ask for leading margins right away)
*
* @param textView to which markdown will be applied
* @param text parsed markdown to process
* @param textView to which toMarkdown will be applied
* @param text parsed toMarkdown to process
* @since 2.0.1
*/
public static void measure(@NonNull TextView textView, @NonNull CharSequence text) {

View File

@ -4,6 +4,7 @@ import android.text.Spanned;
public abstract class LeadingMarginUtils {
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean selfStart(int start, CharSequence text, Object span) {
return text instanceof Spanned && ((Spanned) text).getSpanStart(span) == start;
}

View File

@ -47,7 +47,7 @@ public class MainActivity extends Activity {
final SpannableBuilder builder = new SpannableBuilder();
// please note that here I am passing `0` as fallback it means that if markdown references
// please note that here I am passing `0` as fallback it means that if toMarkdown references
// unknown icon, it will try to load fallback one and will fail with ResourceNotFound. It's
// better to provide a valid fallback option
final IconSpanProvider spanProvider = IconSpanProvider.create(this, 0);
@ -59,7 +59,7 @@ public class MainActivity extends Activity {
.headingTextSizeMultipliers(textSizeMultipliers)
.build())
.build();
// create an instance of visitor to process parsed markdown
// create an instance of visitor to process parsed toMarkdown
final IconVisitor visitor = new IconVisitor(
configuration,
builder,