From 6c4ffd1778acd2d0bb95c4f80116b3864ea07dd4 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 14 Nov 2019 17:38:24 +0300 Subject: [PATCH] Update to commonmark-java 0.13.0 --- build.gradle | 2 +- .../io/noties/markwon/MarkwonVisitorImpl.java | 6 + .../editor/MarkwonEditorTextWatcher.java | 8 +- .../inlineparser/AutolinkInlineProcessor.java | 11 +- .../BackslashInlineProcessor.java | 12 +- .../BackticksInlineProcessor.java | 28 +- .../inlineparser/BangInlineProcessor.java | 10 +- .../CloseBracketInlineProcessor.java | 30 +- .../inlineparser/EntityInlineProcessor.java | 12 +- .../inlineparser/HtmlInlineProcessor.java | 8 +- .../markwon/inlineparser/InlineProcessor.java | 27 +- .../inlineparser/MarkwonInlineParser.java | 334 +++++++----------- .../MarkwonInlineParserContext.java | 15 +- .../inlineparser/NewLineInlineProcessor.java | 23 +- .../OpenBracketInlineProcessor.java | 7 +- .../inlineparser/InlineParserSpecTest.java | 2 +- .../CustomExtensionActivity2.java | 23 +- .../inlineparser/InlineParserActivity.java | 3 +- 18 files changed, 239 insertions(+), 322 deletions(-) diff --git a/build.gradle b/build.gradle index d0578fdf..4bb3392e 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ ext { 'push-aar-gradle': 'https://raw.githubusercontent.com/noties/gradle-mvn-push/master/gradle-mvn-push-aar.gradle' ] - final def commonMarkVersion = '0.12.1' + final def commonMarkVersion = '0.13.0' final def daggerVersion = '2.10' deps = [ diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonVisitorImpl.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonVisitorImpl.java index c6361a00..659a6622 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonVisitorImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonVisitorImpl.java @@ -18,6 +18,7 @@ import org.commonmark.node.HtmlInline; import org.commonmark.node.Image; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Link; +import org.commonmark.node.LinkReferenceDefinition; import org.commonmark.node.ListItem; import org.commonmark.node.Node; import org.commonmark.node.OrderedList; @@ -155,6 +156,11 @@ class MarkwonVisitorImpl implements MarkwonVisitor { visit((Node) text); } + @Override + public void visit(LinkReferenceDefinition linkReferenceDefinition) { + visit((Node) linkReferenceDefinition); + } + @Override public void visit(CustomBlock customBlock) { visit((Node) customBlock); diff --git a/markwon-editor/src/main/java/io/noties/markwon/editor/MarkwonEditorTextWatcher.java b/markwon-editor/src/main/java/io/noties/markwon/editor/MarkwonEditorTextWatcher.java index 12dc9e00..cfa3cbad 100644 --- a/markwon-editor/src/main/java/io/noties/markwon/editor/MarkwonEditorTextWatcher.java +++ b/markwon-editor/src/main/java/io/noties/markwon/editor/MarkwonEditorTextWatcher.java @@ -1,6 +1,7 @@ package io.noties.markwon.editor; import android.text.Editable; +import android.text.SpannableStringBuilder; import android.text.TextWatcher; import android.view.View; import android.widget.EditText; @@ -113,7 +114,7 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher { } @Override - public void afterTextChanged(final Editable s) { + public void afterTextChanged(Editable s) { if (selfChange) { return; @@ -126,11 +127,14 @@ public abstract class MarkwonEditorTextWatcher implements TextWatcher { future.cancel(true); } + // copy current content (it's not good to pass EditText editable to other thread) + final SpannableStringBuilder builder = new SpannableStringBuilder(s); + future = executorService.submit(new Runnable() { @Override public void run() { try { - editor.preRender(s, new MarkwonEditor.PreRenderResultListener() { + editor.preRender(builder, new MarkwonEditor.PreRenderResultListener() { @Override public void onPreRenderResult(@NonNull final MarkwonEditor.PreRenderResult result) { final EditText et = editText; 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 index 6351fe64..ed41a26d 100644 --- 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 @@ -1,6 +1,7 @@ package io.noties.markwon.inlineparser; import org.commonmark.node.Link; +import org.commonmark.node.Node; import org.commonmark.node.Text; import java.util.regex.Pattern; @@ -24,22 +25,20 @@ public class AutolinkInlineProcessor extends InlineProcessor { } @Override - protected boolean parse() { + protected Node 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; + return node; } 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; + return node; } else { - return false; + return null; } } } 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 index e8f433ca..3dfc3560 100644 --- 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 @@ -1,6 +1,7 @@ package io.noties.markwon.inlineparser; import org.commonmark.node.HardLineBreak; +import org.commonmark.node.Node; import java.util.regex.Pattern; @@ -17,17 +18,18 @@ public class BackslashInlineProcessor extends InlineProcessor { } @Override - protected boolean parse() { + protected Node parse() { index++; + Node node; if (peek() == '\n') { - appendNode(new HardLineBreak()); + node = new HardLineBreak(); index++; } else if (index < input.length() && ESCAPABLE.matcher(input.substring(index, index + 1)).matches()) { - appendText(input, index, index + 1); + node = text(input, index, index + 1); index++; } else { - appendText("\\"); + node = text("\\"); } - return true; + return node; } } 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 index f0c8da9c..ed9389d8 100644 --- 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 @@ -1,6 +1,8 @@ package io.noties.markwon.inlineparser; +import org.commonmark.internal.util.Parsing; import org.commonmark.node.Code; +import org.commonmark.node.Node; import java.util.regex.Pattern; @@ -15,18 +17,16 @@ public class BackticksInlineProcessor extends InlineProcessor { private static final Pattern TICKS_HERE = Pattern.compile("^`+"); - private static final Pattern WHITESPACE = MarkwonInlineParser.WHITESPACE; - @Override public char specialCharacter() { return '`'; } @Override - protected boolean parse() { + protected Node parse() { String ticks = match(TICKS_HERE); if (ticks == null) { - return false; + return null; } int afterOpenTicks = index; String matched; @@ -34,15 +34,23 @@ public class BackticksInlineProcessor extends InlineProcessor { 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; + content = content.replace('\n', ' '); + + // spec: If the resulting string both begins and ends with a space character, but does not consist + // entirely of space characters, a single space character is removed from the front and back. + if (content.length() >= 3 && + content.charAt(0) == ' ' && + content.charAt(content.length() - 1) == ' ' && + Parsing.hasNonSpace(content)) { + content = content.substring(1, content.length() - 1); + } + + node.setLiteral(content); + return node; } } // If we got here, we didn't match a closing backtick sequence. index = afterOpenTicks; - appendText(ticks); - return true; + return text(ticks); } } 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 index 7b9995ac..adfcc9cf 100644 --- 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 @@ -1,6 +1,7 @@ package io.noties.markwon.inlineparser; import org.commonmark.internal.Bracket; +import org.commonmark.node.Node; import org.commonmark.node.Text; /** @@ -15,19 +16,20 @@ public class BangInlineProcessor extends InlineProcessor { } @Override - protected boolean parse() { + protected Node parse() { int startIndex = index; index++; if (peek() == '[') { index++; - Text node = appendText("!["); + Text node = text("!["); // Add entry to stack for this opener addBracket(Bracket.image(node, startIndex + 1, lastBracket(), lastDelimiter())); + + return node; } else { - appendText("!"); + return text("!"); } - 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 index d48f0da2..0d23dadf 100644 --- 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 @@ -4,6 +4,7 @@ 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.LinkReferenceDefinition; import org.commonmark.node.Node; import java.util.regex.Pattern; @@ -26,7 +27,7 @@ public class CloseBracketInlineProcessor extends InlineProcessor { } @Override - protected boolean parse() { + protected Node parse() { index++; int startIndex = index; @@ -34,15 +35,13 @@ public class CloseBracketInlineProcessor extends InlineProcessor { Bracket opener = lastBracket(); if (opener == null) { // No matching opener, just return a literal. - appendText("]"); - return true; + return text("]"); } if (!opener.allowed) { // Matching opener but it's not allowed, just return a literal. - appendText("]"); removeLastBracket(); - return true; + return text("]"); } // Check to see if we have a link/image @@ -76,7 +75,8 @@ public class CloseBracketInlineProcessor extends InlineProcessor { // See if there's a link label like `[bar]` or `[]` int beforeLabel = index; - int labelLength = parseLinkLabel(); + parseLinkLabel(); + int labelLength = index - beforeLabel; String ref = null; if (labelLength > 2) { ref = input.substring(beforeLabel, beforeLabel + labelLength); @@ -88,10 +88,11 @@ public class CloseBracketInlineProcessor extends InlineProcessor { } if (ref != null) { - Link link = referenceMap().get(Escaping.normalizeReference(ref)); - if (link != null) { - dest = link.getDestination(); - title = link.getTitle(); + String label = Escaping.normalizeReference(ref); + LinkReferenceDefinition definition = context.getLinkReferenceDefinition(label); + if (definition != null) { + dest = definition.getDestination(); + title = definition.getTitle(); isLinkOrImage = true; } } @@ -107,7 +108,6 @@ public class CloseBracketInlineProcessor extends InlineProcessor { linkOrImage.appendChild(node); node = next; } - appendNode(linkOrImage); // Process delimiters such as emphasis inside link/image processDelimiters(opener.previousDelimiter); @@ -128,15 +128,13 @@ public class CloseBracketInlineProcessor extends InlineProcessor { } } - return true; + return linkOrImage; } else { // no link or image - - appendText("]"); + index = startIndex; removeLastBracket(); - index = startIndex; - return true; + return text("]"); } } } 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 index c1229bd8..0a1467f8 100644 --- 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 @@ -1,6 +1,8 @@ package io.noties.markwon.inlineparser; +import org.commonmark.internal.util.Escaping; import org.commonmark.internal.util.Html5Entities; +import org.commonmark.node.Node; import java.util.regex.Pattern; @@ -11,8 +13,7 @@ import java.util.regex.Pattern; */ 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); + private static final Pattern ENTITY_HERE = Pattern.compile('^' + Escaping.ENTITY, Pattern.CASE_INSENSITIVE); @Override public char specialCharacter() { @@ -20,13 +21,12 @@ public class EntityInlineProcessor extends InlineProcessor { } @Override - protected boolean parse() { + protected Node parse() { String m; if ((m = match(ENTITY_HERE)) != null) { - appendText(Html5Entities.entityToString(m)); - return true; + return text(Html5Entities.entityToString(m)); } else { - return false; + return null; } } } 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 index 2872491c..7f9e032c 100644 --- 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 @@ -2,6 +2,7 @@ package io.noties.markwon.inlineparser; import org.commonmark.internal.util.Parsing; import org.commonmark.node.HtmlInline; +import org.commonmark.node.Node; import java.util.regex.Pattern; @@ -26,15 +27,14 @@ public class HtmlInlineProcessor extends InlineProcessor { } @Override - protected boolean parse() { + protected Node parse() { String m = match(HTML_TAG); if (m != null) { HtmlInline node = new HtmlInline(); node.setLiteral(m); - appendNode(node); - return true; + return node; } else { - return false; + return null; } } } 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 index 2462e324..b89ae1d0 100644 --- 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 @@ -36,7 +36,8 @@ public abstract class InlineProcessor { /** * @return boolean indicating if parsing succeeded */ - protected abstract boolean parse(); + @Nullable + protected abstract Node parse(); protected MarkwonInlineParserContext context; @@ -44,13 +45,14 @@ public abstract class InlineProcessor { protected String input; protected int index; - public boolean parse(@NonNull MarkwonInlineParserContext context) { + @Nullable + public Node parse(@NonNull MarkwonInlineParserContext context) { this.context = context; this.block = context.block(); this.input = context.input(); this.index = context.index(); - final boolean result = parse(); + final Node result = parse(); // synchronize index context.setIndex(index); @@ -66,11 +68,6 @@ public abstract class InlineProcessor { return context.lastDelimiter(); } - @NonNull - protected Map referenceMap() { - return context.referenceMap(); - } - protected void addBracket(Bracket bracket) { context.addBracket(bracket); } @@ -127,18 +124,14 @@ public abstract class InlineProcessor { this.index = context.index(); } - protected void appendNode(@NonNull Node node) { - context.appendNode(node); + @NonNull + protected Text text(@NonNull String text) { + return context.text(text); } @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 Text text(@NonNull String text, int start, int end) { + return context.text(text, start, end); } protected char 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 index 89bb18c5..bdcbd1ad 100644 --- 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 @@ -5,11 +5,11 @@ 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.internal.util.LinkScanner; +import org.commonmark.node.LinkReferenceDefinition; import org.commonmark.node.Node; import org.commonmark.node.Text; import org.commonmark.parser.InlineParser; @@ -32,11 +32,13 @@ import static io.noties.markwon.inlineparser.InlineParserUtils.mergeTextNodesBet /** * @see #factoryBuilder() + * @see #factoryBuilderNoDefaults() * @see FactoryBuilder * @since 4.2.0-SNAPSHOT */ -public class MarkwonInlineParser implements InlineParser, ReferenceParser, MarkwonInlineParserContext { +public class MarkwonInlineParser implements InlineParser, MarkwonInlineParserContext { + @SuppressWarnings("unused") public interface FactoryBuilder { /** @@ -76,52 +78,51 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw InlineParserFactory build(); } + /** + * Creates an instance of {@link FactoryBuilder} and includes all defaults. + * + * @see #factoryBuilderNoDefaults() + */ @NonNull public static FactoryBuilder factoryBuilder() { - return new FactoryBuilderImpl(); + return new FactoryBuilderImpl().includeDefaults(); } - private static final String ESCAPED_CHAR = "\\\\" + Escaping.ESCAPABLE; + /** + * NB, this return an empty builder, so if no {@link FactoryBuilder#includeDefaults()} + * is called, it means effectively no inline parsing (unless further calls + * to {@link FactoryBuilder#addInlineProcessor(InlineProcessor)} or {@link FactoryBuilder#addDelimiterProcessor(DelimiterProcessor)}). + */ + @NonNull + public static FactoryBuilder factoryBuilderNoDefaults() { + return new FactoryBuilderImpl(); + } 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 InlineParserContext inlineParserContext; + private final boolean referencesEnabled; private final BitSet specialCharacters; private final Map> inlineProcessors; private final Map delimiterProcessors; + // currently we still hold a reference to it because we decided not to + // pass previous node argument to inline-processors (current usage is limited with NewLineInlineProcessor) 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.) @@ -135,9 +136,11 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw // might we construct these in factory? public MarkwonInlineParser( + @NonNull InlineParserContext inlineParserContext, boolean referencesEnabled, @NonNull List inlineProcessors, @NonNull List delimiterProcessors) { + this.inlineParserContext = inlineParserContext; this.referencesEnabled = referencesEnabled; this.inlineProcessors = calculateInlines(inlineProcessors); this.delimiterProcessors = calculateDelimiterProcessors(delimiterProcessors); @@ -218,117 +221,29 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw */ @Override public void parse(String content, Node block) { - this.block = block; - this.input = content.trim(); - this.index = 0; - this.lastDelimiter = null; - this.lastBracket = null; + reset(content.trim()); - boolean moreToParse; - do { - moreToParse = parseInline(); - } while (moreToParse); + // we still reference it + this.block = block; + + while (true) { + Node node = parseInline(); + if (node != null) { + block.appendChild(node); + } else { + break; + } + } processDelimiters(null); mergeChildTextNodes(block); } - /** - * Attempt to parse a link reference, modifying the internal reference map. - */ - @Override - public int parseReference(String s) { - - if (!referencesEnabled) { - return 0; - } - - this.input = s; + private void reset(String content) { + this.input = content; this.index = 0; - String dest; - String title; - int matchChars; - int startIndex = index; - - // label: - matchChars = parseLinkLabel(); - if (matchChars == 0) { - return 0; - } - - String rawLabel = input.substring(0, matchChars); - - // colon: - if (peek() != ':') { - return 0; - } - index++; - - // link url - spnl(); - - dest = parseLinkDestination(); - if (dest == null || dest.length() == 0) { - return 0; - } - - int beforeTitle = index; - spnl(); - title = parseLinkTitle(); - if (title == null) { - // rewind before spaces - index = beforeTitle; - } - - boolean atLineEnd = true; - if (index != input.length() && match(LINE_END) == null) { - if (title == null) { - atLineEnd = false; - } else { - // the potential title we found is not at the line end, - // but it could still be a legal link reference if we - // discard the title - title = null; - // rewind before spaces - index = beforeTitle; - // and instead check if the link URL is at the line end - atLineEnd = match(LINE_END) != null; - } - } - - if (!atLineEnd) { - return 0; - } - - String normalizedLabel = Escaping.normalizeReference(rawLabel); - if (normalizedLabel.isEmpty()) { - return 0; - } - - if (!referenceMap.containsKey(normalizedLabel)) { - Link link = new Link(dest, title); - referenceMap.put(normalizedLabel, link); - } - return index - startIndex; - } - - @Override - @NonNull - public Text appendText(@NonNull CharSequence text, int beginIndex, int endIndex) { - return appendText(text.subSequence(beginIndex, endIndex)); - } - - @Override - @NonNull - public Text appendText(@NonNull CharSequence text) { - Text node = new Text(text.toString()); - appendNode(node); - return node; - } - - @Override - public void appendNode(@NonNull Node node) { - block.appendChild(node); + this.lastDelimiter = null; + this.lastBracket = null; } /** @@ -336,43 +251,44 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw * On success, add the result to block's children and return true. * On failure, return false. */ - private boolean parseInline() { + @Nullable + private Node parseInline() { final char c = peek(); if (c == '\0') { - return false; + return null; } - boolean res = false; + Node node = null; final List inlines = this.inlineProcessors.get(c); if (inlines != null) { for (InlineProcessor inline : inlines) { - res = inline.parse(this); - if (res) { + node = inline.parse(this); + if (node != null) { break; } } } else { final DelimiterProcessor delimiterProcessor = delimiterProcessors.get(c); if (delimiterProcessor != null) { - res = parseDelimiters(delimiterProcessor, c); + node = parseDelimiters(delimiterProcessor, c); } else { - res = parseString(); + node = parseString(); } } - if (!res) { + if (node != null) { + return node; + } else { index++; // When we get here, it's only for a single special character that turned out to not have a special meaning. // So we shouldn't have a single surrogate here, hence it should be ok to turn it into a String. String literal = String.valueOf(c); - appendText(literal); + return text(literal); } - - return true; } /** @@ -395,6 +311,26 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw } } + @NonNull + @Override + public Text text(@NonNull String text) { + return new Text(text); + } + + @NonNull + @Override + public Text text(@NonNull String text, int beginIndex, int endIndex) { + return new Text(text.substring(beginIndex, endIndex)); + } + + @Nullable + @Override + public LinkReferenceDefinition getLinkReferenceDefinition(String label) { + return referencesEnabled + ? inlineParserContext.getLinkReferenceDefinition(label) + : null; + } + /** * Returns the char at the current input index, or {@code '\0'} in case there are no more characters. */ @@ -439,12 +375,6 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw return lastDelimiter; } - @NonNull - @Override - public Map referenceMap() { - return referenceMap; - } - @Override public void addBracket(Bracket bracket) { if (lastBracket != null) { @@ -462,24 +392,24 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw * Parse zero or more space characters, including at most one newline. */ @Override - public boolean spnl() { + public void spnl() { match(SPNL); - return true; } /** * Attempt to parse delimiters like emphasis, strong emphasis or custom delimiters. */ - private boolean parseDelimiters(DelimiterProcessor delimiterProcessor, char delimiterChar) { + @Nullable + private Node parseDelimiters(DelimiterProcessor delimiterProcessor, char delimiterChar) { DelimiterData res = scanDelimiters(delimiterProcessor, delimiterChar); if (res == null) { - return false; + return null; } int length = res.count; int startIndex = index; index += length; - Text node = appendText(input, startIndex, index); + Text node = text(input, startIndex, index); // Add entry to stack for this opener lastDelimiter = new Delimiter(node, delimiterChar, res.canOpen, res.canClose, lastDelimiter); @@ -489,7 +419,7 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw lastDelimiter.previous.next = lastDelimiter; } - return true; + return node; } /** @@ -498,57 +428,21 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw @Override @Nullable public String parseLinkDestination() { - String res = match(LINK_DESTINATION_BRACES); - if (res != null) { // chop off surrounding <..>: - if (res.length() == 2) { - return ""; - } else { - return Escaping.unescapeString(res.substring(1, res.length() - 1)); - } - } else { - int startIndex = index; - parseLinkDestinationWithBalancedParens(); - return Escaping.unescapeString(input.substring(startIndex, index)); + int afterDest = LinkScanner.scanLinkDestination(input, index); + if (afterDest == -1) { + return null; } - } - private void parseLinkDestinationWithBalancedParens() { - int parens = 0; - while (true) { - char c = peek(); - switch (c) { - case '\0': - return; - case '\\': - // check if we have an escapable character - if (index + 1 < input.length() && ESCAPABLE.matcher(input.substring(index + 1, index + 2)).matches()) { - // skip over the escaped character (after switch) - index++; - break; - } - // otherwise, we treat this as a literal backslash - break; - case '(': - parens++; - break; - case ')': - if (parens == 0) { - return; - } else { - parens--; - } - break; - case ' ': - // ASCII space - return; - default: - // or control character - if (Character.isISOControl(c)) { - return; - } - } - index++; + String dest; + if (peek() == '<') { + // chop off surrounding <..>: + dest = input.substring(index + 1, afterDest - 1); + } else { + dest = input.substring(index, afterDest); } + + index = afterDest; + return Escaping.unescapeString(dest); } /** @@ -557,13 +451,15 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw @Override @Nullable public String parseLinkTitle() { - String title = match(LINK_TITLE); - if (title != null) { - // chop off quotes from title and unescape: - return Escaping.unescapeString(title.substring(1, title.length() - 1)); - } else { + int afterTitle = LinkScanner.scanLinkTitle(input, index); + if (afterTitle == -1) { return null; } + + // chop off ', " or parens + String title = input.substring(index + 1, afterTitle - 1); + index = afterTitle; + return Escaping.unescapeString(title); } /** @@ -571,19 +467,28 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw */ @Override public int parseLinkLabel() { - String m = match(LINK_LABEL); - // Spec says "A link label can have at most 999 characters inside the square brackets" - if (m == null || m.length() > 1001) { + if (index >= input.length() || input.charAt(index) != '[') { return 0; - } else { - return m.length(); } + + int startContent = index + 1; + int endContent = LinkScanner.scanLinkLabelContent(input, startContent); + // spec: A link label can have at most 999 characters inside the square brackets. + int contentLength = endContent - startContent; + if (endContent == -1 || contentLength > 999) { + return 0; + } + if (endContent >= input.length() || input.charAt(endContent) != ']') { + return 0; + } + index = endContent + 1; + return contentLength + 2; } /** * Parse a run of ordinary characters, or a single character with a special meaning in markdown, as a plain string. */ - private boolean parseString() { + private Node parseString() { int begin = index; int length = input.length(); while (index != length) { @@ -593,10 +498,9 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw index++; } if (begin != index) { - appendText(input, begin, index); - return true; + return text(input, begin, index); } else { - return false; + return null; } } @@ -909,7 +813,11 @@ public class MarkwonInlineParser implements InlineParser, ReferenceParser, Markw } else { delimiterProcessors = this.delimiterProcessors; } - return new MarkwonInlineParser(referencesEnabled, inlineProcessors, delimiterProcessors); + return new MarkwonInlineParser( + inlineParserContext, + referencesEnabled, + inlineProcessors, + delimiterProcessors); } } } diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParserContext.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParserContext.java index 726ff4eb..46870f91 100644 --- a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParserContext.java +++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParserContext.java @@ -6,6 +6,7 @@ import androidx.annotation.Nullable; import org.commonmark.internal.Bracket; import org.commonmark.internal.Delimiter; import org.commonmark.node.Link; +import org.commonmark.node.LinkReferenceDefinition; import org.commonmark.node.Node; import org.commonmark.node.Text; @@ -28,14 +29,11 @@ public interface MarkwonInlineParserContext { Delimiter lastDelimiter(); - @NonNull - Map referenceMap(); - void addBracket(Bracket bracket); void removeLastBracket(); - boolean spnl(); + void spnl(); /** * Returns the char at the current input index, or {@code '\0'} in case there are no more characters. @@ -45,13 +43,14 @@ public interface MarkwonInlineParserContext { @Nullable String match(@NonNull Pattern re); - void appendNode(@NonNull Node node); + @NonNull + Text text(@NonNull String text); @NonNull - Text appendText(@NonNull CharSequence text, int beginIndex, int endIndex); + Text text(@NonNull String text, int beginIndex, int endIndex); - @NonNull - Text appendText(@NonNull CharSequence text); + @Nullable + LinkReferenceDefinition getLinkReferenceDefinition(String label); @Nullable String parseLinkDestination(); diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/NewLineInlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/NewLineInlineProcessor.java index 4f08a74f..4ec874fc 100644 --- a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/NewLineInlineProcessor.java +++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/NewLineInlineProcessor.java @@ -21,29 +21,28 @@ public class NewLineInlineProcessor extends InlineProcessor { } @Override - protected boolean parse() { + protected Node parse() { index++; // assume we're at a \n - Node lastChild = block.getLastChild(); + final Node previous = block.getLastChild(); + // Check previous text for trailing spaces. // The "endsWith" is an optimization to avoid an RE match in the common case. - if (lastChild != null && lastChild instanceof Text && ((Text) lastChild).getLiteral().endsWith(" ")) { - Text text = (Text) lastChild; + if (previous instanceof Text && ((Text) previous).getLiteral().endsWith(" ")) { + Text text = (Text) previous; String literal = text.getLiteral(); Matcher matcher = FINAL_SPACE.matcher(literal); int spaces = matcher.find() ? matcher.end() - matcher.start() : 0; if (spaces > 0) { text.setLiteral(literal.substring(0, literal.length() - spaces)); } - appendNode(spaces >= 2 ? new HardLineBreak() : new SoftLineBreak()); + if (spaces >= 2) { + return new HardLineBreak(); + } else { + return new SoftLineBreak(); + } } else { - appendNode(new SoftLineBreak()); + return new SoftLineBreak(); } - - // gobble leading spaces in next line - while (peek() == ' ') { - index++; - } - return true; } } diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/OpenBracketInlineProcessor.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/OpenBracketInlineProcessor.java index 02edf9bb..9c065900 100644 --- a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/OpenBracketInlineProcessor.java +++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/OpenBracketInlineProcessor.java @@ -1,6 +1,7 @@ package io.noties.markwon.inlineparser; import org.commonmark.internal.Bracket; +import org.commonmark.node.Node; import org.commonmark.node.Text; /** @@ -15,15 +16,15 @@ public class OpenBracketInlineProcessor extends InlineProcessor { } @Override - protected boolean parse() { + protected Node parse() { int startIndex = index; index++; - Text node = appendText("["); + Text node = text("["); // Add entry to stack for this opener addBracket(Bracket.link(node, startIndex, lastBracket(), lastDelimiter())); - return true; + return node; } } 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 index 5f05bb02..b7afb01d 100644 --- 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 @@ -8,7 +8,7 @@ import org.commonmark.testutil.example.Example; public class InlineParserSpecTest extends SpecTestCase { private static final Parser PARSER = Parser.builder() - .inlineParserFactory(MarkwonInlineParser.factoryBuilder().includeDefaults().build()) + .inlineParserFactory(MarkwonInlineParser.factoryBuilder().build()) .build(); // The spec says URL-escaping is optional, but the examples assume that it's enabled. diff --git a/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java b/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java index 65b71800..cb4f178f 100644 --- a/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java +++ b/sample/src/main/java/io/noties/markwon/sample/customextension2/CustomExtensionActivity2.java @@ -8,7 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.commonmark.node.Link; -import org.commonmark.node.Text; +import org.commonmark.node.Node; import org.commonmark.parser.InlineParserFactory; import org.commonmark.parser.Parser; @@ -71,7 +71,8 @@ public class CustomExtensionActivity2 extends Activity { final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder() // include all current defaults (otherwise will be empty - contain only our inline-processors) - .includeDefaults() + // included by default, to create factory-builder without defaults call `factoryBuilderNoDefaults` +// .includeDefaults() .addInlineProcessor(new IssueInlineProcessor()) .addInlineProcessor(new UserInlineProcessor()) .build(); @@ -98,15 +99,14 @@ public class CustomExtensionActivity2 extends Activity { } @Override - protected boolean parse() { + protected Node parse() { final String id = match(RE); if (id != null) { final Link link = new Link(createIssueOrPullRequestLinkDestination(id), null); - link.appendChild(new Text("#" + id)); - appendNode(link); - return true; + link.appendChild(text("#" + id)); + return link; } - return false; + return null; } @NonNull @@ -125,15 +125,14 @@ public class CustomExtensionActivity2 extends Activity { } @Override - protected boolean parse() { + protected Node parse() { final String user = match(RE); if (user != null) { final Link link = new Link(createUserLinkDestination(user), null); - link.appendChild(new Text("@" + user)); - appendNode(link); - return true; + link.appendChild(text("@" + user)); + return link; } - return false; + return null; } @NonNull 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 index 955d6db2..27d069eb 100644 --- a/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/inlineparser/InlineParserActivity.java @@ -54,7 +54,7 @@ public class InlineParserActivity extends Activity { // * no HTML entities (&) // * no HTML tags // markdown blocks are still parsed - final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder() + final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilderNoDefaults() .referencesEnabled(true) .addInlineProcessor(new OpenBracketInlineProcessor()) .addInlineProcessor(new CloseBracketInlineProcessor()) @@ -78,7 +78,6 @@ public class InlineParserActivity extends Activity { // parses all as usual, but ignores code (inline and block) final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder() - .includeDefaults() .excludeInlineProcessor(BackticksInlineProcessor.class) .build();