Commonmark-java inline parser implementation
This commit is contained in:
commit
3c23140ac0
@ -4,6 +4,7 @@
|
|||||||
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
|
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
|
||||||
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])
|
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])
|
||||||
<br>Thanks to [@tylerbwong]
|
<br>Thanks to [@tylerbwong]
|
||||||
|
* `MarkwonInlineParser` to customize inline parsing (new module: `markwon-inline-parser`)
|
||||||
* `Markwon#configuration` method to expose `MarkwonConfiguration` via public API
|
* `Markwon#configuration` method to expose `MarkwonConfiguration` via public API
|
||||||
* `HeadingSpan#getLevel` getter
|
* `HeadingSpan#getLevel` getter
|
||||||
* Add `SvgPictureMediaDecoder` in `image` module to deal with SVG without dimensions ([#165])
|
* Add `SvgPictureMediaDecoder` in `image` module to deal with SVG without dimensions ([#165])
|
||||||
|
@ -83,10 +83,11 @@ ext {
|
|||||||
|
|
||||||
deps['test'] = [
|
deps['test'] = [
|
||||||
'junit' : 'junit:junit:4.12',
|
'junit' : 'junit:junit:4.12',
|
||||||
'robolectric': 'org.robolectric:robolectric:3.8',
|
'robolectric' : 'org.robolectric:robolectric:3.8',
|
||||||
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
|
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
|
||||||
'commons-io' : 'commons-io:commons-io:2.6',
|
'commons-io' : 'commons-io:commons-io:2.6',
|
||||||
'mockito' : 'org.mockito:mockito-core:2.21.0'
|
'mockito' : 'org.mockito:mockito-core:2.21.0',
|
||||||
|
'commonmark-test-util': "com.atlassian.commonmark:commonmark-test-util:$commonMarkVersion",
|
||||||
]
|
]
|
||||||
|
|
||||||
registerArtifact = this.®isterArtifact
|
registerArtifact = this.®isterArtifact
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
|
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
|
||||||
const artifacts = [{"id":"core","name":"Core","group":"io.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"editor","name":"Editor","group":"io.noties.markwon","description":"Markdown editor based on Markwon"},{"id":"ext-latex","name":"LaTeX","group":"io.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"io.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"io.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"io.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"io.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image","name":"Image","group":"io.noties.markwon","description":"Markwon image loading module (with optional GIF and SVG support)"},{"id":"image-glide","name":"Image Glide","group":"io.noties.markwon","description":"Markwon image loading module (based on Glide library)"},{"id":"image-picasso","name":"Image Picasso","group":"io.noties.markwon","description":"Markwon image loading module (based on Picasso library)"},{"id":"linkify","name":"Linkify","group":"io.noties.markwon","description":"Markwon plugin to linkify text (based on Android Linkify)"},{"id":"recycler","name":"Recycler","group":"io.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"io.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"simple-ext","name":"Simple Extension","group":"io.noties.markwon","description":"Custom extension based on simple delimiter usage"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"io.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
|
const artifacts = [{"id":"core","name":"Core","group":"io.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"editor","name":"Editor","group":"io.noties.markwon","description":"Markdown editor based on Markwon"},{"id":"ext-latex","name":"LaTeX","group":"io.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"io.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"io.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"io.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"io.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image","name":"Image","group":"io.noties.markwon","description":"Markwon image loading module (with optional GIF and SVG support)"},{"id":"image-coil","name":"Image Coil","group":"io.noties.markwon","description":"Markwon image loading module (based on Coil library)"},{"id":"image-glide","name":"Image Glide","group":"io.noties.markwon","description":"Markwon image loading module (based on Glide library)"},{"id":"image-picasso","name":"Image Picasso","group":"io.noties.markwon","description":"Markwon image loading module (based on Picasso library)"},{"id":"inline-parser","name":"Inline Parser","group":"io.noties.markwon","description":"Markwon customizable commonmark-java InlineParser"},{"id":"linkify","name":"Linkify","group":"io.noties.markwon","description":"Markwon plugin to linkify text (based on Android Linkify)"},{"id":"recycler","name":"Recycler","group":"io.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"io.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"simple-ext","name":"Simple Extension","group":"io.noties.markwon","description":"Custom extension based on simple delimiter usage"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"io.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
|
||||||
export { artifacts };
|
export { artifacts };
|
||||||
|
@ -105,6 +105,7 @@ module.exports = {
|
|||||||
'/docs/v4/image-coil/',
|
'/docs/v4/image-coil/',
|
||||||
'/docs/v4/image-glide/',
|
'/docs/v4/image-glide/',
|
||||||
'/docs/v4/image-picasso/',
|
'/docs/v4/image-picasso/',
|
||||||
|
'/docs/v4/inline-parser/',
|
||||||
'/docs/v4/linkify/',
|
'/docs/v4/linkify/',
|
||||||
'/docs/v4/recycler/',
|
'/docs/v4/recycler/',
|
||||||
'/docs/v4/recycler-table/',
|
'/docs/v4/recycler-table/',
|
||||||
|
78
docs/docs/v4/inline-parser/README.md
Normal file
78
docs/docs/v4/inline-parser/README.md
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Inline Parser <Badge text="4.2.0" />
|
||||||
|
|
||||||
|
**Experimental** commonmark-java inline parser that allows customizing
|
||||||
|
core features and/or extend with own.
|
||||||
|
|
||||||
|
Usage of _internal_ classes:
|
||||||
|
```java
|
||||||
|
import org.commonmark.internal.Bracket;
|
||||||
|
import org.commonmark.internal.Delimiter;
|
||||||
|
import org.commonmark.internal.ReferenceParser;
|
||||||
|
import org.commonmark.internal.util.Escaping;
|
||||||
|
import org.commonmark.internal.util.Html5Entities;
|
||||||
|
import org.commonmark.internal.util.Parsing;
|
||||||
|
import org.commonmark.internal.inline.AsteriskDelimiterProcessor;
|
||||||
|
import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```java
|
||||||
|
// all default (like current commonmark-java InlineParserImpl)
|
||||||
|
final InlineParserFactory factory = MarkwonInlineParser.factoryBuilder()
|
||||||
|
.includeDefaults()
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
// disable images (current markdown images will be considered as links):
|
||||||
|
final InlineParserFactory factory = MarkwonInlineParser.factoryBuilder()
|
||||||
|
.includeDefaults()
|
||||||
|
.excludeInlineProcessor(BangInlineProcessor.class)
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
// disable core delimiter processors for `*`|`_` and `**`|`__`
|
||||||
|
final InlineParserFactory factory = MarkwonInlineParser.factoryBuilder()
|
||||||
|
.includeDefaults()
|
||||||
|
.excludeDelimiterProcessor(AsteriskDelimiterProcessor.class)
|
||||||
|
.excludeDelimiterProcessor(UnderscoreDelimiterProcessor.class)
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
```java
|
||||||
|
// disable _all_ markdown inlines except for links (open and close bracket handling `[` & `]`)
|
||||||
|
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||||
|
// note that there is no `includeDefaults` method call
|
||||||
|
.referencesEnabled(true)
|
||||||
|
.addInlineProcessor(new OpenBracketInlineProcessor())
|
||||||
|
.addInlineProcessor(new CloseBracketInlineProcessor())
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
To use custom InlineParser:
|
||||||
|
```java
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
|
builder.inlineParserFactory(inlineParserFactory);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The list of available inline processors:
|
||||||
|
|
||||||
|
* `AutolinkInlineProcessor` (`<` => `<me@mydoma.in>`)
|
||||||
|
* `BackslashInlineProcessor` (`\\`)
|
||||||
|
* `BackticksInlineProcessor` (<code>`</code> => <code>`code`</code>)
|
||||||
|
* `BangInlineProcessor` (`!` => ``)
|
||||||
|
* `CloseBracketInlineProcessor` (`]` => `[link](#href)`, ``)
|
||||||
|
* `EntityInlineProcessor` (`&` => `&`)
|
||||||
|
* `HtmlInlineProcessor` (`<` => `<html></html>`)
|
||||||
|
* `NewLineInlineProcessor` (`\n`)
|
||||||
|
* `OpenBracketInlineProcessor` (`[` => `[link](#href)`)
|
16
markwon-inline-parser/README.md
Normal file
16
markwon-inline-parser/README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Inline parser
|
||||||
|
|
||||||
|
**Experimental** due to usage of internal (but still visible) classes of commonmark-java:
|
||||||
|
|
||||||
|
```java
|
||||||
|
import org.commonmark.internal.Bracket;
|
||||||
|
import org.commonmark.internal.Delimiter;
|
||||||
|
import org.commonmark.internal.ReferenceParser;
|
||||||
|
import org.commonmark.internal.util.Escaping;
|
||||||
|
import org.commonmark.internal.util.Html5Entities;
|
||||||
|
import org.commonmark.internal.util.Parsing;
|
||||||
|
import org.commonmark.internal.inline.AsteriskDelimiterProcessor;
|
||||||
|
import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
|
||||||
|
```
|
||||||
|
|
||||||
|
`StaggeredDelimiterProcessor` class source is copied (required for InlineParser)
|
26
markwon-inline-parser/build.gradle
Normal file
26
markwon-inline-parser/build.gradle
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
|
||||||
|
compileSdkVersion config['compile-sdk']
|
||||||
|
buildToolsVersion config['build-tools']
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion config['min-sdk']
|
||||||
|
targetSdkVersion config['target-sdk']
|
||||||
|
versionCode 1
|
||||||
|
versionName version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api deps['x-annotations']
|
||||||
|
api deps['commonmark']
|
||||||
|
|
||||||
|
deps['test'].with {
|
||||||
|
testImplementation it['junit']
|
||||||
|
testImplementation it['commonmark-test-util']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerArtifact(this)
|
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 InlineParser
|
||||||
|
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,45 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import org.commonmark.node.Link;
|
||||||
|
import org.commonmark.node.Text;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses autolinks, for example {@code <me@mydoma.in>}
|
||||||
|
*
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class AutolinkInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
private static final Pattern EMAIL_AUTOLINK = Pattern
|
||||||
|
.compile("^<([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>");
|
||||||
|
|
||||||
|
private static final Pattern AUTOLINK = Pattern
|
||||||
|
.compile("^<[a-zA-Z][a-zA-Z0-9.+-]{1,31}:[^<>\u0000-\u0020]*>");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '<';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean parse() {
|
||||||
|
String m;
|
||||||
|
if ((m = match(EMAIL_AUTOLINK)) != null) {
|
||||||
|
String dest = m.substring(1, m.length() - 1);
|
||||||
|
Link node = new Link("mailto:" + dest, null);
|
||||||
|
node.appendChild(new Text(dest));
|
||||||
|
appendNode(node);
|
||||||
|
return true;
|
||||||
|
} else if ((m = match(AUTOLINK)) != null) {
|
||||||
|
String dest = m.substring(1, m.length() - 1);
|
||||||
|
Link node = new Link(dest, null);
|
||||||
|
node.appendChild(new Text(dest));
|
||||||
|
appendNode(node);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import org.commonmark.node.HardLineBreak;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class BackslashInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
private static final Pattern ESCAPABLE = MarkwonInlineParser.ESCAPABLE;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean parse() {
|
||||||
|
index++;
|
||||||
|
if (peek() == '\n') {
|
||||||
|
appendNode(new HardLineBreak());
|
||||||
|
index++;
|
||||||
|
} else if (index < input.length() && ESCAPABLE.matcher(input.substring(index, index + 1)).matches()) {
|
||||||
|
appendText(input, index, index + 1);
|
||||||
|
index++;
|
||||||
|
} else {
|
||||||
|
appendText("\\");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import org.commonmark.node.Code;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses inline code surrounded with {@code `} chars {@code `code`}
|
||||||
|
*
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class BackticksInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
private static final Pattern TICKS = Pattern.compile("`+");
|
||||||
|
|
||||||
|
private static final Pattern TICKS_HERE = Pattern.compile("^`+");
|
||||||
|
|
||||||
|
private static final Pattern WHITESPACE = MarkwonInlineParser.WHITESPACE;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '`';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean parse() {
|
||||||
|
String ticks = match(TICKS_HERE);
|
||||||
|
if (ticks == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int afterOpenTicks = index;
|
||||||
|
String matched;
|
||||||
|
while ((matched = match(TICKS)) != null) {
|
||||||
|
if (matched.equals(ticks)) {
|
||||||
|
Code node = new Code();
|
||||||
|
String content = input.substring(afterOpenTicks, index - ticks.length());
|
||||||
|
String literal = WHITESPACE.matcher(content.trim()).replaceAll(" ");
|
||||||
|
node.setLiteral(literal);
|
||||||
|
appendNode(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we got here, we didn't match a closing backtick sequence.
|
||||||
|
index = afterOpenTicks;
|
||||||
|
appendText(ticks);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import org.commonmark.internal.Bracket;
|
||||||
|
import org.commonmark.node.Text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses markdown images {@code }
|
||||||
|
*
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class BangInlineProcessor extends InlineProcessor {
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '!';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean parse() {
|
||||||
|
int startIndex = index;
|
||||||
|
index++;
|
||||||
|
if (peek() == '[') {
|
||||||
|
index++;
|
||||||
|
|
||||||
|
Text node = appendText("![");
|
||||||
|
|
||||||
|
// Add entry to stack for this opener
|
||||||
|
addBracket(Bracket.image(node, startIndex + 1, lastBracket(), lastDelimiter()));
|
||||||
|
} else {
|
||||||
|
appendText("!");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import org.commonmark.internal.Bracket;
|
||||||
|
import org.commonmark.internal.util.Escaping;
|
||||||
|
import org.commonmark.node.Image;
|
||||||
|
import org.commonmark.node.Link;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static io.noties.markwon.inlineparser.InlineParserUtils.mergeChildTextNodes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses markdown link or image, relies on {@link OpenBracketInlineProcessor}
|
||||||
|
* to handle start of these elements
|
||||||
|
*
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class CloseBracketInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
private static final Pattern WHITESPACE = MarkwonInlineParser.WHITESPACE;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean parse() {
|
||||||
|
index++;
|
||||||
|
int startIndex = index;
|
||||||
|
|
||||||
|
// Get previous `[` or `![`
|
||||||
|
Bracket opener = lastBracket();
|
||||||
|
if (opener == null) {
|
||||||
|
// No matching opener, just return a literal.
|
||||||
|
appendText("]");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opener.allowed) {
|
||||||
|
// Matching opener but it's not allowed, just return a literal.
|
||||||
|
appendText("]");
|
||||||
|
removeLastBracket();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if we have a link/image
|
||||||
|
|
||||||
|
String dest = null;
|
||||||
|
String title = null;
|
||||||
|
boolean isLinkOrImage = false;
|
||||||
|
|
||||||
|
// Maybe a inline link like `[foo](/uri "title")`
|
||||||
|
if (peek() == '(') {
|
||||||
|
index++;
|
||||||
|
spnl();
|
||||||
|
if ((dest = parseLinkDestination()) != null) {
|
||||||
|
spnl();
|
||||||
|
// title needs a whitespace before
|
||||||
|
if (WHITESPACE.matcher(input.substring(index - 1, index)).matches()) {
|
||||||
|
title = parseLinkTitle();
|
||||||
|
spnl();
|
||||||
|
}
|
||||||
|
if (peek() == ')') {
|
||||||
|
index++;
|
||||||
|
isLinkOrImage = true;
|
||||||
|
} else {
|
||||||
|
index = startIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maybe a reference link like `[foo][bar]`, `[foo][]` or `[foo]`
|
||||||
|
if (!isLinkOrImage) {
|
||||||
|
|
||||||
|
// See if there's a link label like `[bar]` or `[]`
|
||||||
|
int beforeLabel = index;
|
||||||
|
int labelLength = parseLinkLabel();
|
||||||
|
String ref = null;
|
||||||
|
if (labelLength > 2) {
|
||||||
|
ref = input.substring(beforeLabel, beforeLabel + labelLength);
|
||||||
|
} else if (!opener.bracketAfter) {
|
||||||
|
// If the second label is empty `[foo][]` or missing `[foo]`, then the first label is the reference.
|
||||||
|
// But it can only be a reference when there's no (unescaped) bracket in it.
|
||||||
|
// If there is, we don't even need to try to look up the reference. This is an optimization.
|
||||||
|
ref = input.substring(opener.index, startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref != null) {
|
||||||
|
Link link = referenceMap().get(Escaping.normalizeReference(ref));
|
||||||
|
if (link != null) {
|
||||||
|
dest = link.getDestination();
|
||||||
|
title = link.getTitle();
|
||||||
|
isLinkOrImage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinkOrImage) {
|
||||||
|
// If we got here, open is a potential opener
|
||||||
|
Node linkOrImage = opener.image ? new Image(dest, title) : new Link(dest, title);
|
||||||
|
|
||||||
|
Node node = opener.node.getNext();
|
||||||
|
while (node != null) {
|
||||||
|
Node next = node.getNext();
|
||||||
|
linkOrImage.appendChild(node);
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
appendNode(linkOrImage);
|
||||||
|
|
||||||
|
// Process delimiters such as emphasis inside link/image
|
||||||
|
processDelimiters(opener.previousDelimiter);
|
||||||
|
mergeChildTextNodes(linkOrImage);
|
||||||
|
// We don't need the corresponding text node anymore, we turned it into a link/image node
|
||||||
|
opener.node.unlink();
|
||||||
|
removeLastBracket();
|
||||||
|
|
||||||
|
// Links within links are not allowed. We found this link, so there can be no other link around it.
|
||||||
|
if (!opener.image) {
|
||||||
|
Bracket bracket = lastBracket();
|
||||||
|
while (bracket != null) {
|
||||||
|
if (!bracket.image) {
|
||||||
|
// Disallow link opener. It will still get matched, but will not result in a link.
|
||||||
|
bracket.allowed = false;
|
||||||
|
}
|
||||||
|
bracket = bracket.previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} else { // no link or image
|
||||||
|
|
||||||
|
appendText("]");
|
||||||
|
removeLastBracket();
|
||||||
|
|
||||||
|
index = startIndex;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import org.commonmark.internal.util.Html5Entities;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses HTML entities {@code &}
|
||||||
|
*
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class EntityInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
private static final String ENTITY = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});";
|
||||||
|
private static final Pattern ENTITY_HERE = Pattern.compile('^' + ENTITY, Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '&';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean parse() {
|
||||||
|
String m;
|
||||||
|
if ((m = match(ENTITY_HERE)) != null) {
|
||||||
|
appendText(Html5Entities.entityToString(m));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import org.commonmark.internal.util.Parsing;
|
||||||
|
import org.commonmark.node.HtmlInline;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses inline HTML tags
|
||||||
|
*
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class HtmlInlineProcessor extends InlineProcessor {
|
||||||
|
|
||||||
|
private static final String HTMLCOMMENT = "<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->";
|
||||||
|
private static final String PROCESSINGINSTRUCTION = "[<][?].*?[?][>]";
|
||||||
|
private static final String DECLARATION = "<![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 Pattern HTML_TAG = Pattern.compile('^' + HTMLTAG, Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '<';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean parse() {
|
||||||
|
String m = match(HTML_TAG);
|
||||||
|
if (m != null) {
|
||||||
|
HtmlInline node = new HtmlInline();
|
||||||
|
node.setLiteral(m);
|
||||||
|
appendNode(node);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,915 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.commonmark.internal.Bracket;
|
||||||
|
import org.commonmark.internal.Delimiter;
|
||||||
|
import org.commonmark.internal.ReferenceParser;
|
||||||
|
import org.commonmark.internal.inline.AsteriskDelimiterProcessor;
|
||||||
|
import org.commonmark.internal.inline.UnderscoreDelimiterProcessor;
|
||||||
|
import org.commonmark.internal.util.Escaping;
|
||||||
|
import org.commonmark.node.Link;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.node.Text;
|
||||||
|
import org.commonmark.parser.InlineParser;
|
||||||
|
import org.commonmark.parser.InlineParserContext;
|
||||||
|
import org.commonmark.parser.InlineParserFactory;
|
||||||
|
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.BitSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static io.noties.markwon.inlineparser.InlineParserUtils.mergeChildTextNodes;
|
||||||
|
import static io.noties.markwon.inlineparser.InlineParserUtils.mergeTextNodesBetweenExclusive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see #factoryBuilder()
|
||||||
|
* @see FactoryBuilder
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class MarkwonInlineParser implements InlineParser, ReferenceParser, MarkwonInlineParserContext {
|
||||||
|
|
||||||
|
public interface FactoryBuilder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see InlineProcessor
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
FactoryBuilder addInlineProcessor(@NonNull InlineProcessor processor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see AsteriskDelimiterProcessor
|
||||||
|
* @see UnderscoreDelimiterProcessor
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
FactoryBuilder addDelimiterProcessor(@NonNull DelimiterProcessor processor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate if markdown references are enabled. {@code referencesEnabled=true} if {@link #includeDefaults()}
|
||||||
|
* was called
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
FactoryBuilder referencesEnabled(boolean referencesEnabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Includes all default delimiter and inline processors, and sets {@code referencesEnabled=true}.
|
||||||
|
* Useful with subsequent calls to {@link #excludeInlineProcessor(Class)} or {@link #excludeDelimiterProcessor(Class)}
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
FactoryBuilder includeDefaults();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
FactoryBuilder excludeInlineProcessor(@NonNull Class<? extends InlineProcessor> processor);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
FactoryBuilder excludeDelimiterProcessor(@NonNull Class<? extends DelimiterProcessor> processor);
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
InlineParserFactory build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static FactoryBuilder factoryBuilder() {
|
||||||
|
return new FactoryBuilderImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String ESCAPED_CHAR = "\\\\" + Escaping.ESCAPABLE;
|
||||||
|
|
||||||
|
private static final String ASCII_PUNCTUATION = "!\"#\\$%&'\\(\\)\\*\\+,\\-\\./:;<=>\\?@\\[\\\\\\]\\^_`\\{\\|\\}~";
|
||||||
|
private static final Pattern PUNCTUATION = Pattern
|
||||||
|
.compile("^[" + ASCII_PUNCTUATION + "\\p{Pc}\\p{Pd}\\p{Pe}\\p{Pf}\\p{Pi}\\p{Po}\\p{Ps}]");
|
||||||
|
|
||||||
|
private static final Pattern LINK_TITLE = Pattern.compile(
|
||||||
|
"^(?:\"(" + ESCAPED_CHAR + "|[^\"\\x00])*\"" +
|
||||||
|
'|' +
|
||||||
|
"'(" + ESCAPED_CHAR + "|[^'\\x00])*'" +
|
||||||
|
'|' +
|
||||||
|
"\\((" + ESCAPED_CHAR + "|[^)\\x00])*\\))");
|
||||||
|
|
||||||
|
private static final Pattern LINK_DESTINATION_BRACES = Pattern.compile("^(?:[<](?:[^<> \\t\\n\\\\]|\\\\.)*[>])");
|
||||||
|
|
||||||
|
private static final Pattern LINK_LABEL = Pattern.compile("^\\[(?:[^\\\\\\[\\]]|\\\\.)*\\]");
|
||||||
|
|
||||||
|
private static final Pattern SPNL = Pattern.compile("^ *(?:\n *)?");
|
||||||
|
|
||||||
|
private static final Pattern UNICODE_WHITESPACE_CHAR = Pattern.compile("^[\\p{Zs}\t\r\n\f]");
|
||||||
|
|
||||||
|
private static final Pattern LINE_END = Pattern.compile("^ *(?:\n|$)");
|
||||||
|
|
||||||
|
static final Pattern ESCAPABLE = Pattern.compile('^' + Escaping.ESCAPABLE);
|
||||||
|
static final Pattern WHITESPACE = Pattern.compile("\\s+");
|
||||||
|
|
||||||
|
private final boolean referencesEnabled;
|
||||||
|
|
||||||
|
private final BitSet specialCharacters;
|
||||||
|
private final Map<Character, List<InlineProcessor>> inlineProcessors;
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
private Map<String, Link> referenceMap = new HashMap<>(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top delimiter (emphasis, strong emphasis or custom emphasis). (Brackets are on a separate stack, different
|
||||||
|
* from the algorithm described in the spec.)
|
||||||
|
*/
|
||||||
|
private Delimiter lastDelimiter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top opening bracket (<code>[</code> or <code>}
|
||||||
|
*
|
||||||
|
* @since 4.2.0-SNAPSHOT
|
||||||
|
*/
|
||||||
|
public class OpenBracketInlineProcessor extends InlineProcessor {
|
||||||
|
@Override
|
||||||
|
public char specialCharacter() {
|
||||||
|
return '[';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean parse() {
|
||||||
|
int startIndex = index;
|
||||||
|
index++;
|
||||||
|
|
||||||
|
Text node = appendText("[");
|
||||||
|
|
||||||
|
// Add entry to stack for this opener
|
||||||
|
addBracket(Bracket.link(node, startIndex, lastBracket(), lastDelimiter()));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import org.commonmark.node.Text;
|
||||||
|
import org.commonmark.parser.delimiter.DelimiterProcessor;
|
||||||
|
import org.commonmark.parser.delimiter.DelimiterRun;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.ListIterator;
|
||||||
|
|
||||||
|
class StaggeredDelimiterProcessor implements DelimiterProcessor {
|
||||||
|
|
||||||
|
private final char delim;
|
||||||
|
private int minLength = 0;
|
||||||
|
private LinkedList<DelimiterProcessor> processors = new LinkedList<>(); // in reverse getMinLength order
|
||||||
|
|
||||||
|
StaggeredDelimiterProcessor(char delim) {
|
||||||
|
this.delim = delim;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char getOpeningCharacter() {
|
||||||
|
return delim;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char getClosingCharacter() {
|
||||||
|
return delim;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMinLength() {
|
||||||
|
return minLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(DelimiterProcessor dp) {
|
||||||
|
final int len = dp.getMinLength();
|
||||||
|
ListIterator<DelimiterProcessor> it = processors.listIterator();
|
||||||
|
boolean added = false;
|
||||||
|
while (it.hasNext()) {
|
||||||
|
DelimiterProcessor p = it.next();
|
||||||
|
int pLen = p.getMinLength();
|
||||||
|
if (len > pLen) {
|
||||||
|
it.previous();
|
||||||
|
it.add(dp);
|
||||||
|
added = true;
|
||||||
|
break;
|
||||||
|
} else if (len == pLen) {
|
||||||
|
throw new IllegalArgumentException("Cannot add two delimiter processors for char '" + delim + "' and minimum length " + len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!added) {
|
||||||
|
processors.add(dp);
|
||||||
|
this.minLength = len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DelimiterProcessor findProcessor(int len) {
|
||||||
|
for (DelimiterProcessor p : processors) {
|
||||||
|
if (p.getMinLength() <= len) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processors.getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
|
||||||
|
return findProcessor(opener.length()).getDelimiterUse(opener, closer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void process(Text opener, Text closer, int delimiterUse) {
|
||||||
|
findProcessor(delimiterUse).process(opener, closer, delimiterUse);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package io.noties.markwon.inlineparser;
|
||||||
|
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
import org.commonmark.renderer.html.HtmlRenderer;
|
||||||
|
import org.commonmark.testutil.SpecTestCase;
|
||||||
|
import org.commonmark.testutil.example.Example;
|
||||||
|
|
||||||
|
public class InlineParserSpecTest extends SpecTestCase {
|
||||||
|
|
||||||
|
private static final Parser PARSER = Parser.builder()
|
||||||
|
.inlineParserFactory(MarkwonInlineParser.factoryBuilder().includeDefaults().build())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// The spec says URL-escaping is optional, but the examples assume that it's enabled.
|
||||||
|
private static final HtmlRenderer RENDERER = HtmlRenderer.builder().percentEncodeUrls(true).build();
|
||||||
|
|
||||||
|
public InlineParserSpecTest(Example example) {
|
||||||
|
super(example);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String render(String source) {
|
||||||
|
return RENDERER.render(PARSER.parse(source));
|
||||||
|
}
|
||||||
|
}
|
@ -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')
|
||||||
|
@ -33,6 +33,8 @@
|
|||||||
android:name=".editor.EditorActivity"
|
android:name=".editor.EditorActivity"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
|
<activity android:name=".inlineparser.InlineParserActivity" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
@ -24,6 +24,7 @@ import io.noties.markwon.sample.customextension.CustomExtensionActivity;
|
|||||||
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
||||||
import io.noties.markwon.sample.editor.EditorActivity;
|
import io.noties.markwon.sample.editor.EditorActivity;
|
||||||
import io.noties.markwon.sample.html.HtmlActivity;
|
import io.noties.markwon.sample.html.HtmlActivity;
|
||||||
|
import io.noties.markwon.sample.inlineparser.InlineParserActivity;
|
||||||
import io.noties.markwon.sample.latex.LatexActivity;
|
import io.noties.markwon.sample.latex.LatexActivity;
|
||||||
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
||||||
import io.noties.markwon.sample.recycler.RecyclerActivity;
|
import io.noties.markwon.sample.recycler.RecyclerActivity;
|
||||||
@ -122,6 +123,10 @@ public class MainActivity extends Activity {
|
|||||||
activity = EditorActivity.class;
|
activity = EditorActivity.class;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case INLINE_PARSER:
|
||||||
|
activity = InlineParserActivity.class;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,9 @@ public enum Sample {
|
|||||||
|
|
||||||
PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
|
PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
|
||||||
|
|
||||||
EDITOR(R.string.sample_editor);
|
EDITOR(R.string.sample_editor),
|
||||||
|
|
||||||
|
INLINE_PARSER(R.string.sample_inline_parser);
|
||||||
|
|
||||||
private final int textResId;
|
private final int textResId;
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.commonmark.parser.InlineParserFactory;
|
||||||
import org.commonmark.parser.Parser;
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -36,6 +37,10 @@ import io.noties.markwon.editor.PersistedSpans;
|
|||||||
import io.noties.markwon.editor.handler.EmphasisEditHandler;
|
import io.noties.markwon.editor.handler.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;
|
||||||
|
|
||||||
@ -102,7 +107,7 @@ public class EditorActivity extends Activity {
|
|||||||
private void additional_edit_span() {
|
private void additional_edit_span() {
|
||||||
// An additional span is used to highlight strong-emphasis
|
// An additional span is used to highlight strong-emphasis
|
||||||
|
|
||||||
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
||||||
.useEditHandler(new AbstractEditHandler<StrongEmphasisSpan>() {
|
.useEditHandler(new AbstractEditHandler<StrongEmphasisSpan>() {
|
||||||
@Override
|
@Override
|
||||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||||
@ -171,14 +176,27 @@ final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
|||||||
// for links to be clickable
|
// for links to be clickable
|
||||||
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
|
||||||
|
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||||
|
.includeDefaults()
|
||||||
|
// no inline images will be parsed
|
||||||
|
.excludeInlineProcessor(BangInlineProcessor.class)
|
||||||
|
// no html tags will be parsed
|
||||||
|
.excludeInlineProcessor(HtmlInlineProcessor.class)
|
||||||
|
// no entities will be parsed (aka `&` etc)
|
||||||
|
.excludeInlineProcessor(EntityInlineProcessor.class)
|
||||||
|
.build();
|
||||||
|
|
||||||
final Markwon markwon = Markwon.builder(this)
|
final Markwon markwon = Markwon.builder(this)
|
||||||
.usePlugin(StrikethroughPlugin.create())
|
.usePlugin(StrikethroughPlugin.create())
|
||||||
.usePlugin(LinkifyPlugin.create())
|
.usePlugin(LinkifyPlugin.create())
|
||||||
.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(inlineParserFactory);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
package io.noties.markwon.sample.inlineparser;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.commonmark.node.Block;
|
||||||
|
import org.commonmark.node.BlockQuote;
|
||||||
|
import org.commonmark.node.Heading;
|
||||||
|
import org.commonmark.node.HtmlBlock;
|
||||||
|
import org.commonmark.node.ListBlock;
|
||||||
|
import org.commonmark.node.ThematicBreak;
|
||||||
|
import org.commonmark.parser.InlineParserFactory;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.inlineparser.BackticksInlineProcessor;
|
||||||
|
import io.noties.markwon.inlineparser.CloseBracketInlineProcessor;
|
||||||
|
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||||
|
import io.noties.markwon.inlineparser.OpenBracketInlineProcessor;
|
||||||
|
import io.noties.markwon.sample.R;
|
||||||
|
|
||||||
|
public class InlineParserActivity extends Activity {
|
||||||
|
|
||||||
|
private TextView textView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_text_view);
|
||||||
|
|
||||||
|
this.textView = findViewById(R.id.text_view);
|
||||||
|
|
||||||
|
// links_only();
|
||||||
|
|
||||||
|
disable_code();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void links_only() {
|
||||||
|
|
||||||
|
// create an inline-parser-factory that will _ONLY_ parse links
|
||||||
|
// this would mean:
|
||||||
|
// * no emphasises (strong and regular aka bold and italics),
|
||||||
|
// * no images,
|
||||||
|
// * no code,
|
||||||
|
// * no HTML entities (&)
|
||||||
|
// * no HTML tags
|
||||||
|
// markdown blocks are still parsed
|
||||||
|
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||||
|
.referencesEnabled(true)
|
||||||
|
.addInlineProcessor(new OpenBracketInlineProcessor())
|
||||||
|
.addInlineProcessor(new CloseBracketInlineProcessor())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
|
builder.inlineParserFactory(inlineParserFactory);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// note that image is considered a link now
|
||||||
|
final String md = "**bold_bold-italic_** <u>html-u</u>, [link](#)  `code`";
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disable_code() {
|
||||||
|
// parses all as usual, but ignores code (inline and block)
|
||||||
|
|
||||||
|
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||||
|
.includeDefaults()
|
||||||
|
.excludeInlineProcessor(BackticksInlineProcessor.class)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// unfortunately there is no _exclude_ method for parser-builder
|
||||||
|
final Set<Class<? extends Block>> enabledBlocks = new HashSet<Class<? extends Block>>() {{
|
||||||
|
// IndentedCodeBlock.class and FencedCodeBlock.class are missing
|
||||||
|
// this is full list (including above) that can be passed to `enabledBlockTypes` method
|
||||||
|
addAll(Arrays.asList(
|
||||||
|
BlockQuote.class,
|
||||||
|
Heading.class,
|
||||||
|
HtmlBlock.class,
|
||||||
|
ThematicBreak.class,
|
||||||
|
ListBlock.class));
|
||||||
|
}};
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(new AbstractMarkwonPlugin() {
|
||||||
|
@Override
|
||||||
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
|
builder
|
||||||
|
.inlineParserFactory(inlineParserFactory)
|
||||||
|
.enabledBlockTypes(enabledBlocks);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final String md = "# Head!\n\n" +
|
||||||
|
"* one\n" +
|
||||||
|
"+ two\n\n" +
|
||||||
|
"and **bold** to `you`!\n\n" +
|
||||||
|
"> a quote _em_\n\n" +
|
||||||
|
"```java\n" +
|
||||||
|
"final int i = 0;\n" +
|
||||||
|
"```\n\n" +
|
||||||
|
"**Good day!**";
|
||||||
|
markwon.setMarkdown(textView, md);
|
||||||
|
}
|
||||||
|
}
|
@ -27,4 +27,6 @@
|
|||||||
|
|
||||||
<string name="sample_editor"># \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText</string>
|
<string name="sample_editor"># \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText</string>
|
||||||
|
|
||||||
|
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -11,6 +11,7 @@ include ':app', ':sample',
|
|||||||
':markwon-image-coil',
|
':markwon-image-coil',
|
||||||
':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