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 index bc76e93f..2471cd4f 100644 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java +++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/AnchorHeadingPlugin.java @@ -89,7 +89,7 @@ public class AnchorHeadingPlugin extends AbstractMarkwonPlugin { } @NonNull - private static String createAnchor(@NonNull CharSequence content) { + public 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 d6f9fef0..9b9ffdde 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 @@ -58,7 +58,8 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions { .add("headingNoSpaceBlockHandler", this::headingNoSpaceBlockHandler) .add("allBlocksNoForcedLine", this::allBlocksNoForcedLine) .add("anchor", this::anchor) - .add("letterOrderedList", this::letterOrderedList); + .add("letterOrderedList", this::letterOrderedList) + .add("tableOfContents", this::tableOfContents); } @Override @@ -411,4 +412,26 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions { markwon.setMarkdown(textView, md); } + + private void tableOfContents() { + final String lorem = getString(R.string.lorem); + final String md = "" + + "# First\n" + + "" + lorem + "\n\n" + + "# Second\n" + + "" + lorem + "\n\n" + + "## Second level\n\n" + + "" + lorem + "\n\n" + + "### Level 3\n\n" + + "" + lorem + "\n\n" + + "# First again\n" + + "" + lorem + "\n\n"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new TableOfContentsPlugin()) + .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top))) + .build(); + + markwon.setMarkdown(textView, md); + } } diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/TableOfContentsPlugin.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/TableOfContentsPlugin.java new file mode 100644 index 00000000..c172ec7e --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/TableOfContentsPlugin.java @@ -0,0 +1,115 @@ +package io.noties.markwon.sample.basicplugins; + +import androidx.annotation.NonNull; + +import org.commonmark.node.AbstractVisitor; +import org.commonmark.node.BulletList; +import org.commonmark.node.CustomBlock; +import org.commonmark.node.Heading; +import org.commonmark.node.Link; +import org.commonmark.node.ListItem; +import org.commonmark.node.Node; +import org.commonmark.node.Text; + +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.core.SimpleBlockNodeVisitor; + +public class TableOfContentsPlugin extends AbstractMarkwonPlugin { + @Override + public void configure(@NonNull Registry registry) { + // just to make it explicit + registry.require(AnchorHeadingPlugin.class); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(TableOfContentsBlock.class, new SimpleBlockNodeVisitor()); + } + + @Override + public void beforeRender(@NonNull Node node) { + + // custom block to hold TOC + final TableOfContentsBlock block = new TableOfContentsBlock(); + + // create TOC title + { + final Text text = new Text("Table of contents"); + final Heading heading = new Heading(); + // important one - set TOC heading level + heading.setLevel(1); + heading.appendChild(text); + block.appendChild(heading); + } + + final HeadingVisitor visitor = new HeadingVisitor(block); + node.accept(visitor); + + // make it the very first node in rendered markdown + node.prependChild(block); + } + + private static class HeadingVisitor extends AbstractVisitor { + + private final BulletList bulletList = new BulletList(); + private final StringBuilder builder = new StringBuilder(); + private boolean isInsideHeading; + + HeadingVisitor(@NonNull Node node) { + node.appendChild(bulletList); + } + + @Override + public void visit(Heading heading) { + this.isInsideHeading = true; + try { + // reset build from previous content + builder.setLength(0); + + // obtain level (can additionally filter by level, to skip lower ones) + final int level = heading.getLevel(); + + // build heading title + visitChildren(heading); + + // initial list item + final ListItem listItem = new ListItem(); + + Node parent = listItem; + Node node = listItem; + + for (int i = 1; i < level; i++) { + final ListItem li = new ListItem(); + final BulletList bulletList = new BulletList(); + bulletList.appendChild(li); + parent.appendChild(bulletList); + parent = li; + node = li; + } + + final String content = builder.toString(); + final Link link = new Link("#" + AnchorHeadingPlugin.createAnchor(content), null); + final Text text = new Text(content); + link.appendChild(text); + node.appendChild(link); + bulletList.appendChild(listItem); + + + } finally { + isInsideHeading = false; + } + } + + @Override + public void visit(Text text) { + // can additionally check if we are building heading (to skip all other texts) + if (isInsideHeading) { + builder.append(text.getLiteral()); + } + } + } + + private static class TableOfContentsBlock extends CustomBlock { + } +}