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`)
|
||||
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])
|
||||
<br>Thanks to [@tylerbwong]
|
||||
* `MarkwonInlineParser` to customize inline parsing (new module: `markwon-inline-parser`)
|
||||
* `Markwon#configuration` method to expose `MarkwonConfiguration` via public API
|
||||
* `HeadingSpan#getLevel` getter
|
||||
* Add `SvgPictureMediaDecoder` in `image` module to deal with SVG without dimensions ([#165])
|
||||
|
@ -86,7 +86,8 @@ ext {
|
||||
'robolectric' : 'org.robolectric:robolectric:3.8',
|
||||
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
|
||||
'commons-io' : 'commons-io:commons-io:2.6',
|
||||
'mockito' : 'org.mockito:mockito-core:2.21.0'
|
||||
'mockito' : 'org.mockito:mockito-core:2.21.0',
|
||||
'commonmark-test-util': "com.atlassian.commonmark:commonmark-test-util:$commonMarkVersion",
|
||||
]
|
||||
|
||||
registerArtifact = this.®isterArtifact
|
||||
|
@ -1,4 +1,4 @@
|
||||
|
||||
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
|
||||
const artifacts = [{"id":"core","name":"Core","group":"io.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"editor","name":"Editor","group":"io.noties.markwon","description":"Markdown editor based on Markwon"},{"id":"ext-latex","name":"LaTeX","group":"io.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"io.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"io.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"io.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"io.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image","name":"Image","group":"io.noties.markwon","description":"Markwon image loading module (with optional GIF and SVG support)"},{"id":"image-glide","name":"Image Glide","group":"io.noties.markwon","description":"Markwon image loading module (based on Glide library)"},{"id":"image-picasso","name":"Image Picasso","group":"io.noties.markwon","description":"Markwon image loading module (based on Picasso library)"},{"id":"linkify","name":"Linkify","group":"io.noties.markwon","description":"Markwon plugin to linkify text (based on Android Linkify)"},{"id":"recycler","name":"Recycler","group":"io.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"io.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"simple-ext","name":"Simple Extension","group":"io.noties.markwon","description":"Custom extension based on simple delimiter usage"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"io.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
|
||||
const artifacts = [{"id":"core","name":"Core","group":"io.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"editor","name":"Editor","group":"io.noties.markwon","description":"Markdown editor based on Markwon"},{"id":"ext-latex","name":"LaTeX","group":"io.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"io.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"io.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"io.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"io.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image","name":"Image","group":"io.noties.markwon","description":"Markwon image loading module (with optional GIF and SVG support)"},{"id":"image-coil","name":"Image Coil","group":"io.noties.markwon","description":"Markwon image loading module (based on Coil library)"},{"id":"image-glide","name":"Image Glide","group":"io.noties.markwon","description":"Markwon image loading module (based on Glide library)"},{"id":"image-picasso","name":"Image Picasso","group":"io.noties.markwon","description":"Markwon image loading module (based on Picasso library)"},{"id":"inline-parser","name":"Inline Parser","group":"io.noties.markwon","description":"Markwon customizable commonmark-java InlineParser"},{"id":"linkify","name":"Linkify","group":"io.noties.markwon","description":"Markwon plugin to linkify text (based on Android Linkify)"},{"id":"recycler","name":"Recycler","group":"io.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"io.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"simple-ext","name":"Simple Extension","group":"io.noties.markwon","description":"Custom extension based on simple delimiter usage"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"io.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
|
||||
export { artifacts };
|
||||
|
@ -105,6 +105,7 @@ module.exports = {
|
||||
'/docs/v4/image-coil/',
|
||||
'/docs/v4/image-glide/',
|
||||
'/docs/v4/image-picasso/',
|
||||
'/docs/v4/inline-parser/',
|
||||
'/docs/v4/linkify/',
|
||||
'/docs/v4/recycler/',
|
||||
'/docs/v4/recycler-table/',
|
||||
|
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-html')
|
||||
implementation project(':markwon-image')
|
||||
implementation project(':markwon-inline-parser')
|
||||
implementation project(':markwon-linkify')
|
||||
implementation project(':markwon-recycler')
|
||||
implementation project(':markwon-recycler-table')
|
||||
|
@ -33,6 +33,8 @@
|
||||
android:name=".editor.EditorActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity android:name=".inlineparser.InlineParserActivity" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -24,6 +24,7 @@ import io.noties.markwon.sample.customextension.CustomExtensionActivity;
|
||||
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
||||
import io.noties.markwon.sample.editor.EditorActivity;
|
||||
import io.noties.markwon.sample.html.HtmlActivity;
|
||||
import io.noties.markwon.sample.inlineparser.InlineParserActivity;
|
||||
import io.noties.markwon.sample.latex.LatexActivity;
|
||||
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
||||
import io.noties.markwon.sample.recycler.RecyclerActivity;
|
||||
@ -122,6 +123,10 @@ public class MainActivity extends Activity {
|
||||
activity = EditorActivity.class;
|
||||
break;
|
||||
|
||||
case INLINE_PARSER:
|
||||
activity = InlineParserActivity.class;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
||||
}
|
||||
|
@ -23,7 +23,9 @@ public enum Sample {
|
||||
|
||||
PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
|
||||
|
||||
EDITOR(R.string.sample_editor);
|
||||
EDITOR(R.string.sample_editor),
|
||||
|
||||
INLINE_PARSER(R.string.sample_inline_parser);
|
||||
|
||||
private final int textResId;
|
||||
|
||||
|
@ -18,6 +18,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.commonmark.parser.InlineParserFactory;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -36,6 +37,10 @@ import io.noties.markwon.editor.PersistedSpans;
|
||||
import io.noties.markwon.editor.handler.EmphasisEditHandler;
|
||||
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.inlineparser.BangInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.EntityInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||
import io.noties.markwon.sample.R;
|
||||
|
||||
@ -171,14 +176,27 @@ final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this))
|
||||
// for links to be clickable
|
||||
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder()
|
||||
.includeDefaults()
|
||||
// no inline images will be parsed
|
||||
.excludeInlineProcessor(BangInlineProcessor.class)
|
||||
// no html tags will be parsed
|
||||
.excludeInlineProcessor(HtmlInlineProcessor.class)
|
||||
// no entities will be parsed (aka `&` etc)
|
||||
.excludeInlineProcessor(EntityInlineProcessor.class)
|
||||
.build();
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(LinkifyPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
|
||||
// disable all commonmark-java blocks, only inlines will be parsed
|
||||
// builder.enabledBlockTypes(Collections.emptySet());
|
||||
|
||||
builder.inlineParserFactory(inlineParserFactory);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
@ -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_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
|
||||
|
||||
</resources>
|
@ -11,6 +11,7 @@ include ':app', ':sample',
|
||||
':markwon-image-coil',
|
||||
':markwon-image-glide',
|
||||
':markwon-image-picasso',
|
||||
':markwon-inline-parser',
|
||||
':markwon-linkify',
|
||||
':markwon-recycler',
|
||||
':markwon-recycler-table',
|
||||
|
Loading…
x
Reference in New Issue
Block a user