From 3ee62a724cbda63f428d5990c15bba43e0984c44 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 24 Mar 2020 12:16:03 +0300 Subject: [PATCH] Update sample with anchor and bullet-letter plugins --- .../basicplugins/AnchorHeadingPlugin.java | 97 +++++++ .../basicplugins/BasicPluginsActivity.java | 268 ++---------------- ...tIsOrderedWithLettersWhenNestedPlugin.java | 156 ++++++++++ 3 files changed, 276 insertions(+), 245 deletions(-) create mode 100644 sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java create mode 100644 sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java new file mode 100644 index 00000000..bc76e93f --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java @@ -0,0 +1,97 @@ +package io.noties.markwon.sample.basicplugins; + +import android.text.Spannable; +import android.text.Spanned; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.LinkResolverDef; +import io.noties.markwon.MarkwonConfiguration; +import io.noties.markwon.core.spans.HeadingSpan; + +public class AnchorHeadingPlugin extends AbstractMarkwonPlugin { + + public interface ScrollTo { + void scrollTo(@NonNull TextView view, int top); + } + + private final ScrollTo scrollTo; + + AnchorHeadingPlugin(@NonNull ScrollTo scrollTo) { + this.scrollTo = scrollTo; + } + + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.linkResolver(new AnchorLinkResolver(scrollTo)); + } + + @Override + public void afterSetText(@NonNull TextView textView) { + final Spannable spannable = (Spannable) textView.getText(); + // obtain heading spans + final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class); + if (spans != null) { + for (HeadingSpan span : spans) { + final int start = spannable.getSpanStart(span); + final int end = spannable.getSpanEnd(span); + final int flags = spannable.getSpanFlags(span); + spannable.setSpan( + new AnchorSpan(createAnchor(spannable.subSequence(start, end))), + start, + end, + flags + ); + } + } + } + + private static class AnchorLinkResolver extends LinkResolverDef { + + private final ScrollTo scrollTo; + + AnchorLinkResolver(@NonNull ScrollTo scrollTo) { + this.scrollTo = scrollTo; + } + + @Override + public void resolve(@NonNull View view, @NonNull String link) { + if (link.startsWith("#")) { + final TextView textView = (TextView) view; + final Spanned spanned = (Spannable) textView.getText(); + final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class); + if (spans != null) { + final String anchor = link.substring(1); + for (AnchorSpan span : spans) { + if (anchor.equals(span.anchor)) { + final int start = spanned.getSpanStart(span); + final int line = textView.getLayout().getLineForOffset(start); + final int top = textView.getLayout().getLineTop(line); + scrollTo.scrollTo(textView, top); + return; + } + } + } + } + super.resolve(view, link); + } + } + + private static class AnchorSpan { + final String anchor; + + AnchorSpan(@NonNull String anchor) { + this.anchor = anchor; + } + } + + @NonNull + private static String createAnchor(@NonNull CharSequence content) { + return String.valueOf(content) + .replaceAll("[^\\w]", "") + .toLowerCase(); + } +} diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java index fa9ea9ca..d6f9fef0 100644 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java @@ -3,22 +3,16 @@ package io.noties.markwon.sample.basicplugins; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; -import android.text.Spannable; -import android.text.Spanned; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; -import android.view.View; import android.widget.ScrollView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.commonmark.node.BulletList; import org.commonmark.node.Heading; -import org.commonmark.node.ListItem; import org.commonmark.node.Node; -import org.commonmark.node.OrderedList; import org.commonmark.node.Paragraph; import java.util.Collection; @@ -26,21 +20,14 @@ import java.util.Collections; import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.BlockHandlerDef; -import io.noties.markwon.LinkResolverDef; import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.MarkwonVisitor; -import io.noties.markwon.Prop; -import io.noties.markwon.RenderProps; import io.noties.markwon.SoftBreakAddsNewLinePlugin; -import io.noties.markwon.SpanFactory; import io.noties.markwon.core.CoreProps; import io.noties.markwon.core.MarkwonTheme; -import io.noties.markwon.core.spans.BulletListItemSpan; -import io.noties.markwon.core.spans.HeadingSpan; import io.noties.markwon.core.spans.LastLineSpacingSpan; -import io.noties.markwon.core.spans.OrderedListItemSpan; import io.noties.markwon.image.ImageItem; import io.noties.markwon.image.ImagesPlugin; import io.noties.markwon.image.SchemeHandler; @@ -332,26 +319,26 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions { } private void headingNoSpaceBlockHandler() { -final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.blockHandler(new BlockHandlerDef() { + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { @Override - public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) { - if (node instanceof Heading) { - if (visitor.hasNext(node)) { - visitor.ensureNewLine(); - // ensure new line but do not force insert one + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.blockHandler(new BlockHandlerDef() { + @Override + public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) { + if (node instanceof Heading) { + if (visitor.hasNext(node)) { + visitor.ensureNewLine(); + // ensure new line but do not force insert one + } + } else { + super.blockEnd(visitor, node); + } } - } else { - super.blockEnd(visitor, node); - } + }); } - }); - } - }) - .build(); + }) + .build(); final String md = "" + "# Title title title title title title title title title title \n\ntext text text text"; @@ -393,85 +380,6 @@ final Markwon markwon = Markwon.builder(this) markwon.setMarkdown(textView, md); } -// public void step_6() { -// -// final Markwon markwon = Markwon.builder(this) -// .usePlugin(HtmlPlugin.create()) -// .usePlugin(new AbstractMarkwonPlugin() { -// @Override -// public void configure(@NonNull Registry registry) { -// registry.require(HtmlPlugin.class, plugin -> plugin.addHandler(new SimpleTagHandler() { -// @Override -// public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) { -// return new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER); -// } -// -// @NonNull -// @Override -// public Collection supportedTags() { -// return Collections.singleton("center"); -// } -// })); -// } -// }) -// .build(); -// } - - // text lifecycle (after/before) - // rendering lifecycle (before/after) - // renderProps - // process - - private static class AnchorSpan { - final String anchor; - - AnchorSpan(@NonNull String anchor) { - this.anchor = anchor; - } - } - - @NonNull - private String createAnchor(@NonNull CharSequence content) { - return String.valueOf(content) - .replaceAll("[^\\w]", "") - .toLowerCase(); - } - - private static class AnchorLinkResolver extends LinkResolverDef { - - interface ScrollTo { - void scrollTo(@NonNull View view, int top); - } - - private final ScrollTo scrollTo; - - AnchorLinkResolver(@NonNull ScrollTo scrollTo) { - this.scrollTo = scrollTo; - } - - @Override - public void resolve(@NonNull View view, @NonNull String link) { - if (link.startsWith("#")) { - final TextView textView = (TextView) view; - final Spanned spanned = (Spannable) textView.getText(); - final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class); - if (spans != null) { - final String anchor = link.substring(1); - for (AnchorSpan span: spans) { - if (anchor.equals(span.anchor)) { - final int start = spanned.getSpanStart(span); - final int line = textView.getLayout().getLineForOffset(start); - final int top = textView.getLayout().getLineTop(line); - scrollTo.scrollTo(textView, top); - return; - } - } - } - } - super.resolve(view, link); - } - } - private void anchor() { final String lorem = getString(R.string.lorem); final String md = "" + @@ -481,156 +389,26 @@ final Markwon markwon = Markwon.builder(this) lorem; final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { - builder.linkResolver(new AnchorLinkResolver((view, top) -> scrollView.smoothScrollTo(0, top))); - } - - @Override - public void afterSetText(@NonNull TextView textView) { - final Spannable spannable = (Spannable) textView.getText(); - // obtain heading spans - final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class); - if (spans != null) { - for (HeadingSpan span : spans) { - final int start = spannable.getSpanStart(span); - final int end = spannable.getSpanEnd(span); - final int flags = spannable.getSpanFlags(span); - spannable.setSpan( - new AnchorSpan(createAnchor(spannable.subSequence(start, end))), - start, - end, - flags - ); - } - } - } - }) + .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top))) .build(); markwon.setMarkdown(textView, md); } - private static final Prop ORDERED_IS_NESTED = Prop.of("my-ordered-is-nested"); - private void letterOrderedList() { - // first level of ordered-list is still number, - // other ordered-list levels start with `A` + // bullet list nested in ordered list renders letters instead of bullets final String md = "" + "1. Hello there!\n" + "1. And here is how:\n" + - " 1. First\n" + - " 2. Second\n" + - " 3. Third\n" + + " - First\n" + + " - Second\n" + + " - Third\n" + " 1. And first here\n\n"; final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - builder.on(ListItem.class, new MarkwonVisitor.NodeVisitor() { - @Override - public void visit(@NonNull MarkwonVisitor visitor, @NonNull ListItem listItem) { - final int length = visitor.length(); - - // it's important to visit children before applying render props ( - // we can have nested children, who are list items also, thus they will - // override out props (if we set them before visiting children) - visitor.visitChildren(listItem); - - 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); - ORDERED_IS_NESTED.set(visitor.renderProps(), isOrderedListNested(parent)); - - // 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)); - } - - visitor.setSpansForNodeOptional(listItem, length); - - if (visitor.hasNext(listItem)) { - visitor.ensureNewLine(); - } - } - }); - } - - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.setFactory(ListItem.class, new SpanFactory() { - @Override - public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) { - - // type of list item - // bullet : level - // ordered: number - final Object spans; - - if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) { - spans = new BulletListItemSpan( - configuration.theme(), - CoreProps.BULLET_LIST_ITEM_LEVEL.require(props) - ); - } else { - final int number = CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props); - final String text; - if (ORDERED_IS_NESTED.get(props, false)) { - // or `a`, or any other logic - text = ((char)('A' + number - 1)) + ".\u00a0"; - } else { - text = number + ".\u00a0"; - } - - spans = new OrderedListItemSpan( - configuration.theme(), - text - ); - } - - return spans; - } - }); - } - }) + .usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin()) .build(); markwon.setMarkdown(textView, md); } - - 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; - } - - private static boolean isOrderedListNested(@NonNull Node node) { - node = node.getParent(); - while (node != null) { - if (node instanceof OrderedList) { - return true; - } - if (node instanceof BulletList) { - return false; - } - node = node.getParent(); - } - return false; - } } diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java new file mode 100644 index 00000000..26712de0 --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BulletListIsOrderedWithLettersWhenNestedPlugin.java @@ -0,0 +1,156 @@ +package io.noties.markwon.sample.basicplugins; + +import android.text.TextUtils; +import android.util.SparseIntArray; + +import androidx.annotation.NonNull; + +import org.commonmark.node.BulletList; +import org.commonmark.node.ListItem; +import org.commonmark.node.Node; +import org.commonmark.node.OrderedList; + +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.MarkwonSpansFactory; +import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.Prop; +import io.noties.markwon.core.CoreProps; +import io.noties.markwon.core.spans.BulletListItemSpan; +import io.noties.markwon.core.spans.OrderedListItemSpan; + +public class BulletListIsOrderedWithLettersWhenNestedPlugin extends AbstractMarkwonPlugin { + + private static final Prop BULLET_LETTER = Prop.of("my-bullet-letter"); + + // or introduce some kind of synchronization if planning to use from multiple threads, + // for example via ThreadLocal + private final SparseIntArray bulletCounter = new SparseIntArray(); + + @Override + public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { + // clear counter after render + bulletCounter.clear(); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + // NB that both ordered and bullet lists are represented + // by ListItem (must inspect parent to detect the type) + builder.on(ListItem.class, (visitor, listItem) -> { + // mimic original behaviour (copy-pasta from CorePlugin) + + final int length = visitor.length(); + + visitor.visitChildren(listItem); + + 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); + + if (isBulletOrdered(parent)) { + // obtain current count value + final int count = currentBulletCountIn(parent); + BULLET_LETTER.set(visitor.renderProps(), createBulletLetter(count)); + // update current count value + setCurrentBulletCountIn(parent, count + 1); + } else { + CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem)); + // clear letter info when regular bullet list is used + BULLET_LETTER.clear(visitor.renderProps()); + } + } + + visitor.setSpansForNodeOptional(listItem, length); + + if (visitor.hasNext(listItem)) { + visitor.ensureNewLine(); + } + }); + } + + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(ListItem.class, (configuration, props) -> { + final Object spans; + + if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) { + final String letter = BULLET_LETTER.get(props); + if (!TextUtils.isEmpty(letter)) { + // NB, we are using OrderedListItemSpan here! + spans = new OrderedListItemSpan( + configuration.theme(), + letter + ); + } else { + spans = new BulletListItemSpan( + configuration.theme(), + CoreProps.BULLET_LIST_ITEM_LEVEL.require(props) + ); + } + } else { + + final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props)) + + "." + '\u00a0'; + + spans = new OrderedListItemSpan( + configuration.theme(), + number + ); + } + + return spans; + }); + } + + private int currentBulletCountIn(@NonNull Node parent) { + return bulletCounter.get(parent.hashCode(), 0); + } + + private void setCurrentBulletCountIn(@NonNull Node parent, int count) { + bulletCounter.put(parent.hashCode(), count); + } + + @NonNull + private static String createBulletLetter(int count) { + // or lower `a` + // `'u00a0` is non-breakable space char + return ((char) ('A' + count)) + ".\u00a0"; + } + + 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; + } + + private static boolean isBulletOrdered(@NonNull Node node) { + node = node.getParent(); + while (node != null) { + if (node instanceof OrderedList) { + return true; + } + if (node instanceof BulletList) { + return false; + } + node = node.getParent(); + } + return false; + } +}