Created inline-parser module
This commit is contained in:
parent
e95defb67c
commit
93a14b4731
20
markwon-inline-parser/README.md
Normal file
20
markwon-inline-parser/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# 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.util.Escaping;
|
||||||
|
import org.commonmark.internal.util.Html5Entities;
|
||||||
|
import org.commonmark.internal.util.Parsing;
|
||||||
|
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.internal.Bracket;
|
||||||
|
import org.commonmark.internal.Delimiter;
|
||||||
|
```
|
||||||
|
|
||||||
|
`StaggeredDelimiterProcessor` class source is copied (required for InlineParser)
|
21
markwon-inline-parser/build.gradle
Normal file
21
markwon-inline-parser/build.gradle
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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']
|
||||||
|
}
|
||||||
|
|
||||||
|
registerArtifact(this)
|
4
markwon-inline-parser/gradle.properties
Normal file
4
markwon-inline-parser/gradle.properties
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
POM_NAME=Inline Parser
|
||||||
|
POM_ARTIFACT_ID=inline-parser
|
||||||
|
POM_DESCRIPTION=Markwon customizable commonmark-java InlineParse
|
||||||
|
POM_PACKAGING=aar
|
1
markwon-inline-parser/src/main/AndroidManifest.xml
Normal file
1
markwon-inline-parser/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest package="io.noties.markwon.inlineparser" />
|
@ -0,0 +1,7 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class AsteriskDelimiterProcessor extends org.commonmark.internal.inline.AsteriskDelimiterProcessor {
|
||||||
|
}
|
@ -1,15 +1,16 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.node.Link;
|
import org.commonmark.node.Link;
|
||||||
import org.commonmark.node.Text;
|
import org.commonmark.node.Text;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class AutolinkInline extends Inline {
|
/**
|
||||||
|
* Parses autolinks, for example {@code <me@mydoma.in>}
|
||||||
|
*
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class AutolinkInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
private static final Pattern EMAIL_AUTOLINK = Pattern
|
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])?)*)>");
|
.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])?)*)>");
|
||||||
@ -17,14 +18,13 @@ public class AutolinkInline extends Inline {
|
|||||||
private static final Pattern AUTOLINK = Pattern
|
private static final Pattern AUTOLINK = Pattern
|
||||||
.compile("^<[a-zA-Z][a-zA-Z0-9.+-]{1,31}:[^<>\u0000-\u0020]*>");
|
.compile("^<[a-zA-Z][a-zA-Z0-9.+-]{1,31}:[^<>\u0000-\u0020]*>");
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Character> characters() {
|
public char specialCharacter() {
|
||||||
return Collections.singleton('<');
|
return '<';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean parse() {
|
protected boolean parse() {
|
||||||
String m;
|
String m;
|
||||||
if ((m = match(EMAIL_AUTOLINK)) != null) {
|
if ((m = match(EMAIL_AUTOLINK)) != null) {
|
||||||
String dest = m.substring(1, m.length() - 1);
|
String dest = m.substring(1, m.length() - 1);
|
@ -1,21 +1,23 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.node.HardLineBreak;
|
import org.commonmark.node.HardLineBreak;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.regex.Pattern;
|
||||||
import java.util.Collections;
|
|
||||||
|
/**
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class BackslashInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
private static final Pattern ESCAPABLE = MarkwonInlineParser.ESCAPABLE;
|
||||||
|
|
||||||
public class BackslashInline extends Inline {
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Character> characters() {
|
public char specialCharacter() {
|
||||||
return Collections.singleton('\\');
|
return '\\';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean parse() {
|
protected boolean parse() {
|
||||||
index++;
|
index++;
|
||||||
if (peek() == '\n') {
|
if (peek() == '\n') {
|
||||||
appendNode(new HardLineBreak());
|
appendNode(new HardLineBreak());
|
@ -1,27 +1,29 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.node.Code;
|
import org.commonmark.node.Code;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class BackticksInline extends Inline {
|
/**
|
||||||
|
* 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 = Pattern.compile("`+");
|
||||||
|
|
||||||
private static final Pattern TICKS_HERE = Pattern.compile("^`+");
|
private static final Pattern TICKS_HERE = Pattern.compile("^`+");
|
||||||
|
|
||||||
@NonNull
|
private static final Pattern WHITESPACE = MarkwonInlineParser.WHITESPACE;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Character> characters() {
|
public char specialCharacter() {
|
||||||
return Collections.singleton('`');
|
return '`';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean parse() {
|
protected boolean parse() {
|
||||||
String ticks = match(TICKS_HERE);
|
String ticks = match(TICKS_HERE);
|
||||||
if (ticks == null) {
|
if (ticks == null) {
|
||||||
return false;
|
return false;
|
@ -1,22 +1,21 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.internal.Bracket;
|
import org.commonmark.internal.Bracket;
|
||||||
import org.commonmark.node.Text;
|
import org.commonmark.node.Text;
|
||||||
|
|
||||||
import java.util.Collection;
|
/**
|
||||||
import java.util.Collections;
|
* Parses markdown images {@code }
|
||||||
|
*
|
||||||
public class BangInline extends Inline {
|
* @since 4.2.0-SNAPSHOT
|
||||||
@NonNull
|
*/
|
||||||
|
public class BangInlineProcessor extends InlineProcessor {
|
||||||
@Override
|
@Override
|
||||||
public Collection<Character> characters() {
|
public char specialCharacter() {
|
||||||
return Collections.singleton('!');
|
return '!';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean parse() {
|
protected boolean parse() {
|
||||||
int startIndex = index;
|
int startIndex = index;
|
||||||
index++;
|
index++;
|
||||||
if (peek() == '[') {
|
if (peek() == '[') {
|
@ -1,6 +1,4 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.internal.Bracket;
|
import org.commonmark.internal.Bracket;
|
||||||
import org.commonmark.internal.util.Escaping;
|
import org.commonmark.internal.util.Escaping;
|
||||||
@ -8,18 +6,27 @@ import org.commonmark.node.Image;
|
|||||||
import org.commonmark.node.Link;
|
import org.commonmark.node.Link;
|
||||||
import org.commonmark.node.Node;
|
import org.commonmark.node.Node;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.regex.Pattern;
|
||||||
import java.util.Collections;
|
|
||||||
|
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;
|
||||||
|
|
||||||
public class CloseBracketInline extends Inline {
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Character> characters() {
|
public char specialCharacter() {
|
||||||
return Collections.singleton(']');
|
return ']';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean parse() {
|
protected boolean parse() {
|
||||||
index++;
|
index++;
|
||||||
int startIndex = index;
|
int startIndex = index;
|
||||||
|
|
@ -1,26 +1,26 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.internal.util.Html5Entities;
|
import org.commonmark.internal.util.Html5Entities;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class EntityInline extends Inline {
|
/**
|
||||||
|
* 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 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('^' + ENTITY, Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Character> characters() {
|
public char specialCharacter() {
|
||||||
return Collections.singleton('&');
|
return '&';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean parse() {
|
protected boolean parse() {
|
||||||
String m;
|
String m;
|
||||||
if ((m = match(ENTITY_HERE)) != null) {
|
if ((m = match(ENTITY_HERE)) != null) {
|
||||||
appendText(Html5Entities.entityToString(m));
|
appendText(Html5Entities.entityToString(m));
|
@ -1,14 +1,16 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.internal.util.Parsing;
|
import org.commonmark.internal.util.Parsing;
|
||||||
|
import org.commonmark.node.HtmlInline;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class HtmlInline extends Inline {
|
/**
|
||||||
|
* Parses inline HTML tags
|
||||||
|
*
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class HtmlInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
private static final String HTMLCOMMENT = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->";
|
private static final String HTMLCOMMENT = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->";
|
||||||
private static final String PROCESSINGINSTRUCTION = "[<][?].*?[?][>]";
|
private static final String PROCESSINGINSTRUCTION = "[<][?].*?[?][>]";
|
||||||
@ -18,17 +20,16 @@ public class HtmlInline extends Inline {
|
|||||||
+ "|" + PROCESSINGINSTRUCTION + "|" + DECLARATION + "|" + CDATA + ")";
|
+ "|" + PROCESSINGINSTRUCTION + "|" + DECLARATION + "|" + CDATA + ")";
|
||||||
private static final Pattern HTML_TAG = Pattern.compile('^' + HTMLTAG, Pattern.CASE_INSENSITIVE);
|
private static final Pattern HTML_TAG = Pattern.compile('^' + HTMLTAG, Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Character> characters() {
|
public char specialCharacter() {
|
||||||
return Collections.singleton('<');
|
return '<';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean parse() {
|
protected boolean parse() {
|
||||||
String m = match(HTML_TAG);
|
String m = match(HTML_TAG);
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
org.commonmark.node.HtmlInline node = new org.commonmark.node.HtmlInline();
|
HtmlInline node = new HtmlInline();
|
||||||
node.setLiteral(m);
|
node.setLiteral(m);
|
||||||
appendNode(node);
|
appendNode(node);
|
||||||
return true;
|
return true;
|
@ -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() {
|
||||||
|
}
|
||||||
|
}
|
@ -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<String, Link> 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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,21 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.commonmark.internal.Bracket;
|
import org.commonmark.internal.Bracket;
|
||||||
import org.commonmark.internal.Delimiter;
|
import org.commonmark.internal.Delimiter;
|
||||||
import org.commonmark.internal.ReferenceParser;
|
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.internal.util.Escaping;
|
||||||
import org.commonmark.internal.util.Html5Entities;
|
|
||||||
import org.commonmark.internal.util.Parsing;
|
|
||||||
import org.commonmark.node.Code;
|
|
||||||
import org.commonmark.node.HardLineBreak;
|
|
||||||
import org.commonmark.node.HtmlInline;
|
|
||||||
import org.commonmark.node.Image;
|
|
||||||
import org.commonmark.node.Link;
|
import org.commonmark.node.Link;
|
||||||
import org.commonmark.node.Node;
|
import org.commonmark.node.Node;
|
||||||
import org.commonmark.node.SoftLineBreak;
|
|
||||||
import org.commonmark.node.Text;
|
import org.commonmark.node.Text;
|
||||||
import org.commonmark.parser.InlineParser;
|
import org.commonmark.parser.InlineParser;
|
||||||
|
import org.commonmark.parser.InlineParserContext;
|
||||||
import org.commonmark.parser.InlineParserFactory;
|
import org.commonmark.parser.InlineParserFactory;
|
||||||
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -31,28 +25,66 @@ import java.util.Set;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
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
|
@NonNull
|
||||||
public static InlineParserFactory factory() {
|
public static FactoryBuilder factoryBuilder() {
|
||||||
return context -> new InlineParserOriginal(context.getCustomDelimiterProcessors());
|
return new FactoryBuilderImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String ESCAPED_CHAR = "\\\\" + Escaping.ESCAPABLE;
|
private static final String ESCAPED_CHAR = "\\\\" + Escaping.ESCAPABLE;
|
||||||
private static final String HTMLCOMMENT = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->";
|
|
||||||
private static final String PROCESSINGINSTRUCTION = "[<][?].*?[?][>]";
|
|
||||||
private static final String DECLARATION = "<![A-Z]+\\s+[^>]*>";
|
|
||||||
private static final String CDATA = "<!\\[CDATA\\[[\\s\\S]*?\\]\\]>";
|
|
||||||
private static final String HTMLTAG = "(?:" + Parsing.OPENTAG + "|" + Parsing.CLOSETAG + "|" + HTMLCOMMENT
|
|
||||||
+ "|" + PROCESSINGINSTRUCTION + "|" + DECLARATION + "|" + CDATA + ")";
|
|
||||||
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 String ASCII_PUNCTUATION = "!\"#\\$%&'\\(\\)\\*\\+,\\-\\./:;<=>\\?@\\[\\\\\\]\\^_`\\{\\|\\}~";
|
private static final String ASCII_PUNCTUATION = "!\"#\\$%&'\\(\\)\\*\\+,\\-\\./:;<=>\\?@\\[\\\\\\]\\^_`\\{\\|\\}~";
|
||||||
private static final Pattern PUNCTUATION = Pattern
|
private static final Pattern PUNCTUATION = Pattern
|
||||||
.compile("^[" + ASCII_PUNCTUATION + "\\p{Pc}\\p{Pd}\\p{Pe}\\p{Pf}\\p{Pi}\\p{Po}\\p{Ps}]");
|
.compile("^[" + ASCII_PUNCTUATION + "\\p{Pc}\\p{Pd}\\p{Pe}\\p{Pf}\\p{Pi}\\p{Po}\\p{Ps}]");
|
||||||
|
|
||||||
private static final Pattern HTML_TAG = Pattern.compile('^' + HTMLTAG, Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
private static final Pattern LINK_TITLE = Pattern.compile(
|
private static final Pattern LINK_TITLE = Pattern.compile(
|
||||||
"^(?:\"(" + ESCAPED_CHAR + "|[^\"\\x00])*\"" +
|
"^(?:\"(" + ESCAPED_CHAR + "|[^\"\\x00])*\"" +
|
||||||
'|' +
|
'|' +
|
||||||
@ -64,43 +96,29 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
|
|
||||||
private static final Pattern LINK_LABEL = Pattern.compile("^\\[(?:[^\\\\\\[\\]]|\\\\.)*\\]");
|
private static final Pattern LINK_LABEL = Pattern.compile("^\\[(?:[^\\\\\\[\\]]|\\\\.)*\\]");
|
||||||
|
|
||||||
private static final Pattern ESCAPABLE = Pattern.compile('^' + Escaping.ESCAPABLE);
|
|
||||||
|
|
||||||
private static final Pattern ENTITY_HERE = Pattern.compile('^' + ENTITY, Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
private static final Pattern TICKS = Pattern.compile("`+");
|
|
||||||
|
|
||||||
private static final Pattern TICKS_HERE = Pattern.compile("^`+");
|
|
||||||
|
|
||||||
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]*>");
|
|
||||||
|
|
||||||
private static final Pattern SPNL = Pattern.compile("^ *(?:\n *)?");
|
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 UNICODE_WHITESPACE_CHAR = Pattern.compile("^[\\p{Zs}\t\r\n\f]");
|
||||||
|
|
||||||
private static final Pattern WHITESPACE = Pattern.compile("\\s+");
|
|
||||||
|
|
||||||
private static final Pattern FINAL_SPACE = Pattern.compile(" *$");
|
|
||||||
|
|
||||||
private static final Pattern LINE_END = Pattern.compile("^ *(?:\n|$)");
|
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 BitSet specialCharacters;
|
||||||
private final BitSet delimiterCharacters;
|
private final Map<Character, List<InlineProcessor>> inlineProcessors;
|
||||||
private final Map<Character, DelimiterProcessor> delimiterProcessors;
|
private final Map<Character, DelimiterProcessor> delimiterProcessors;
|
||||||
|
|
||||||
|
private Node block;
|
||||||
|
private String input;
|
||||||
|
private int index;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link references by ID, needs to be built up using parseReference before calling parse.
|
* Link references by ID, needs to be built up using parseReference before calling parse.
|
||||||
*/
|
*/
|
||||||
private Map<String, Link> referenceMap = new HashMap<>();
|
private Map<String, Link> referenceMap = new HashMap<>(1);
|
||||||
|
|
||||||
private Node block;
|
|
||||||
|
|
||||||
private String input;
|
|
||||||
private int index;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top delimiter (emphasis, strong emphasis or custom emphasis). (Brackets are on a separate stack, different
|
* Top delimiter (emphasis, strong emphasis or custom emphasis). (Brackets are on a separate stack, different
|
||||||
@ -113,37 +131,49 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
*/
|
*/
|
||||||
private Bracket lastBracket;
|
private Bracket lastBracket;
|
||||||
|
|
||||||
public InlineParserOriginal(List<DelimiterProcessor> delimiterProcessors) {
|
// might we construct these in factory?
|
||||||
|
public MarkwonInlineParser(
|
||||||
|
boolean referencesEnabled,
|
||||||
|
@NonNull List<InlineProcessor> inlineProcessors,
|
||||||
|
@NonNull List<DelimiterProcessor> delimiterProcessors) {
|
||||||
|
this.referencesEnabled = referencesEnabled;
|
||||||
|
this.inlineProcessors = calculateInlines(inlineProcessors);
|
||||||
this.delimiterProcessors = calculateDelimiterProcessors(delimiterProcessors);
|
this.delimiterProcessors = calculateDelimiterProcessors(delimiterProcessors);
|
||||||
this.delimiterCharacters = calculateDelimiterCharacters(this.delimiterProcessors.keySet());
|
this.specialCharacters = calculateSpecialCharacters(
|
||||||
this.specialCharacters = calculateSpecialCharacters(delimiterCharacters);
|
this.inlineProcessors.keySet(),
|
||||||
|
this.delimiterProcessors.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BitSet calculateDelimiterCharacters(Set<Character> characters) {
|
@NonNull
|
||||||
BitSet bitSet = new BitSet();
|
private static Map<Character, List<InlineProcessor>> calculateInlines(@NonNull List<InlineProcessor> inlines) {
|
||||||
for (Character character : characters) {
|
final Map<Character, List<InlineProcessor>> map = new HashMap<>(inlines.size());
|
||||||
bitSet.set(character);
|
List<InlineProcessor> list;
|
||||||
|
for (InlineProcessor inlineProcessor : inlines) {
|
||||||
|
final char character = inlineProcessor.specialCharacter();
|
||||||
|
list = map.get(character);
|
||||||
|
if (list == null) {
|
||||||
|
list = new ArrayList<>(1);
|
||||||
|
map.put(character, list);
|
||||||
|
}
|
||||||
|
list.add(inlineProcessor);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static BitSet calculateSpecialCharacters(Set<Character> inlineCharacters, Set<Character> delimiterCharacters) {
|
||||||
|
final BitSet bitSet = new BitSet();
|
||||||
|
for (Character c : inlineCharacters) {
|
||||||
|
bitSet.set(c);
|
||||||
|
}
|
||||||
|
for (Character c : delimiterCharacters) {
|
||||||
|
bitSet.set(c);
|
||||||
}
|
}
|
||||||
return bitSet;
|
return bitSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BitSet calculateSpecialCharacters(BitSet delimiterCharacters) {
|
private static Map<Character, DelimiterProcessor> calculateDelimiterProcessors(List<DelimiterProcessor> delimiterProcessors) {
|
||||||
BitSet bitSet = new BitSet();
|
|
||||||
bitSet.or(delimiterCharacters);
|
|
||||||
bitSet.set('\n');
|
|
||||||
bitSet.set('`');
|
|
||||||
bitSet.set('[');
|
|
||||||
bitSet.set(']');
|
|
||||||
bitSet.set('\\');
|
|
||||||
bitSet.set('!');
|
|
||||||
bitSet.set('<');
|
|
||||||
bitSet.set('&');
|
|
||||||
return bitSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Map<Character, DelimiterProcessor> calculateDelimiterProcessors(List<DelimiterProcessor> delimiterProcessors) {
|
|
||||||
Map<Character, DelimiterProcessor> map = new HashMap<>();
|
Map<Character, DelimiterProcessor> map = new HashMap<>();
|
||||||
addDelimiterProcessors(Arrays.<DelimiterProcessor>asList(new AsteriskDelimiterProcessor(), new UnderscoreDelimiterProcessor()), map);
|
|
||||||
addDelimiterProcessors(delimiterProcessors, map);
|
addDelimiterProcessors(delimiterProcessors, map);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
@ -206,6 +236,11 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int parseReference(String s) {
|
public int parseReference(String s) {
|
||||||
|
|
||||||
|
if (!referencesEnabled) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
this.input = s;
|
this.input = s;
|
||||||
this.index = 0;
|
this.index = 0;
|
||||||
String dest;
|
String dest;
|
||||||
@ -275,17 +310,22 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
return index - startIndex;
|
return index - startIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Text appendText(CharSequence text, int beginIndex, int endIndex) {
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Text appendText(@NonNull CharSequence text, int beginIndex, int endIndex) {
|
||||||
return appendText(text.subSequence(beginIndex, endIndex));
|
return appendText(text.subSequence(beginIndex, endIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Text appendText(CharSequence text) {
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public Text appendText(@NonNull CharSequence text) {
|
||||||
Text node = new Text(text.toString());
|
Text node = new Text(text.toString());
|
||||||
appendNode(node);
|
appendNode(node);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendNode(Node node) {
|
@Override
|
||||||
|
public void appendNode(@NonNull Node node) {
|
||||||
block.appendChild(node);
|
block.appendChild(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,46 +335,33 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
* On failure, return false.
|
* On failure, return false.
|
||||||
*/
|
*/
|
||||||
private boolean parseInline() {
|
private boolean parseInline() {
|
||||||
boolean res;
|
|
||||||
char c = peek();
|
final char c = peek();
|
||||||
|
|
||||||
if (c == '\0') {
|
if (c == '\0') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
switch (c) {
|
|
||||||
case '\n':
|
boolean res = false;
|
||||||
res = parseNewline();
|
|
||||||
break;
|
final List<InlineProcessor> inlines = this.inlineProcessors.get(c);
|
||||||
case '\\':
|
|
||||||
res = parseBackslash();
|
if (inlines != null) {
|
||||||
break;
|
for (InlineProcessor inline : inlines) {
|
||||||
case '`':
|
res = inline.parse(this);
|
||||||
res = parseBackticks();
|
if (res) {
|
||||||
break;
|
break;
|
||||||
case '[':
|
|
||||||
res = parseOpenBracket();
|
|
||||||
break;
|
|
||||||
case '!':
|
|
||||||
res = parseBang();
|
|
||||||
break;
|
|
||||||
case ']':
|
|
||||||
res = parseCloseBracket();
|
|
||||||
break;
|
|
||||||
case '<':
|
|
||||||
res = parseAutolink() || parseHtmlInline();
|
|
||||||
break;
|
|
||||||
case '&':
|
|
||||||
res = parseEntity();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
boolean isDelimiter = delimiterCharacters.get(c);
|
|
||||||
if (isDelimiter) {
|
|
||||||
DelimiterProcessor delimiterProcessor = delimiterProcessors.get(c);
|
|
||||||
res = parseDelimiters(delimiterProcessor, c);
|
|
||||||
} else {
|
|
||||||
res = parseString();
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
} else {
|
||||||
|
final DelimiterProcessor delimiterProcessor = delimiterProcessors.get(c);
|
||||||
|
if (delimiterProcessor != null) {
|
||||||
|
res = parseDelimiters(delimiterProcessor, c);
|
||||||
|
} else {
|
||||||
|
res = parseString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
index++;
|
index++;
|
||||||
// When we get here, it's only for a single special character that turned out to not have a special meaning.
|
// When we get here, it's only for a single special character that turned out to not have a special meaning.
|
||||||
@ -349,7 +376,9 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
/**
|
/**
|
||||||
* If RE matches at current index in the input, advance index and return the match; otherwise return null.
|
* If RE matches at current index in the input, advance index and return the match; otherwise return null.
|
||||||
*/
|
*/
|
||||||
private String match(Pattern re) {
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String match(@NonNull Pattern re) {
|
||||||
if (index >= input.length()) {
|
if (index >= input.length()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -367,7 +396,8 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
/**
|
/**
|
||||||
* Returns the char at the current input index, or {@code '\0'} in case there are no more characters.
|
* Returns the char at the current input index, or {@code '\0'} in case there are no more characters.
|
||||||
*/
|
*/
|
||||||
private char peek() {
|
@Override
|
||||||
|
public char peek() {
|
||||||
if (index < input.length()) {
|
if (index < input.length()) {
|
||||||
return input.charAt(index);
|
return input.charAt(index);
|
||||||
} else {
|
} else {
|
||||||
@ -375,87 +405,66 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Node block() {
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String input() {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int index() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setIndex(int index) {
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Bracket lastBracket() {
|
||||||
|
return lastBracket;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Delimiter lastDelimiter() {
|
||||||
|
return lastDelimiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Map<String, Link> referenceMap() {
|
||||||
|
return referenceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addBracket(Bracket bracket) {
|
||||||
|
if (lastBracket != null) {
|
||||||
|
lastBracket.bracketAfter = true;
|
||||||
|
}
|
||||||
|
lastBracket = bracket;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeLastBracket() {
|
||||||
|
lastBracket = lastBracket.previous;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse zero or more space characters, including at most one newline.
|
* Parse zero or more space characters, including at most one newline.
|
||||||
*/
|
*/
|
||||||
private boolean spnl() {
|
@Override
|
||||||
|
public boolean spnl() {
|
||||||
match(SPNL);
|
match(SPNL);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a newline. If it was preceded by two spaces, return a hard line break; otherwise a soft line break.
|
|
||||||
*/
|
|
||||||
private boolean parseNewline() {
|
|
||||||
index++; // assume we're at a \n
|
|
||||||
|
|
||||||
Node lastChild = 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;
|
|
||||||
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());
|
|
||||||
} else {
|
|
||||||
appendNode(new SoftLineBreak());
|
|
||||||
}
|
|
||||||
|
|
||||||
// gobble leading spaces in next line
|
|
||||||
while (peek() == ' ') {
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a backslash-escaped special character, adding either the escaped character, a hard line break
|
|
||||||
* (if the backslash is followed by a newline), or a literal backslash to the block's children.
|
|
||||||
*/
|
|
||||||
private boolean parseBackslash() {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to parse backticks, adding either a backtick code span or a literal sequence of backticks.
|
|
||||||
*/
|
|
||||||
private boolean parseBackticks() {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to parse delimiters like emphasis, strong emphasis or custom delimiters.
|
* Attempt to parse delimiters like emphasis, strong emphasis or custom delimiters.
|
||||||
*/
|
*/
|
||||||
@ -481,174 +490,12 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add open bracket to delimiter stack and add a text node to block's children.
|
|
||||||
*/
|
|
||||||
private boolean parseOpenBracket() {
|
|
||||||
int startIndex = index;
|
|
||||||
index++;
|
|
||||||
|
|
||||||
Text node = appendText("[");
|
|
||||||
|
|
||||||
// Add entry to stack for this opener
|
|
||||||
addBracket(Bracket.link(node, startIndex, lastBracket, lastDelimiter));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If next character is [, and ! delimiter to delimiter stack and add a text node to block's children.
|
|
||||||
* Otherwise just add a text node.
|
|
||||||
*/
|
|
||||||
private boolean parseBang() {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to match close bracket against an opening in the delimiter stack. Add either a link or image, or a
|
|
||||||
* plain [ character, to block's children. If there is a matching delimiter, remove it from the delimiter stack.
|
|
||||||
*/
|
|
||||||
private boolean parseCloseBracket() {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addBracket(Bracket bracket) {
|
|
||||||
if (lastBracket != null) {
|
|
||||||
lastBracket.bracketAfter = true;
|
|
||||||
}
|
|
||||||
lastBracket = bracket;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeLastBracket() {
|
|
||||||
lastBracket = lastBracket.previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to parse link destination, returning the string or null if no match.
|
* Attempt to parse link destination, returning the string or null if no match.
|
||||||
*/
|
*/
|
||||||
private String parseLinkDestination() {
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String parseLinkDestination() {
|
||||||
String res = match(LINK_DESTINATION_BRACES);
|
String res = match(LINK_DESTINATION_BRACES);
|
||||||
if (res != null) { // chop off surrounding <..>:
|
if (res != null) { // chop off surrounding <..>:
|
||||||
if (res.length() == 2) {
|
if (res.length() == 2) {
|
||||||
@ -705,7 +552,9 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
/**
|
/**
|
||||||
* Attempt to parse link title (sans quotes), returning the string or null if no match.
|
* Attempt to parse link title (sans quotes), returning the string or null if no match.
|
||||||
*/
|
*/
|
||||||
private String parseLinkTitle() {
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public String parseLinkTitle() {
|
||||||
String title = match(LINK_TITLE);
|
String title = match(LINK_TITLE);
|
||||||
if (title != null) {
|
if (title != null) {
|
||||||
// chop off quotes from title and unescape:
|
// chop off quotes from title and unescape:
|
||||||
@ -718,7 +567,8 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
/**
|
/**
|
||||||
* Attempt to parse a link label, returning number of characters parsed.
|
* Attempt to parse a link label, returning number of characters parsed.
|
||||||
*/
|
*/
|
||||||
private int parseLinkLabel() {
|
@Override
|
||||||
|
public int parseLinkLabel() {
|
||||||
String m = match(LINK_LABEL);
|
String m = match(LINK_LABEL);
|
||||||
// Spec says "A link label can have at most 999 characters inside the square brackets"
|
// Spec says "A link label can have at most 999 characters inside the square brackets"
|
||||||
if (m == null || m.length() > 1001) {
|
if (m == null || m.length() > 1001) {
|
||||||
@ -728,56 +578,6 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to parse an autolink (URL or email in pointy brackets).
|
|
||||||
*/
|
|
||||||
private boolean parseAutolink() {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to parse inline HTML.
|
|
||||||
*/
|
|
||||||
private boolean parseHtmlInline() {
|
|
||||||
String m = match(HTML_TAG);
|
|
||||||
if (m != null) {
|
|
||||||
HtmlInline node = new HtmlInline();
|
|
||||||
node.setLiteral(m);
|
|
||||||
appendNode(node);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to parse an entity, return Entity object if successful.
|
|
||||||
*/
|
|
||||||
private boolean parseEntity() {
|
|
||||||
String m;
|
|
||||||
if ((m = match(ENTITY_HERE)) != null) {
|
|
||||||
appendText(Html5Entities.entityToString(m));
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a run of ordinary characters, or a single character with a special meaning in markdown, as a plain string.
|
* Parse a run of ordinary characters, or a single character with a special meaning in markdown, as a plain string.
|
||||||
*/
|
*/
|
||||||
@ -849,7 +649,8 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
return new DelimiterData(delimiterCount, canOpen, canClose);
|
return new DelimiterData(delimiterCount, canOpen, canClose);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processDelimiters(Delimiter stackBottom) {
|
@Override
|
||||||
|
public void processDelimiters(Delimiter stackBottom) {
|
||||||
|
|
||||||
Map<Character, Delimiter> openersBottom = new HashMap<>();
|
Map<Character, Delimiter> openersBottom = new HashMap<>();
|
||||||
|
|
||||||
@ -981,70 +782,6 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mergeTextNodesBetweenExclusive(Node fromNode, Node toNode) {
|
|
||||||
// No nodes between them
|
|
||||||
if (fromNode == toNode || fromNode.getNext() == toNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeTextNodesInclusive(fromNode.getNext(), toNode.getPrevious());
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 static class DelimiterData {
|
private static class DelimiterData {
|
||||||
|
|
||||||
final int count;
|
final int count;
|
||||||
@ -1057,4 +794,120 @@ public class InlineParserOriginal implements InlineParser, ReferenceParser {
|
|||||||
this.canClose = canClose;
|
this.canClose = canClose;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class FactoryBuilderImpl implements FactoryBuilder {
|
||||||
|
|
||||||
|
private final List<InlineProcessor> inlineProcessors = new ArrayList<>(3);
|
||||||
|
private final List<DelimiterProcessor> delimiterProcessors = new ArrayList<>(3);
|
||||||
|
private boolean referencesEnabled;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public FactoryBuilder addInlineProcessor(@NonNull InlineProcessor processor) {
|
||||||
|
this.inlineProcessors.add(processor);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public FactoryBuilder addDelimiterProcessor(@NonNull DelimiterProcessor processor) {
|
||||||
|
this.delimiterProcessors.add(processor);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public FactoryBuilder referencesEnabled(boolean referencesEnabled) {
|
||||||
|
this.referencesEnabled = referencesEnabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public FactoryBuilder includeDefaults() {
|
||||||
|
|
||||||
|
// by default enabled
|
||||||
|
this.referencesEnabled = true;
|
||||||
|
|
||||||
|
this.inlineProcessors.addAll(Arrays.asList(
|
||||||
|
new AutolinkInlineProcessor(),
|
||||||
|
new BackslashInlineProcessor(),
|
||||||
|
new BackticksInlineProcessor(),
|
||||||
|
new BangInlineProcessor(),
|
||||||
|
new CloseBracketInlineProcessor(),
|
||||||
|
new EntityInlineProcessor(),
|
||||||
|
new HtmlInlineProcessor(),
|
||||||
|
new NewLineInlineProcessor(),
|
||||||
|
new OpenBracketInlineProcessor()));
|
||||||
|
|
||||||
|
this.delimiterProcessors.addAll(Arrays.asList(
|
||||||
|
new AsteriskDelimiterProcessor(),
|
||||||
|
new UnderscoreDelimiterProcessor()));
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public FactoryBuilder excludeInlineProcessor(@NonNull Class<? extends InlineProcessor> type) {
|
||||||
|
for (int i = 0, size = inlineProcessors.size(); i < size; i++) {
|
||||||
|
if (type.equals(inlineProcessors.get(i).getClass())) {
|
||||||
|
inlineProcessors.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public FactoryBuilder excludeDelimiterProcessor(@NonNull Class<? extends DelimiterProcessor> type) {
|
||||||
|
for (int i = 0, size = delimiterProcessors.size(); i < size; i++) {
|
||||||
|
if (type.equals(delimiterProcessors.get(i).getClass())) {
|
||||||
|
delimiterProcessors.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public InlineParserFactory build() {
|
||||||
|
return new InlineParserFactoryImpl(referencesEnabled, inlineProcessors, delimiterProcessors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class InlineParserFactoryImpl implements InlineParserFactory {
|
||||||
|
|
||||||
|
private final boolean referencesEnabled;
|
||||||
|
private final List<InlineProcessor> inlineProcessors;
|
||||||
|
private final List<DelimiterProcessor> delimiterProcessors;
|
||||||
|
|
||||||
|
InlineParserFactoryImpl(
|
||||||
|
boolean referencesEnabled,
|
||||||
|
@NonNull List<InlineProcessor> inlineProcessors,
|
||||||
|
@NonNull List<DelimiterProcessor> delimiterProcessors) {
|
||||||
|
this.referencesEnabled = referencesEnabled;
|
||||||
|
this.inlineProcessors = inlineProcessors;
|
||||||
|
this.delimiterProcessors = delimiterProcessors;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InlineParser create(InlineParserContext inlineParserContext) {
|
||||||
|
final List<DelimiterProcessor> delimiterProcessors;
|
||||||
|
final List<DelimiterProcessor> customDelimiterProcessors = inlineParserContext.getCustomDelimiterProcessors();
|
||||||
|
final int size = customDelimiterProcessors != null
|
||||||
|
? customDelimiterProcessors.size()
|
||||||
|
: 0;
|
||||||
|
if (size > 0) {
|
||||||
|
delimiterProcessors = new ArrayList<>(size + this.delimiterProcessors.size());
|
||||||
|
delimiterProcessors.addAll(this.delimiterProcessors);
|
||||||
|
delimiterProcessors.addAll(customDelimiterProcessors);
|
||||||
|
} else {
|
||||||
|
delimiterProcessors = this.delimiterProcessors;
|
||||||
|
}
|
||||||
|
return new MarkwonInlineParser(referencesEnabled, inlineProcessors, delimiterProcessors);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
public interface MarkwonInlineParserContext {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
Node block();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
String input();
|
||||||
|
|
||||||
|
int index();
|
||||||
|
|
||||||
|
void setIndex(int index);
|
||||||
|
|
||||||
|
Bracket lastBracket();
|
||||||
|
|
||||||
|
Delimiter lastDelimiter();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
Map<String, Link> referenceMap();
|
||||||
|
|
||||||
|
void addBracket(Bracket bracket);
|
||||||
|
|
||||||
|
void removeLastBracket();
|
||||||
|
|
||||||
|
boolean spnl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the char at the current input index, or {@code '\0'} in case there are no more characters.
|
||||||
|
*/
|
||||||
|
char peek();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
String match(@NonNull Pattern re);
|
||||||
|
|
||||||
|
void appendNode(@NonNull Node node);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
Text appendText(@NonNull CharSequence text, int beginIndex, int endIndex);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
Text appendText(@NonNull CharSequence text);
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
String parseLinkDestination();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
String parseLinkTitle();
|
||||||
|
|
||||||
|
int parseLinkLabel();
|
||||||
|
|
||||||
|
void processDelimiters(Delimiter stackBottom);
|
||||||
|
}
|
@ -1,29 +1,27 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.node.HardLineBreak;
|
import org.commonmark.node.HardLineBreak;
|
||||||
import org.commonmark.node.Node;
|
import org.commonmark.node.Node;
|
||||||
import org.commonmark.node.SoftLineBreak;
|
import org.commonmark.node.SoftLineBreak;
|
||||||
import org.commonmark.node.Text;
|
import org.commonmark.node.Text;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class NewLineInline extends Inline {
|
/**
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class NewLineInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
private static final Pattern FINAL_SPACE = Pattern.compile(" *$");
|
private static final Pattern FINAL_SPACE = Pattern.compile(" *$");
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Character> characters() {
|
public char specialCharacter() {
|
||||||
return Collections.singleton('\n');
|
return '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean parse() {
|
protected boolean parse() {
|
||||||
index++; // assume we're at a \n
|
index++; // assume we're at a \n
|
||||||
|
|
||||||
Node lastChild = block.getLastChild();
|
Node lastChild = block.getLastChild();
|
@ -1,23 +1,21 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.internal.Bracket;
|
import org.commonmark.internal.Bracket;
|
||||||
import org.commonmark.node.Text;
|
import org.commonmark.node.Text;
|
||||||
|
|
||||||
import java.util.Collection;
|
/**
|
||||||
import java.util.Collections;
|
* Parses markdown links {@code [link](#href)}
|
||||||
|
*
|
||||||
public class OpenBracketInline extends Inline {
|
* @since 4.2.0-SNAPSHOT
|
||||||
@NonNull
|
*/
|
||||||
|
public class OpenBracketInlineProcessor extends InlineProcessor {
|
||||||
@Override
|
@Override
|
||||||
public Collection<Character> characters() {
|
public char specialCharacter() {
|
||||||
return Collections.singleton('[');
|
return '[';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean parse() {
|
protected boolean parse() {
|
||||||
|
|
||||||
int startIndex = index;
|
int startIndex = index;
|
||||||
index++;
|
index++;
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
import org.commonmark.node.Text;
|
import org.commonmark.node.Text;
|
||||||
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
||||||
@ -17,7 +17,6 @@ class StaggeredDelimiterProcessor implements DelimiterProcessor {
|
|||||||
this.delim = delim;
|
this.delim = delim;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public char getOpeningCharacter() {
|
public char getOpeningCharacter() {
|
||||||
return delim;
|
return delim;
|
@ -0,0 +1,7 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class UnderscoreDelimiterProcessor extends org.commonmark.internal.inline.UnderscoreDelimiterProcessor {
|
||||||
|
}
|
@ -41,6 +41,7 @@ dependencies {
|
|||||||
implementation project(':markwon-ext-tasklist')
|
implementation project(':markwon-ext-tasklist')
|
||||||
implementation project(':markwon-html')
|
implementation project(':markwon-html')
|
||||||
implementation project(':markwon-image')
|
implementation project(':markwon-image')
|
||||||
|
implementation project(':markwon-inline-parser')
|
||||||
implementation project(':markwon-linkify')
|
implementation project(':markwon-linkify')
|
||||||
implementation project(':markwon-recycler')
|
implementation project(':markwon-recycler')
|
||||||
implementation project(':markwon-recycler-table')
|
implementation project(':markwon-recycler-table')
|
||||||
|
@ -18,18 +18,12 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.commonmark.internal.inline.AsteriskDelimiterProcessor;
|
import org.commonmark.parser.InlineParserFactory;
|
||||||
import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
|
|
||||||
import org.commonmark.node.Link;
|
|
||||||
import org.commonmark.node.Text;
|
|
||||||
import org.commonmark.parser.Parser;
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
import io.noties.markwon.Markwon;
|
import io.noties.markwon.Markwon;
|
||||||
@ -43,17 +37,12 @@ import io.noties.markwon.editor.PersistedSpans;
|
|||||||
import io.noties.markwon.editor.handler.EmphasisEditHandler;
|
import io.noties.markwon.editor.handler.EmphasisEditHandler;
|
||||||
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
|
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
|
||||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
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.linkify.LinkifyPlugin;
|
||||||
import io.noties.markwon.sample.R;
|
import io.noties.markwon.sample.R;
|
||||||
import io.noties.markwon.sample.editor.inline.AutolinkInline;
|
|
||||||
import io.noties.markwon.sample.editor.inline.BackslashInline;
|
|
||||||
import io.noties.markwon.sample.editor.inline.BackticksInline;
|
|
||||||
import io.noties.markwon.sample.editor.inline.CloseBracketInline;
|
|
||||||
import io.noties.markwon.sample.editor.inline.EntityInline;
|
|
||||||
import io.noties.markwon.sample.editor.inline.HtmlInline;
|
|
||||||
import io.noties.markwon.sample.editor.inline.Inline;
|
|
||||||
import io.noties.markwon.sample.editor.inline.InlineParserImpl;
|
|
||||||
import io.noties.markwon.sample.editor.inline.NewLineInline;
|
|
||||||
|
|
||||||
public class EditorActivity extends Activity {
|
public class EditorActivity extends Activity {
|
||||||
|
|
||||||
@ -187,66 +176,15 @@ public class EditorActivity extends Activity {
|
|||||||
// for links to be clickable
|
// for links to be clickable
|
||||||
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
|
||||||
// provider?
|
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||||
final InlineParserImpl.Builder inlineParserFactoryBuilder = InlineParserImpl.builder()
|
.includeDefaults()
|
||||||
.addDelimiterProcessor(new AsteriskDelimiterProcessor())
|
// no inline images will be parsed
|
||||||
.addDelimiterProcessor(new UnderscoreDelimiterProcessor())
|
.excludeInlineProcessor(BangInlineProcessor.class)
|
||||||
.addInlineProcessor(new AutolinkInline())
|
// no html tags will be parsed
|
||||||
.addInlineProcessor(new BackslashInline())
|
.excludeInlineProcessor(HtmlInlineProcessor.class)
|
||||||
.addInlineProcessor(new BackticksInline())
|
// no entities will be parsed (aka `&` etc)
|
||||||
// .addInlineProcessor(new BangInline()) // no images then
|
.excludeInlineProcessor(EntityInlineProcessor.class)
|
||||||
.addInlineProcessor(new CloseBracketInline())
|
.build();
|
||||||
.addInlineProcessor(new EntityInline())
|
|
||||||
.addInlineProcessor(new HtmlInline())
|
|
||||||
.addInlineProcessor(new NewLineInline())
|
|
||||||
.addInlineProcessor(new Inline() {
|
|
||||||
|
|
||||||
private final Pattern RE = Pattern.compile("\\d+");
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Collection<Character> characters() {
|
|
||||||
return Collections.singleton('#');
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean parse() {
|
|
||||||
final String id = match(RE);
|
|
||||||
if (id != null) {
|
|
||||||
final Link link = new Link("https://github.com/noties/Markwon/issues/" + id, null);
|
|
||||||
final Text text = new Text("#" + id);
|
|
||||||
link.appendChild(text);
|
|
||||||
appendNode(link);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.addInlineProcessor(new Inline() {
|
|
||||||
|
|
||||||
private final Pattern RE = Pattern.compile("\\w+");
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Collection<Character> characters() {
|
|
||||||
return Collections.singleton('#');
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean parse() {
|
|
||||||
final String s = match(RE);
|
|
||||||
if (s != null) {
|
|
||||||
final Link link = new Link("https://noties.io", null);
|
|
||||||
final Text text = new Text("#" + s);
|
|
||||||
link.appendChild(text);
|
|
||||||
appendNode(link);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// .addInlineProcessor(new OpenBracketInline())
|
|
||||||
;
|
|
||||||
|
|
||||||
final Markwon markwon = Markwon.builder(this)
|
final Markwon markwon = Markwon.builder(this)
|
||||||
.usePlugin(StrikethroughPlugin.create())
|
.usePlugin(StrikethroughPlugin.create())
|
||||||
@ -254,9 +192,11 @@ public class EditorActivity extends Activity {
|
|||||||
.usePlugin(new AbstractMarkwonPlugin() {
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
@Override
|
@Override
|
||||||
public void configureParser(@NonNull Parser.Builder builder) {
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
|
|
||||||
// disable all commonmark-java blocks, only inlines will be parsed
|
// disable all commonmark-java blocks, only inlines will be parsed
|
||||||
// builder.enabledBlockTypes(Collections.emptySet());
|
// builder.enabledBlockTypes(Collections.emptySet());
|
||||||
builder.inlineParserFactory(inlineParserFactoryBuilder.build());
|
|
||||||
|
builder.inlineParserFactory(inlineParserFactory);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
@ -1,429 +0,0 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.commonmark.internal.Bracket;
|
|
||||||
import org.commonmark.internal.Delimiter;
|
|
||||||
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.delimiter.DelimiterProcessor;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public abstract class Inline {
|
|
||||||
|
|
||||||
private static final String ESCAPED_CHAR = "\\\\" + Escaping.ESCAPABLE;
|
|
||||||
|
|
||||||
protected static final Pattern ESCAPABLE = Pattern.compile('^' + Escaping.ESCAPABLE);
|
|
||||||
|
|
||||||
protected static final Pattern WHITESPACE = Pattern.compile("\\s+");
|
|
||||||
|
|
||||||
protected static final Pattern SPNL = Pattern.compile("^ *(?:\n *)?");
|
|
||||||
|
|
||||||
protected static final Pattern LINK_TITLE = Pattern.compile(
|
|
||||||
"^(?:\"(" + ESCAPED_CHAR + "|[^\"\\x00])*\"" +
|
|
||||||
'|' +
|
|
||||||
"'(" + ESCAPED_CHAR + "|[^'\\x00])*'" +
|
|
||||||
'|' +
|
|
||||||
"\\((" + ESCAPED_CHAR + "|[^)\\x00])*\\))");
|
|
||||||
|
|
||||||
protected static final Pattern LINK_DESTINATION_BRACES = Pattern.compile("^(?:[<](?:[^<> \\t\\n\\\\]|\\\\.)*[>])");
|
|
||||||
|
|
||||||
protected static final Pattern LINK_LABEL = Pattern.compile("^\\[(?:[^\\\\\\[\\]]|\\\\.)*\\]");
|
|
||||||
|
|
||||||
|
|
||||||
protected InlineContext context;
|
|
||||||
protected Node block;
|
|
||||||
protected int index;
|
|
||||||
protected String input;
|
|
||||||
|
|
||||||
protected void bind(
|
|
||||||
@NonNull InlineContext context,
|
|
||||||
@NonNull Node block,
|
|
||||||
@NonNull String input,
|
|
||||||
int index) {
|
|
||||||
this.context = context;
|
|
||||||
this.block = block;
|
|
||||||
this.input = input;
|
|
||||||
this.index = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public abstract Collection<Character> characters();
|
|
||||||
|
|
||||||
public abstract boolean parse();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If RE matches at current index in the input, advance index and return the match; otherwise return null.
|
|
||||||
*/
|
|
||||||
protected String match(Pattern re) {
|
|
||||||
if (index >= input.length()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Matcher matcher = re.matcher(input);
|
|
||||||
matcher.region(index, input.length());
|
|
||||||
boolean m = matcher.find();
|
|
||||||
if (m) {
|
|
||||||
index = matcher.end();
|
|
||||||
return matcher.group();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void appendNode(Node node) {
|
|
||||||
block.appendChild(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Text appendText(CharSequence text, int beginIndex, int endIndex) {
|
|
||||||
return appendText(text.subSequence(beginIndex, endIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Text appendText(CharSequence text) {
|
|
||||||
Text node = new Text(text.toString());
|
|
||||||
appendNode(node);
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the char at the current input index, or {@code '\0'} in case there are no more characters.
|
|
||||||
*/
|
|
||||||
protected char peek() {
|
|
||||||
if (index < input.length()) {
|
|
||||||
return input.charAt(index);
|
|
||||||
} else {
|
|
||||||
return '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void addBracket(Bracket bracket) {
|
|
||||||
final Bracket lastBracket = context.lastBracket();
|
|
||||||
if (lastBracket != null) {
|
|
||||||
lastBracket.bracketAfter = true;
|
|
||||||
}
|
|
||||||
context.lastBracket(bracket);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void removeLastBracket() {
|
|
||||||
final InlineContext context = this.context;
|
|
||||||
context.lastBracket(context.lastBracket().previous);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Bracket lastBracket() {
|
|
||||||
return context.lastBracket();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Delimiter lastDelimiter() {
|
|
||||||
return context.lastDelimiter();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Map<String, Link> referenceMap() {
|
|
||||||
return context.referenceMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Map<Character, DelimiterProcessor> delimiterProcessors() {
|
|
||||||
return context.delimiterProcessors();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse zero or more space characters, including at most one newline.
|
|
||||||
*/
|
|
||||||
protected boolean spnl() {
|
|
||||||
match(SPNL);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to parse link destination, returning the string or null if no match.
|
|
||||||
*/
|
|
||||||
protected 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to parse link title (sans quotes), returning the string or null if no match.
|
|
||||||
*/
|
|
||||||
protected 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 {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to parse a link label, returning number of characters parsed.
|
|
||||||
*/
|
|
||||||
protected 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) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return m.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void processDelimiters(Delimiter stackBottom) {
|
|
||||||
|
|
||||||
Map<Character, Delimiter> openersBottom = new HashMap<>();
|
|
||||||
|
|
||||||
// find first closer above stackBottom:
|
|
||||||
Delimiter closer = lastDelimiter();
|
|
||||||
while (closer != null && closer.previous != stackBottom) {
|
|
||||||
closer = closer.previous;
|
|
||||||
}
|
|
||||||
// move forward, looking for closers, and handling each
|
|
||||||
while (closer != null) {
|
|
||||||
char delimiterChar = closer.delimiterChar;
|
|
||||||
|
|
||||||
DelimiterProcessor delimiterProcessor = delimiterProcessors().get(delimiterChar);
|
|
||||||
if (!closer.canClose || delimiterProcessor == null) {
|
|
||||||
closer = closer.next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
char openingDelimiterChar = delimiterProcessor.getOpeningCharacter();
|
|
||||||
|
|
||||||
// Found delimiter closer. Now look back for first matching opener.
|
|
||||||
int useDelims = 0;
|
|
||||||
boolean openerFound = false;
|
|
||||||
boolean potentialOpenerFound = false;
|
|
||||||
Delimiter opener = closer.previous;
|
|
||||||
while (opener != null && opener != stackBottom && opener != openersBottom.get(delimiterChar)) {
|
|
||||||
if (opener.canOpen && opener.delimiterChar == openingDelimiterChar) {
|
|
||||||
potentialOpenerFound = true;
|
|
||||||
useDelims = delimiterProcessor.getDelimiterUse(opener, closer);
|
|
||||||
if (useDelims > 0) {
|
|
||||||
openerFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opener = opener.previous;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!openerFound) {
|
|
||||||
if (!potentialOpenerFound) {
|
|
||||||
// Set lower bound for future searches for openers.
|
|
||||||
// Only do this when we didn't even have a potential
|
|
||||||
// opener (one that matches the character and can open).
|
|
||||||
// If an opener was rejected because of the number of
|
|
||||||
// delimiters (e.g. because of the "multiple of 3" rule),
|
|
||||||
// we want to consider it next time because the number
|
|
||||||
// of delimiters can change as we continue processing.
|
|
||||||
openersBottom.put(delimiterChar, closer.previous);
|
|
||||||
if (!closer.canOpen) {
|
|
||||||
// We can remove a closer that can't be an opener,
|
|
||||||
// once we've seen there's no matching opener:
|
|
||||||
removeDelimiterKeepNode(closer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closer = closer.next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Text openerNode = opener.node;
|
|
||||||
Text closerNode = closer.node;
|
|
||||||
|
|
||||||
// Remove number of used delimiters from stack and inline nodes.
|
|
||||||
opener.length -= useDelims;
|
|
||||||
closer.length -= useDelims;
|
|
||||||
openerNode.setLiteral(
|
|
||||||
openerNode.getLiteral().substring(0,
|
|
||||||
openerNode.getLiteral().length() - useDelims));
|
|
||||||
closerNode.setLiteral(
|
|
||||||
closerNode.getLiteral().substring(0,
|
|
||||||
closerNode.getLiteral().length() - useDelims));
|
|
||||||
|
|
||||||
removeDelimitersBetween(opener, closer);
|
|
||||||
// The delimiter processor can re-parent the nodes between opener and closer,
|
|
||||||
// so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
|
|
||||||
mergeTextNodesBetweenExclusive(openerNode, closerNode);
|
|
||||||
delimiterProcessor.process(openerNode, closerNode, useDelims);
|
|
||||||
|
|
||||||
// No delimiter characters left to process, so we can remove delimiter and the now empty node.
|
|
||||||
if (opener.length == 0) {
|
|
||||||
removeDelimiterAndNode(opener);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closer.length == 0) {
|
|
||||||
Delimiter next = closer.next;
|
|
||||||
removeDelimiterAndNode(closer);
|
|
||||||
closer = next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove all delimiters
|
|
||||||
Delimiter lastDelimiter;
|
|
||||||
while ((lastDelimiter = lastDelimiter()) != null) {
|
|
||||||
if (lastDelimiter != stackBottom) {
|
|
||||||
removeDelimiterKeepNode(lastDelimiter);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// while (lastDelimiter != null && lastDelimiter != stackBottom) {
|
|
||||||
// removeDelimiterKeepNode(lastDelimiter);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void mergeTextNodesBetweenExclusive(Node fromNode, Node toNode) {
|
|
||||||
// No nodes between them
|
|
||||||
if (fromNode == toNode || fromNode.getNext() == toNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeTextNodesInclusive(fromNode.getNext(), toNode.getPrevious());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void removeDelimitersBetween(Delimiter opener, Delimiter closer) {
|
|
||||||
Delimiter delimiter = closer.previous;
|
|
||||||
while (delimiter != null && delimiter != opener) {
|
|
||||||
Delimiter previousDelimiter = delimiter.previous;
|
|
||||||
removeDelimiterKeepNode(delimiter);
|
|
||||||
delimiter = previousDelimiter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the delimiter and the corresponding text node. For used delimiters, e.g. `*` in `*foo*`.
|
|
||||||
*/
|
|
||||||
protected void removeDelimiterAndNode(Delimiter delim) {
|
|
||||||
Text node = delim.node;
|
|
||||||
node.unlink();
|
|
||||||
removeDelimiter(delim);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the delimiter but keep the corresponding node as text. For unused delimiters such as `_` in `foo_bar`.
|
|
||||||
*/
|
|
||||||
protected void removeDelimiterKeepNode(Delimiter delim) {
|
|
||||||
removeDelimiter(delim);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void removeDelimiter(Delimiter delim) {
|
|
||||||
if (delim.previous != null) {
|
|
||||||
delim.previous.next = delim.next;
|
|
||||||
}
|
|
||||||
if (delim.next == null) {
|
|
||||||
// top of stack
|
|
||||||
// lastDelimiter = delim.previous;
|
|
||||||
context.lastDelimiter(delim.previous);
|
|
||||||
} else {
|
|
||||||
delim.next.previous = delim.previous;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
package io.noties.markwon.sample.editor.inline;
|
|
||||||
|
|
||||||
import org.commonmark.internal.Bracket;
|
|
||||||
import org.commonmark.internal.Delimiter;
|
|
||||||
import org.commonmark.node.Link;
|
|
||||||
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class InlineContext {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 (<code>[</code> or <code>![)</code>).
|
|
||||||
*/
|
|
||||||
private Bracket lastBracket;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Link references by ID, needs to be built up using parseReference before calling parse.
|
|
||||||
*/
|
|
||||||
private Map<String, Link> referenceMap;
|
|
||||||
|
|
||||||
private Map<Character, DelimiterProcessor> delimiterProcessors;
|
|
||||||
|
|
||||||
|
|
||||||
public Delimiter lastDelimiter() {
|
|
||||||
return lastDelimiter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void lastDelimiter(Delimiter lastDelimiter) {
|
|
||||||
this.lastDelimiter = lastDelimiter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bracket lastBracket() {
|
|
||||||
return lastBracket;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void lastBracket(Bracket lastBracket) {
|
|
||||||
this.lastBracket = lastBracket;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, Link> referenceMap() {
|
|
||||||
return referenceMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void referenceMap(Map<String, Link> referenceMap) {
|
|
||||||
this.referenceMap = referenceMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<Character, DelimiterProcessor> delimiterProcessors() {
|
|
||||||
return delimiterProcessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delimiterProcessors(Map<Character, DelimiterProcessor> delimiterProcessors) {
|
|
||||||
this.delimiterProcessors = delimiterProcessors;
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@ include ':app', ':sample',
|
|||||||
':markwon-image',
|
':markwon-image',
|
||||||
':markwon-image-glide',
|
':markwon-image-glide',
|
||||||
':markwon-image-picasso',
|
':markwon-image-picasso',
|
||||||
|
':markwon-inline-parser',
|
||||||
':markwon-linkify',
|
':markwon-linkify',
|
||||||
':markwon-recycler',
|
':markwon-recycler',
|
||||||
':markwon-recycler-table',
|
':markwon-recycler-table',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user