diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0837eee..4f0746e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])
Thanks to [@tylerbwong]
+* `MarkwonInlineParser` to customize inline parsing (new module: `markwon-inline-parser`)
* `Markwon#configuration` method to expose `MarkwonConfiguration` via public API
* `HeadingSpan#getLevel` getter
* Add `SvgPictureMediaDecoder` in `image` module to deal with SVG without dimensions ([#165])
diff --git a/build.gradle b/build.gradle
index 180abe37..05de0777 100644
--- a/build.gradle
+++ b/build.gradle
@@ -82,11 +82,12 @@ ext {
]
deps['test'] = [
- 'junit' : 'junit:junit:4.12',
- 'robolectric': 'org.robolectric:robolectric:3.8',
- 'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
- 'commons-io' : 'commons-io:commons-io:2.6',
- 'mockito' : 'org.mockito:mockito-core:2.21.0'
+ 'junit' : 'junit:junit:4.12',
+ 'robolectric' : 'org.robolectric:robolectric:3.8',
+ 'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
+ 'commons-io' : 'commons-io:commons-io:2.6',
+ 'mockito' : 'org.mockito:mockito-core:2.21.0',
+ 'commonmark-test-util': "com.atlassian.commonmark:commonmark-test-util:$commonMarkVersion",
]
registerArtifact = this.®isterArtifact
diff --git a/docs/.vuepress/.artifacts.js b/docs/.vuepress/.artifacts.js
index 50625f79..a9880b7d 100644
--- a/docs/.vuepress/.artifacts.js
+++ b/docs/.vuepress/.artifacts.js
@@ -1,4 +1,4 @@
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
-const artifacts = [{"id":"core","name":"Core","group":"io.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"editor","name":"Editor","group":"io.noties.markwon","description":"Markdown editor based on Markwon"},{"id":"ext-latex","name":"LaTeX","group":"io.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"io.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"io.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"io.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"io.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image","name":"Image","group":"io.noties.markwon","description":"Markwon image loading module (with optional GIF and SVG support)"},{"id":"image-glide","name":"Image Glide","group":"io.noties.markwon","description":"Markwon image loading module (based on Glide library)"},{"id":"image-picasso","name":"Image Picasso","group":"io.noties.markwon","description":"Markwon image loading module (based on Picasso library)"},{"id":"linkify","name":"Linkify","group":"io.noties.markwon","description":"Markwon plugin to linkify text (based on Android Linkify)"},{"id":"recycler","name":"Recycler","group":"io.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"io.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"simple-ext","name":"Simple Extension","group":"io.noties.markwon","description":"Custom extension based on simple delimiter usage"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"io.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
+const artifacts = [{"id":"core","name":"Core","group":"io.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"editor","name":"Editor","group":"io.noties.markwon","description":"Markdown editor based on Markwon"},{"id":"ext-latex","name":"LaTeX","group":"io.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"io.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"io.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"io.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"io.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image","name":"Image","group":"io.noties.markwon","description":"Markwon image loading module (with optional GIF and SVG support)"},{"id":"image-coil","name":"Image Coil","group":"io.noties.markwon","description":"Markwon image loading module (based on Coil library)"},{"id":"image-glide","name":"Image Glide","group":"io.noties.markwon","description":"Markwon image loading module (based on Glide library)"},{"id":"image-picasso","name":"Image Picasso","group":"io.noties.markwon","description":"Markwon image loading module (based on Picasso library)"},{"id":"inline-parser","name":"Inline Parser","group":"io.noties.markwon","description":"Markwon customizable commonmark-java InlineParser"},{"id":"linkify","name":"Linkify","group":"io.noties.markwon","description":"Markwon plugin to linkify text (based on Android Linkify)"},{"id":"recycler","name":"Recycler","group":"io.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"io.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"simple-ext","name":"Simple Extension","group":"io.noties.markwon","description":"Custom extension based on simple delimiter usage"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"io.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
export { artifacts };
diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js
index 6a5e53cf..32d0cd9a 100644
--- a/docs/.vuepress/config.js
+++ b/docs/.vuepress/config.js
@@ -105,6 +105,7 @@ module.exports = {
'/docs/v4/image-coil/',
'/docs/v4/image-glide/',
'/docs/v4/image-picasso/',
+ '/docs/v4/inline-parser/',
'/docs/v4/linkify/',
'/docs/v4/recycler/',
'/docs/v4/recycler-table/',
diff --git a/docs/docs/v4/inline-parser/README.md b/docs/docs/v4/inline-parser/README.md
new file mode 100644
index 00000000..4c5471a1
--- /dev/null
+++ b/docs/docs/v4/inline-parser/README.md
@@ -0,0 +1,78 @@
+# Inline Parser
+
+**Experimental** commonmark-java inline parser that allows customizing
+core features and/or extend with own.
+
+Usage of _internal_ classes:
+```java
+import org.commonmark.internal.Bracket;
+import org.commonmark.internal.Delimiter;
+import org.commonmark.internal.ReferenceParser;
+import org.commonmark.internal.util.Escaping;
+import org.commonmark.internal.util.Html5Entities;
+import org.commonmark.internal.util.Parsing;
+import org.commonmark.internal.inline.AsteriskDelimiterProcessor;
+import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
+```
+
+---
+
+```java
+// all default (like current commonmark-java InlineParserImpl)
+final InlineParserFactory factory = MarkwonInlineParser.factoryBuilder()
+ .includeDefaults()
+ .build();
+```
+
+```java
+// disable images (current markdown images will be considered as links):
+final InlineParserFactory factory = MarkwonInlineParser.factoryBuilder()
+ .includeDefaults()
+ .excludeInlineProcessor(BangInlineProcessor.class)
+ .build();
+```
+
+```java
+// disable core delimiter processors for `*`|`_` and `**`|`__`
+final InlineParserFactory factory = MarkwonInlineParser.factoryBuilder()
+ .includeDefaults()
+ .excludeDelimiterProcessor(AsteriskDelimiterProcessor.class)
+ .excludeDelimiterProcessor(UnderscoreDelimiterProcessor.class)
+ .build();
+```
+
+```java
+// disable _all_ markdown inlines except for links (open and close bracket handling `[` & `]`)
+final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
+ // note that there is no `includeDefaults` method call
+ .referencesEnabled(true)
+ .addInlineProcessor(new OpenBracketInlineProcessor())
+ .addInlineProcessor(new CloseBracketInlineProcessor())
+ .build();
+```
+
+To use custom InlineParser:
+```java
+final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureParser(@NonNull Parser.Builder builder) {
+ builder.inlineParserFactory(inlineParserFactory);
+ }
+ })
+ .build();
+```
+
+---
+
+The list of available inline processors:
+
+* `AutolinkInlineProcessor` (`<` => ``)
+* `BackslashInlineProcessor` (`\\`)
+* `BackticksInlineProcessor` (`
=> `code`
)
+* `BangInlineProcessor` (`!` => ``)
+* `CloseBracketInlineProcessor` (`]` => `[link](#href)`, ``)
+* `EntityInlineProcessor` (`&` => `&`)
+* `HtmlInlineProcessor` (`<` => ``)
+* `NewLineInlineProcessor` (`\n`)
+* `OpenBracketInlineProcessor` (`[` => `[link](#href)`)
\ No newline at end of file
diff --git a/markwon-inline-parser/README.md b/markwon-inline-parser/README.md
new file mode 100644
index 00000000..bcfa3802
--- /dev/null
+++ b/markwon-inline-parser/README.md
@@ -0,0 +1,16 @@
+# Inline parser
+
+**Experimental** due to usage of internal (but still visible) classes of commonmark-java:
+
+```java
+import org.commonmark.internal.Bracket;
+import org.commonmark.internal.Delimiter;
+import org.commonmark.internal.ReferenceParser;
+import org.commonmark.internal.util.Escaping;
+import org.commonmark.internal.util.Html5Entities;
+import org.commonmark.internal.util.Parsing;
+import org.commonmark.internal.inline.AsteriskDelimiterProcessor;
+import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
+```
+
+`StaggeredDelimiterProcessor` class source is copied (required for InlineParser)
\ No newline at end of file
diff --git a/markwon-inline-parser/build.gradle b/markwon-inline-parser/build.gradle
new file mode 100644
index 00000000..703a18ff
--- /dev/null
+++ b/markwon-inline-parser/build.gradle
@@ -0,0 +1,26 @@
+apply plugin: 'com.android.library'
+
+android {
+
+ compileSdkVersion config['compile-sdk']
+ buildToolsVersion config['build-tools']
+
+ defaultConfig {
+ minSdkVersion config['min-sdk']
+ targetSdkVersion config['target-sdk']
+ versionCode 1
+ versionName version
+ }
+}
+
+dependencies {
+ api deps['x-annotations']
+ api deps['commonmark']
+
+ deps['test'].with {
+ testImplementation it['junit']
+ testImplementation it['commonmark-test-util']
+ }
+}
+
+registerArtifact(this)
\ No newline at end of file
diff --git a/markwon-inline-parser/gradle.properties b/markwon-inline-parser/gradle.properties
new file mode 100644
index 00000000..264a18ee
--- /dev/null
+++ b/markwon-inline-parser/gradle.properties
@@ -0,0 +1,4 @@
+POM_NAME=Inline Parser
+POM_ARTIFACT_ID=inline-parser
+POM_DESCRIPTION=Markwon customizable commonmark-java InlineParser
+POM_PACKAGING=aar
\ No newline at end of file
diff --git a/markwon-inline-parser/src/main/AndroidManifest.xml b/markwon-inline-parser/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..1a8bcbb5
--- /dev/null
+++ b/markwon-inline-parser/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/AutolinkInlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/AutolinkInlineProcessor.java
new file mode 100644
index 00000000..6351fe64
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/AutolinkInlineProcessor.java
@@ -0,0 +1,45 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.node.Link;
+import org.commonmark.node.Text;
+
+import java.util.regex.Pattern;
+
+/**
+ * Parses autolinks, for example {@code }
+ *
+ * @since 4.2.0-SNAPSHOT
+ */
+public class AutolinkInlineProcessor extends InlineProcessor {
+
+ private static final Pattern EMAIL_AUTOLINK = Pattern
+ .compile("^<([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>");
+
+ private static final Pattern AUTOLINK = Pattern
+ .compile("^<[a-zA-Z][a-zA-Z0-9.+-]{1,31}:[^<>\u0000-\u0020]*>");
+
+ @Override
+ public char specialCharacter() {
+ return '<';
+ }
+
+ @Override
+ protected boolean parse() {
+ String m;
+ if ((m = match(EMAIL_AUTOLINK)) != null) {
+ String dest = m.substring(1, m.length() - 1);
+ Link node = new Link("mailto:" + dest, null);
+ node.appendChild(new Text(dest));
+ appendNode(node);
+ return true;
+ } else if ((m = match(AUTOLINK)) != null) {
+ String dest = m.substring(1, m.length() - 1);
+ Link node = new Link(dest, null);
+ node.appendChild(new Text(dest));
+ appendNode(node);
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/BackslashInlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/BackslashInlineProcessor.java
new file mode 100644
index 00000000..e8f433ca
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/BackslashInlineProcessor.java
@@ -0,0 +1,33 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.node.HardLineBreak;
+
+import java.util.regex.Pattern;
+
+/**
+ * @since 4.2.0-SNAPSHOT
+ */
+public class BackslashInlineProcessor extends InlineProcessor {
+
+ private static final Pattern ESCAPABLE = MarkwonInlineParser.ESCAPABLE;
+
+ @Override
+ public char specialCharacter() {
+ return '\\';
+ }
+
+ @Override
+ protected boolean parse() {
+ index++;
+ if (peek() == '\n') {
+ appendNode(new HardLineBreak());
+ index++;
+ } else if (index < input.length() && ESCAPABLE.matcher(input.substring(index, index + 1)).matches()) {
+ appendText(input, index, index + 1);
+ index++;
+ } else {
+ appendText("\\");
+ }
+ return true;
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/BackticksInlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/BackticksInlineProcessor.java
new file mode 100644
index 00000000..f0c8da9c
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/BackticksInlineProcessor.java
@@ -0,0 +1,48 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.node.Code;
+
+import java.util.regex.Pattern;
+
+/**
+ * Parses inline code surrounded with {@code `} chars {@code `code`}
+ *
+ * @since 4.2.0-SNAPSHOT
+ */
+public class BackticksInlineProcessor extends InlineProcessor {
+
+ private static final Pattern TICKS = Pattern.compile("`+");
+
+ private static final Pattern TICKS_HERE = Pattern.compile("^`+");
+
+ private static final Pattern WHITESPACE = MarkwonInlineParser.WHITESPACE;
+
+ @Override
+ public char specialCharacter() {
+ return '`';
+ }
+
+ @Override
+ protected boolean parse() {
+ String ticks = match(TICKS_HERE);
+ if (ticks == null) {
+ return false;
+ }
+ int afterOpenTicks = index;
+ String matched;
+ while ((matched = match(TICKS)) != null) {
+ if (matched.equals(ticks)) {
+ Code node = new Code();
+ String content = input.substring(afterOpenTicks, index - ticks.length());
+ String literal = WHITESPACE.matcher(content.trim()).replaceAll(" ");
+ node.setLiteral(literal);
+ appendNode(node);
+ return true;
+ }
+ }
+ // If we got here, we didn't match a closing backtick sequence.
+ index = afterOpenTicks;
+ appendText(ticks);
+ return true;
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/BangInlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/BangInlineProcessor.java
new file mode 100644
index 00000000..7b9995ac
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/BangInlineProcessor.java
@@ -0,0 +1,33 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.internal.Bracket;
+import org.commonmark.node.Text;
+
+/**
+ * Parses markdown images {@code }
+ *
+ * @since 4.2.0-SNAPSHOT
+ */
+public class BangInlineProcessor extends InlineProcessor {
+ @Override
+ public char specialCharacter() {
+ return '!';
+ }
+
+ @Override
+ protected boolean parse() {
+ int startIndex = index;
+ index++;
+ if (peek() == '[') {
+ index++;
+
+ Text node = appendText("![");
+
+ // Add entry to stack for this opener
+ addBracket(Bracket.image(node, startIndex + 1, lastBracket(), lastDelimiter()));
+ } else {
+ appendText("!");
+ }
+ return true;
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/CloseBracketInlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/CloseBracketInlineProcessor.java
new file mode 100644
index 00000000..d48f0da2
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/CloseBracketInlineProcessor.java
@@ -0,0 +1,142 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.internal.Bracket;
+import org.commonmark.internal.util.Escaping;
+import org.commonmark.node.Image;
+import org.commonmark.node.Link;
+import org.commonmark.node.Node;
+
+import java.util.regex.Pattern;
+
+import static io.noties.markwon.inlineparser.InlineParserUtils.mergeChildTextNodes;
+
+/**
+ * Parses markdown link or image, relies on {@link OpenBracketInlineProcessor}
+ * to handle start of these elements
+ *
+ * @since 4.2.0-SNAPSHOT
+ */
+public class CloseBracketInlineProcessor extends InlineProcessor {
+
+ private static final Pattern WHITESPACE = MarkwonInlineParser.WHITESPACE;
+
+ @Override
+ public char specialCharacter() {
+ return ']';
+ }
+
+ @Override
+ protected boolean parse() {
+ index++;
+ int startIndex = index;
+
+ // Get previous `[` or `![`
+ Bracket opener = lastBracket();
+ if (opener == null) {
+ // No matching opener, just return a literal.
+ appendText("]");
+ return true;
+ }
+
+ if (!opener.allowed) {
+ // Matching opener but it's not allowed, just return a literal.
+ appendText("]");
+ removeLastBracket();
+ return true;
+ }
+
+ // Check to see if we have a link/image
+
+ String dest = null;
+ String title = null;
+ boolean isLinkOrImage = false;
+
+ // Maybe a inline link like `[foo](/uri "title")`
+ if (peek() == '(') {
+ index++;
+ spnl();
+ if ((dest = parseLinkDestination()) != null) {
+ spnl();
+ // title needs a whitespace before
+ if (WHITESPACE.matcher(input.substring(index - 1, index)).matches()) {
+ title = parseLinkTitle();
+ spnl();
+ }
+ if (peek() == ')') {
+ index++;
+ isLinkOrImage = true;
+ } else {
+ index = startIndex;
+ }
+ }
+ }
+
+ // Maybe a reference link like `[foo][bar]`, `[foo][]` or `[foo]`
+ if (!isLinkOrImage) {
+
+ // See if there's a link label like `[bar]` or `[]`
+ int beforeLabel = index;
+ int labelLength = parseLinkLabel();
+ String ref = null;
+ if (labelLength > 2) {
+ ref = input.substring(beforeLabel, beforeLabel + labelLength);
+ } else if (!opener.bracketAfter) {
+ // If the second label is empty `[foo][]` or missing `[foo]`, then the first label is the reference.
+ // But it can only be a reference when there's no (unescaped) bracket in it.
+ // If there is, we don't even need to try to look up the reference. This is an optimization.
+ ref = input.substring(opener.index, startIndex);
+ }
+
+ if (ref != null) {
+ Link link = referenceMap().get(Escaping.normalizeReference(ref));
+ if (link != null) {
+ dest = link.getDestination();
+ title = link.getTitle();
+ isLinkOrImage = true;
+ }
+ }
+ }
+
+ if (isLinkOrImage) {
+ // If we got here, open is a potential opener
+ Node linkOrImage = opener.image ? new Image(dest, title) : new Link(dest, title);
+
+ Node node = opener.node.getNext();
+ while (node != null) {
+ Node next = node.getNext();
+ linkOrImage.appendChild(node);
+ node = next;
+ }
+ appendNode(linkOrImage);
+
+ // Process delimiters such as emphasis inside link/image
+ processDelimiters(opener.previousDelimiter);
+ mergeChildTextNodes(linkOrImage);
+ // We don't need the corresponding text node anymore, we turned it into a link/image node
+ opener.node.unlink();
+ removeLastBracket();
+
+ // Links within links are not allowed. We found this link, so there can be no other link around it.
+ if (!opener.image) {
+ Bracket bracket = lastBracket();
+ while (bracket != null) {
+ if (!bracket.image) {
+ // Disallow link opener. It will still get matched, but will not result in a link.
+ bracket.allowed = false;
+ }
+ bracket = bracket.previous;
+ }
+ }
+
+ return true;
+
+ } else { // no link or image
+
+ appendText("]");
+ removeLastBracket();
+
+ index = startIndex;
+ return true;
+ }
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/EntityInlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/EntityInlineProcessor.java
new file mode 100644
index 00000000..c1229bd8
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/EntityInlineProcessor.java
@@ -0,0 +1,32 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.internal.util.Html5Entities;
+
+import java.util.regex.Pattern;
+
+/**
+ * Parses HTML entities {@code &}
+ *
+ * @since 4.2.0-SNAPSHOT
+ */
+public class EntityInlineProcessor extends InlineProcessor {
+
+ private static final String ENTITY = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});";
+ private static final Pattern ENTITY_HERE = Pattern.compile('^' + ENTITY, Pattern.CASE_INSENSITIVE);
+
+ @Override
+ public char specialCharacter() {
+ return '&';
+ }
+
+ @Override
+ protected boolean parse() {
+ String m;
+ if ((m = match(ENTITY_HERE)) != null) {
+ appendText(Html5Entities.entityToString(m));
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/HtmlInlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/HtmlInlineProcessor.java
new file mode 100644
index 00000000..2872491c
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/HtmlInlineProcessor.java
@@ -0,0 +1,40 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.internal.util.Parsing;
+import org.commonmark.node.HtmlInline;
+
+import java.util.regex.Pattern;
+
+/**
+ * Parses inline HTML tags
+ *
+ * @since 4.2.0-SNAPSHOT
+ */
+public class HtmlInlineProcessor extends InlineProcessor {
+
+ private static final String HTMLCOMMENT = "|";
+ private static final String PROCESSINGINSTRUCTION = "[<][?].*?[?][>]";
+ private static final String DECLARATION = "]*>";
+ private static final String CDATA = "";
+ private static final String HTMLTAG = "(?:" + Parsing.OPENTAG + "|" + Parsing.CLOSETAG + "|" + HTMLCOMMENT
+ + "|" + PROCESSINGINSTRUCTION + "|" + DECLARATION + "|" + CDATA + ")";
+ private static final Pattern HTML_TAG = Pattern.compile('^' + HTMLTAG, Pattern.CASE_INSENSITIVE);
+
+ @Override
+ public char specialCharacter() {
+ return '<';
+ }
+
+ @Override
+ protected boolean parse() {
+ String m = match(HTML_TAG);
+ if (m != null) {
+ HtmlInline node = new HtmlInline();
+ node.setLiteral(m);
+ appendNode(node);
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/InlineParserUtils.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/InlineParserUtils.java
new file mode 100644
index 00000000..544576ee
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/InlineParserUtils.java
@@ -0,0 +1,77 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
+
+/**
+ * @since 4.2.0-SNAPSHOT
+ */
+public abstract class InlineParserUtils {
+
+ public static void mergeTextNodesBetweenExclusive(Node fromNode, Node toNode) {
+ // No nodes between them
+ if (fromNode == toNode || fromNode.getNext() == toNode) {
+ return;
+ }
+
+ mergeTextNodesInclusive(fromNode.getNext(), toNode.getPrevious());
+ }
+
+ public static void mergeChildTextNodes(Node node) {
+ // No children or just one child node, no need for merging
+ if (node.getFirstChild() == node.getLastChild()) {
+ return;
+ }
+
+ mergeTextNodesInclusive(node.getFirstChild(), node.getLastChild());
+ }
+
+ public static void mergeTextNodesInclusive(Node fromNode, Node toNode) {
+ Text first = null;
+ Text last = null;
+ int length = 0;
+
+ Node node = fromNode;
+ while (node != null) {
+ if (node instanceof Text) {
+ Text text = (Text) node;
+ if (first == null) {
+ first = text;
+ }
+ length += text.getLiteral().length();
+ last = text;
+ } else {
+ mergeIfNeeded(first, last, length);
+ first = null;
+ last = null;
+ length = 0;
+ }
+ if (node == toNode) {
+ break;
+ }
+ node = node.getNext();
+ }
+
+ mergeIfNeeded(first, last, length);
+ }
+
+ public static void mergeIfNeeded(Text first, Text last, int textLength) {
+ if (first != null && last != null && first != last) {
+ StringBuilder sb = new StringBuilder(textLength);
+ sb.append(first.getLiteral());
+ Node node = first.getNext();
+ Node stop = last.getNext();
+ while (node != stop) {
+ sb.append(((Text) node).getLiteral());
+ Node unlink = node;
+ node = node.getNext();
+ unlink.unlink();
+ }
+ String literal = sb.toString();
+ first.setLiteral(literal);
+ }
+ }
+
+ private InlineParserUtils() {
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/InlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/InlineProcessor.java
new file mode 100644
index 00000000..2462e324
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/InlineProcessor.java
@@ -0,0 +1,148 @@
+package io.noties.markwon.inlineparser;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.commonmark.internal.Bracket;
+import org.commonmark.internal.Delimiter;
+import org.commonmark.node.Link;
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
+
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * @see AutolinkInlineProcessor
+ * @see BackslashInlineProcessor
+ * @see BackticksInlineProcessor
+ * @see BangInlineProcessor
+ * @see CloseBracketInlineProcessor
+ * @see EntityInlineProcessor
+ * @see HtmlInlineProcessor
+ * @see NewLineInlineProcessor
+ * @see OpenBracketInlineProcessor
+ * @see MarkwonInlineParser.FactoryBuilder#addInlineProcessor(InlineProcessor)
+ * @see MarkwonInlineParser.FactoryBuilder#excludeInlineProcessor(Class)
+ * @since 4.2.0-SNAPSHOT
+ */
+public abstract class InlineProcessor {
+
+ /**
+ * Special character that triggers parsing attempt
+ */
+ public abstract char specialCharacter();
+
+ /**
+ * @return boolean indicating if parsing succeeded
+ */
+ protected abstract boolean parse();
+
+
+ protected MarkwonInlineParserContext context;
+ protected Node block;
+ protected String input;
+ protected int index;
+
+ public boolean parse(@NonNull MarkwonInlineParserContext context) {
+ this.context = context;
+ this.block = context.block();
+ this.input = context.input();
+ this.index = context.index();
+
+ final boolean result = parse();
+
+ // synchronize index
+ context.setIndex(index);
+
+ return result;
+ }
+
+ protected Bracket lastBracket() {
+ return context.lastBracket();
+ }
+
+ protected Delimiter lastDelimiter() {
+ return context.lastDelimiter();
+ }
+
+ @NonNull
+ protected Map referenceMap() {
+ return context.referenceMap();
+ }
+
+ protected void addBracket(Bracket bracket) {
+ context.addBracket(bracket);
+ }
+
+ protected void removeLastBracket() {
+ context.removeLastBracket();
+ }
+
+ protected void spnl() {
+ context.setIndex(index);
+ context.spnl();
+ index = context.index();
+ }
+
+ @Nullable
+ protected String match(@NonNull Pattern re) {
+ // before trying to match, we must notify context about our index (which we store additionally here)
+ context.setIndex(index);
+
+ final String result = context.match(re);
+
+ // after match we must reflect index change here
+ this.index = context.index();
+
+ return result;
+ }
+
+ @Nullable
+ protected String parseLinkDestination() {
+ context.setIndex(index);
+ final String result = context.parseLinkDestination();
+ this.index = context.index();
+ return result;
+ }
+
+ @Nullable
+ protected String parseLinkTitle() {
+ context.setIndex(index);
+ final String result = context.parseLinkTitle();
+ this.index = context.index();
+ return result;
+ }
+
+ protected int parseLinkLabel() {
+ context.setIndex(index);
+ final int result = context.parseLinkLabel();
+ this.index = context.index();
+ return result;
+ }
+
+ protected void processDelimiters(Delimiter stackBottom) {
+ context.setIndex(index);
+ context.processDelimiters(stackBottom);
+ this.index = context.index();
+ }
+
+ protected void appendNode(@NonNull Node node) {
+ context.appendNode(node);
+ }
+
+ @NonNull
+ protected Text appendText(@NonNull CharSequence text, int beginIndex, int endIndex) {
+ return context.appendText(text, beginIndex, endIndex);
+ }
+
+ @NonNull
+ protected Text appendText(@NonNull CharSequence text) {
+ return context.appendText(text);
+ }
+
+ protected char peek() {
+ context.setIndex(index);
+ return context.peek();
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParser.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParser.java
new file mode 100644
index 00000000..89bb18c5
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParser.java
@@ -0,0 +1,915 @@
+package io.noties.markwon.inlineparser;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.commonmark.internal.Bracket;
+import org.commonmark.internal.Delimiter;
+import org.commonmark.internal.ReferenceParser;
+import org.commonmark.internal.inline.AsteriskDelimiterProcessor;
+import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
+import org.commonmark.internal.util.Escaping;
+import org.commonmark.node.Link;
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
+import org.commonmark.parser.InlineParser;
+import org.commonmark.parser.InlineParserContext;
+import org.commonmark.parser.InlineParserFactory;
+import org.commonmark.parser.delimiter.DelimiterProcessor;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static io.noties.markwon.inlineparser.InlineParserUtils.mergeChildTextNodes;
+import static io.noties.markwon.inlineparser.InlineParserUtils.mergeTextNodesBetweenExclusive;
+
+/**
+ * @see #factoryBuilder()
+ * @see FactoryBuilder
+ * @since 4.2.0-SNAPSHOT
+ */
+public class MarkwonInlineParser implements InlineParser, ReferenceParser, MarkwonInlineParserContext {
+
+ public interface FactoryBuilder {
+
+ /**
+ * @see InlineProcessor
+ */
+ @NonNull
+ FactoryBuilder addInlineProcessor(@NonNull InlineProcessor processor);
+
+ /**
+ * @see AsteriskDelimiterProcessor
+ * @see UnderscoreDelimiterProcessor
+ */
+ @NonNull
+ FactoryBuilder addDelimiterProcessor(@NonNull DelimiterProcessor processor);
+
+ /**
+ * Indicate if markdown references are enabled. {@code referencesEnabled=true} if {@link #includeDefaults()}
+ * was called
+ */
+ @NonNull
+ FactoryBuilder referencesEnabled(boolean referencesEnabled);
+
+ /**
+ * Includes all default delimiter and inline processors, and sets {@code referencesEnabled=true}.
+ * Useful with subsequent calls to {@link #excludeInlineProcessor(Class)} or {@link #excludeDelimiterProcessor(Class)}
+ */
+ @NonNull
+ FactoryBuilder includeDefaults();
+
+ @NonNull
+ FactoryBuilder excludeInlineProcessor(@NonNull Class extends InlineProcessor> processor);
+
+ @NonNull
+ FactoryBuilder excludeDelimiterProcessor(@NonNull Class extends DelimiterProcessor> processor);
+
+ @NonNull
+ InlineParserFactory build();
+ }
+
+ @NonNull
+ public static FactoryBuilder factoryBuilder() {
+ return new FactoryBuilderImpl();
+ }
+
+ private static final String ESCAPED_CHAR = "\\\\" + Escaping.ESCAPABLE;
+
+ private static final String ASCII_PUNCTUATION = "!\"#\\$%&'\\(\\)\\*\\+,\\-\\./:;<=>\\?@\\[\\\\\\]\\^_`\\{\\|\\}~";
+ private static final Pattern PUNCTUATION = Pattern
+ .compile("^[" + ASCII_PUNCTUATION + "\\p{Pc}\\p{Pd}\\p{Pe}\\p{Pf}\\p{Pi}\\p{Po}\\p{Ps}]");
+
+ private static final Pattern LINK_TITLE = Pattern.compile(
+ "^(?:\"(" + ESCAPED_CHAR + "|[^\"\\x00])*\"" +
+ '|' +
+ "'(" + ESCAPED_CHAR + "|[^'\\x00])*'" +
+ '|' +
+ "\\((" + ESCAPED_CHAR + "|[^)\\x00])*\\))");
+
+ private static final Pattern LINK_DESTINATION_BRACES = Pattern.compile("^(?:[<](?:[^<> \\t\\n\\\\]|\\\\.)*[>])");
+
+ private static final Pattern LINK_LABEL = Pattern.compile("^\\[(?:[^\\\\\\[\\]]|\\\\.)*\\]");
+
+ private static final Pattern SPNL = Pattern.compile("^ *(?:\n *)?");
+
+ private static final Pattern UNICODE_WHITESPACE_CHAR = Pattern.compile("^[\\p{Zs}\t\r\n\f]");
+
+ private static final Pattern LINE_END = Pattern.compile("^ *(?:\n|$)");
+
+ static final Pattern ESCAPABLE = Pattern.compile('^' + Escaping.ESCAPABLE);
+ static final Pattern WHITESPACE = Pattern.compile("\\s+");
+
+ private final boolean referencesEnabled;
+
+ private final BitSet specialCharacters;
+ private final Map> inlineProcessors;
+ private final Map delimiterProcessors;
+
+ private Node block;
+ private String input;
+ private int index;
+
+ /**
+ * Link references by ID, needs to be built up using parseReference before calling parse.
+ */
+ private Map referenceMap = new HashMap<>(1);
+
+ /**
+ * Top delimiter (emphasis, strong emphasis or custom emphasis). (Brackets are on a separate stack, different
+ * from the algorithm described in the spec.)
+ */
+ private Delimiter lastDelimiter;
+
+ /**
+ * Top opening bracket ([
or }
+ *
+ * @since 4.2.0-SNAPSHOT
+ */
+public class OpenBracketInlineProcessor extends InlineProcessor {
+ @Override
+ public char specialCharacter() {
+ return '[';
+ }
+
+ @Override
+ protected boolean parse() {
+ int startIndex = index;
+ index++;
+
+ Text node = appendText("[");
+
+ // Add entry to stack for this opener
+ addBracket(Bracket.link(node, startIndex, lastBracket(), lastDelimiter()));
+
+ return true;
+ }
+}
diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/StaggeredDelimiterProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/StaggeredDelimiterProcessor.java
new file mode 100644
index 00000000..c2a92c3d
--- /dev/null
+++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/StaggeredDelimiterProcessor.java
@@ -0,0 +1,75 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.node.Text;
+import org.commonmark.parser.delimiter.DelimiterProcessor;
+import org.commonmark.parser.delimiter.DelimiterRun;
+
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+class StaggeredDelimiterProcessor implements DelimiterProcessor {
+
+ private final char delim;
+ private int minLength = 0;
+ private LinkedList processors = new LinkedList<>(); // in reverse getMinLength order
+
+ StaggeredDelimiterProcessor(char delim) {
+ this.delim = delim;
+ }
+
+ @Override
+ public char getOpeningCharacter() {
+ return delim;
+ }
+
+ @Override
+ public char getClosingCharacter() {
+ return delim;
+ }
+
+ @Override
+ public int getMinLength() {
+ return minLength;
+ }
+
+ void add(DelimiterProcessor dp) {
+ final int len = dp.getMinLength();
+ ListIterator it = processors.listIterator();
+ boolean added = false;
+ while (it.hasNext()) {
+ DelimiterProcessor p = it.next();
+ int pLen = p.getMinLength();
+ if (len > pLen) {
+ it.previous();
+ it.add(dp);
+ added = true;
+ break;
+ } else if (len == pLen) {
+ throw new IllegalArgumentException("Cannot add two delimiter processors for char '" + delim + "' and minimum length " + len);
+ }
+ }
+ if (!added) {
+ processors.add(dp);
+ this.minLength = len;
+ }
+ }
+
+ private DelimiterProcessor findProcessor(int len) {
+ for (DelimiterProcessor p : processors) {
+ if (p.getMinLength() <= len) {
+ return p;
+ }
+ }
+ return processors.getFirst();
+ }
+
+ @Override
+ public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
+ return findProcessor(opener.length()).getDelimiterUse(opener, closer);
+ }
+
+ @Override
+ public void process(Text opener, Text closer, int delimiterUse) {
+ findProcessor(delimiterUse).process(opener, closer, delimiterUse);
+ }
+}
diff --git a/markwon-inline-parser/src/test/java/io/noties/markwon/inlineparser/InlineParserSpecTest.java b/markwon-inline-parser/src/test/java/io/noties/markwon/inlineparser/InlineParserSpecTest.java
new file mode 100644
index 00000000..5f05bb02
--- /dev/null
+++ b/markwon-inline-parser/src/test/java/io/noties/markwon/inlineparser/InlineParserSpecTest.java
@@ -0,0 +1,25 @@
+package io.noties.markwon.inlineparser;
+
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.testutil.SpecTestCase;
+import org.commonmark.testutil.example.Example;
+
+public class InlineParserSpecTest extends SpecTestCase {
+
+ private static final Parser PARSER = Parser.builder()
+ .inlineParserFactory(MarkwonInlineParser.factoryBuilder().includeDefaults().build())
+ .build();
+
+ // The spec says URL-escaping is optional, but the examples assume that it's enabled.
+ private static final HtmlRenderer RENDERER = HtmlRenderer.builder().percentEncodeUrls(true).build();
+
+ public InlineParserSpecTest(Example example) {
+ super(example);
+ }
+
+ @Override
+ protected String render(String source) {
+ return RENDERER.render(PARSER.parse(source));
+ }
+}
diff --git a/sample/build.gradle b/sample/build.gradle
index a7dec247..d2a9e27f 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -41,6 +41,7 @@ dependencies {
implementation project(':markwon-ext-tasklist')
implementation project(':markwon-html')
implementation project(':markwon-image')
+ implementation project(':markwon-inline-parser')
implementation project(':markwon-linkify')
implementation project(':markwon-recycler')
implementation project(':markwon-recycler-table')
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index 6bc6ef02..ee887f8c 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -33,6 +33,8 @@
android:name=".editor.EditorActivity"
android:windowSoftInputMode="adjustResize" />
+
+
\ No newline at end of file
diff --git a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java
index 59c6049f..db937d19 100644
--- a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java
@@ -24,6 +24,7 @@ import io.noties.markwon.sample.customextension.CustomExtensionActivity;
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
import io.noties.markwon.sample.editor.EditorActivity;
import io.noties.markwon.sample.html.HtmlActivity;
+import io.noties.markwon.sample.inlineparser.InlineParserActivity;
import io.noties.markwon.sample.latex.LatexActivity;
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
import io.noties.markwon.sample.recycler.RecyclerActivity;
@@ -122,6 +123,10 @@ public class MainActivity extends Activity {
activity = EditorActivity.class;
break;
+ case INLINE_PARSER:
+ activity = InlineParserActivity.class;
+ break;
+
default:
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
}
diff --git a/sample/src/main/java/io/noties/markwon/sample/Sample.java b/sample/src/main/java/io/noties/markwon/sample/Sample.java
index c2a2add3..221ee0bc 100644
--- a/sample/src/main/java/io/noties/markwon/sample/Sample.java
+++ b/sample/src/main/java/io/noties/markwon/sample/Sample.java
@@ -23,7 +23,9 @@ public enum Sample {
PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
- EDITOR(R.string.sample_editor);
+ EDITOR(R.string.sample_editor),
+
+ INLINE_PARSER(R.string.sample_inline_parser);
private final int textResId;
diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java
index f7389827..17387418 100644
--- a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java
+++ b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java
@@ -18,6 +18,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import org.commonmark.parser.InlineParserFactory;
import org.commonmark.parser.Parser;
import java.util.ArrayList;
@@ -36,6 +37,10 @@ import io.noties.markwon.editor.PersistedSpans;
import io.noties.markwon.editor.handler.EmphasisEditHandler;
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
+import io.noties.markwon.inlineparser.BangInlineProcessor;
+import io.noties.markwon.inlineparser.EntityInlineProcessor;
+import io.noties.markwon.inlineparser.HtmlInlineProcessor;
+import io.noties.markwon.inlineparser.MarkwonInlineParser;
import io.noties.markwon.linkify.LinkifyPlugin;
import io.noties.markwon.sample.R;
@@ -102,52 +107,52 @@ public class EditorActivity extends Activity {
private void additional_edit_span() {
// An additional span is used to highlight strong-emphasis
-final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
- .useEditHandler(new AbstractEditHandler() {
- @Override
- public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
- // Here we define which span is _persisted_ in EditText, it is not removed
- // from EditText between text changes, but instead - reused (by changing
- // position). Consider it as a cache for spans. We could use `StrongEmphasisSpan`
- // here also, but I chose Bold to indicate that this span is not the same
- // as in off-screen rendered markdown
- builder.persistSpan(Bold.class, Bold::new);
- }
+ final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
+ .useEditHandler(new AbstractEditHandler() {
+ @Override
+ public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
+ // Here we define which span is _persisted_ in EditText, it is not removed
+ // from EditText between text changes, but instead - reused (by changing
+ // position). Consider it as a cache for spans. We could use `StrongEmphasisSpan`
+ // here also, but I chose Bold to indicate that this span is not the same
+ // as in off-screen rendered markdown
+ builder.persistSpan(Bold.class, Bold::new);
+ }
- @Override
- public void handleMarkdownSpan(
- @NonNull PersistedSpans persistedSpans,
- @NonNull Editable editable,
- @NonNull String input,
- @NonNull StrongEmphasisSpan span,
- int spanStart,
- int spanTextLength) {
- // Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4)
- // because multiple inline markdown nodes can refer to the same text.
- // For example, `**_~~hey~~_**` - we will receive `**_~~` in this method,
- // and thus will have to manually find actual position in raw user input
- final MarkwonEditorUtils.Match match =
- MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__");
- if (match != null) {
- editable.setSpan(
- // we handle StrongEmphasisSpan and represent it with Bold in EditText
- // we still could use StrongEmphasisSpan, but it must be accessed
- // via persistedSpans
- persistedSpans.get(Bold.class),
- match.start(),
- match.end(),
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
- );
- }
- }
+ @Override
+ public void handleMarkdownSpan(
+ @NonNull PersistedSpans persistedSpans,
+ @NonNull Editable editable,
+ @NonNull String input,
+ @NonNull StrongEmphasisSpan span,
+ int spanStart,
+ int spanTextLength) {
+ // Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4)
+ // because multiple inline markdown nodes can refer to the same text.
+ // For example, `**_~~hey~~_**` - we will receive `**_~~` in this method,
+ // and thus will have to manually find actual position in raw user input
+ final MarkwonEditorUtils.Match match =
+ MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__");
+ if (match != null) {
+ editable.setSpan(
+ // we handle StrongEmphasisSpan and represent it with Bold in EditText
+ // we still could use StrongEmphasisSpan, but it must be accessed
+ // via persistedSpans
+ persistedSpans.get(Bold.class),
+ match.start(),
+ match.end(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ }
+ }
- @NonNull
- @Override
- public Class markdownSpanType() {
- return StrongEmphasisSpan.class;
- }
- })
- .build();
+ @NonNull
+ @Override
+ public Class markdownSpanType() {
+ return StrongEmphasisSpan.class;
+ }
+ })
+ .build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor));
}
@@ -171,14 +176,27 @@ final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
// for links to be clickable
editText.setMovementMethod(LinkMovementMethod.getInstance());
+ final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
+ .includeDefaults()
+ // no inline images will be parsed
+ .excludeInlineProcessor(BangInlineProcessor.class)
+ // no html tags will be parsed
+ .excludeInlineProcessor(HtmlInlineProcessor.class)
+ // no entities will be parsed (aka `&` etc)
+ .excludeInlineProcessor(EntityInlineProcessor.class)
+ .build();
+
final Markwon markwon = Markwon.builder(this)
.usePlugin(StrikethroughPlugin.create())
.usePlugin(LinkifyPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureParser(@NonNull Parser.Builder builder) {
+
// disable all commonmark-java blocks, only inlines will be parsed
// builder.enabledBlockTypes(Collections.emptySet());
+
+ builder.inlineParserFactory(inlineParserFactory);
}
})
.build();
diff --git a/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java b/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java
new file mode 100644
index 00000000..955d6db2
--- /dev/null
+++ b/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java
@@ -0,0 +1,119 @@
+package io.noties.markwon.sample.inlineparser;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.commonmark.node.Block;
+import org.commonmark.node.BlockQuote;
+import org.commonmark.node.Heading;
+import org.commonmark.node.HtmlBlock;
+import org.commonmark.node.ListBlock;
+import org.commonmark.node.ThematicBreak;
+import org.commonmark.parser.InlineParserFactory;
+import org.commonmark.parser.Parser;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import io.noties.markwon.AbstractMarkwonPlugin;
+import io.noties.markwon.Markwon;
+import io.noties.markwon.inlineparser.BackticksInlineProcessor;
+import io.noties.markwon.inlineparser.CloseBracketInlineProcessor;
+import io.noties.markwon.inlineparser.MarkwonInlineParser;
+import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
+import io.noties.markwon.sample.R;
+
+public class InlineParserActivity extends Activity {
+
+ private TextView textView;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_text_view);
+
+ this.textView = findViewById(R.id.text_view);
+
+// links_only();
+
+ disable_code();
+ }
+
+ private void links_only() {
+
+ // create an inline-parser-factory that will _ONLY_ parse links
+ // this would mean:
+ // * no emphasises (strong and regular aka bold and italics),
+ // * no images,
+ // * no code,
+ // * no HTML entities (&)
+ // * no HTML tags
+ // markdown blocks are still parsed
+ final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
+ .referencesEnabled(true)
+ .addInlineProcessor(new OpenBracketInlineProcessor())
+ .addInlineProcessor(new CloseBracketInlineProcessor())
+ .build();
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureParser(@NonNull Parser.Builder builder) {
+ builder.inlineParserFactory(inlineParserFactory);
+ }
+ })
+ .build();
+
+ // note that image is considered a link now
+ final String md = "**bold_bold-italic_** html-u, [link](#)  `code`";
+ markwon.setMarkdown(textView, md);
+ }
+
+ private void disable_code() {
+ // parses all as usual, but ignores code (inline and block)
+
+ final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
+ .includeDefaults()
+ .excludeInlineProcessor(BackticksInlineProcessor.class)
+ .build();
+
+ // unfortunately there is no _exclude_ method for parser-builder
+ final Set> enabledBlocks = new HashSet>() {{
+ // IndentedCodeBlock.class and FencedCodeBlock.class are missing
+ // this is full list (including above) that can be passed to `enabledBlockTypes` method
+ addAll(Arrays.asList(
+ BlockQuote.class,
+ Heading.class,
+ HtmlBlock.class,
+ ThematicBreak.class,
+ ListBlock.class));
+ }};
+
+ final Markwon markwon = Markwon.builder(this)
+ .usePlugin(new AbstractMarkwonPlugin() {
+ @Override
+ public void configureParser(@NonNull Parser.Builder builder) {
+ builder
+ .inlineParserFactory(inlineParserFactory)
+ .enabledBlockTypes(enabledBlocks);
+ }
+ })
+ .build();
+
+ final String md = "# Head!\n\n" +
+ "* one\n" +
+ "+ two\n\n" +
+ "and **bold** to `you`!\n\n" +
+ "> a quote _em_\n\n" +
+ "```java\n" +
+ "final int i = 0;\n" +
+ "```\n\n" +
+ "**Good day!**";
+ markwon.setMarkdown(textView, md);
+ }
+}
diff --git a/sample/src/main/res/values/strings-samples.xml b/sample/src/main/res/values/strings-samples.xml
index 7cf55ed2..a26f62c5 100644
--- a/sample/src/main/res/values/strings-samples.xml
+++ b/sample/src/main/res/values/strings-samples.xml
@@ -27,4 +27,6 @@
# \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText
+ # \# Inline Parser\n\nUsage of custom inline parser
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index a65bf67b..8bf10dcb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -11,6 +11,7 @@ include ':app', ':sample',
':markwon-image-coil',
':markwon-image-glide',
':markwon-image-picasso',
+ ':markwon-inline-parser',
':markwon-linkify',
':markwon-recycler',
':markwon-recycler-table',