Update sample with anchor and bullet-letter plugins

This commit is contained in:
Dimitry Ivanov 2020-03-24 12:16:03 +03:00
parent 54e5b27d59
commit 3ee62a724c
3 changed files with 276 additions and 245 deletions

View File

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

View File

@ -3,22 +3,16 @@ package io.noties.markwon.sample.basicplugins;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.view.View;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.commonmark.node.BulletList;
import org.commonmark.node.Heading; import org.commonmark.node.Heading;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node; import org.commonmark.node.Node;
import org.commonmark.node.OrderedList;
import org.commonmark.node.Paragraph; import org.commonmark.node.Paragraph;
import java.util.Collection; import java.util.Collection;
@ -26,21 +20,14 @@ import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.BlockHandlerDef; import io.noties.markwon.BlockHandlerDef;
import io.noties.markwon.LinkResolverDef;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.Prop;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SoftBreakAddsNewLinePlugin; import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.CoreProps; import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.MarkwonTheme; 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.LastLineSpacingSpan;
import io.noties.markwon.core.spans.OrderedListItemSpan;
import io.noties.markwon.image.ImageItem; import io.noties.markwon.image.ImageItem;
import io.noties.markwon.image.ImagesPlugin; import io.noties.markwon.image.ImagesPlugin;
import io.noties.markwon.image.SchemeHandler; import io.noties.markwon.image.SchemeHandler;
@ -332,7 +319,7 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
} }
private void headingNoSpaceBlockHandler() { private void headingNoSpaceBlockHandler() {
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() { .usePlugin(new AbstractMarkwonPlugin() {
@Override @Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
@ -393,85 +380,6 @@ final Markwon markwon = Markwon.builder(this)
markwon.setMarkdown(textView, md); 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<String> 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() { private void anchor() {
final String lorem = getString(R.string.lorem); final String lorem = getString(R.string.lorem);
final String md = "" + final String md = "" +
@ -481,156 +389,26 @@ final Markwon markwon = Markwon.builder(this)
lorem; lorem;
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() { .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
@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
);
}
}
}
})
.build(); .build();
markwon.setMarkdown(textView, md); markwon.setMarkdown(textView, md);
} }
private static final Prop<Boolean> ORDERED_IS_NESTED = Prop.of("my-ordered-is-nested");
private void letterOrderedList() { private void letterOrderedList() {
// first level of ordered-list is still number, // bullet list nested in ordered list renders letters instead of bullets
// other ordered-list levels start with `A`
final String md = "" + final String md = "" +
"1. Hello there!\n" + "1. Hello there!\n" +
"1. And here is how:\n" + "1. And here is how:\n" +
" 1. First\n" + " - First\n" +
" 2. Second\n" + " - Second\n" +
" 3. Third\n" + " - Third\n" +
" 1. And first here\n\n"; " 1. And first here\n\n";
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() { .usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin())
@Override
public void configureVisitor(@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();
// 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;
}
});
}
})
.build(); .build();
markwon.setMarkdown(textView, md); 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;
}
} }

View File

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