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]) | ||||
|  | ||||
							
								
								
									
										11
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								build.gradle
									
									
									
									
									
								
							| @ -82,11 +82,12 @@ ext { | ||||
|     ] | ||||
| 
 | ||||
|     deps['test'] = [ | ||||
|             'junit'      : 'junit:junit:4.12', | ||||
|             'robolectric': 'org.robolectric:robolectric:3.8', | ||||
|             'ix-java'    : 'com.github.akarnokd:ixjava:1.0.0', | ||||
|             'commons-io' : 'commons-io:commons-io:2.6', | ||||
|             'mockito'    : 'org.mockito:mockito-core:2.21.0' | ||||
|             'junit'               : 'junit:junit:4.12', | ||||
|             'robolectric'         : 'org.robolectric:robolectric:3.8', | ||||
|             'ix-java'             : 'com.github.akarnokd:ixjava:1.0.0', | ||||
|             'commons-io'          : 'commons-io:commons-io:2.6', | ||||
|             'mockito'             : 'org.mockito:mockito-core:2.21.0', | ||||
|             'commonmark-test-util': "com.atlassian.commonmark:commonmark-test-util:$commonMarkVersion", | ||||
|     ] | ||||
| 
 | ||||
|     registerArtifact = this.®isterArtifact | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
| @ -102,52 +107,52 @@ public class EditorActivity extends Activity { | ||||
|     private void additional_edit_span() { | ||||
|         // An additional span is used to highlight strong-emphasis | ||||
| 
 | ||||
| final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this)) | ||||
|         .useEditHandler(new AbstractEditHandler<StrongEmphasisSpan>() { | ||||
|             @Override | ||||
|             public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||
|                 // Here we define which span is _persisted_ in EditText, it is not removed | ||||
|                 //  from EditText between text changes, but instead - reused (by changing | ||||
|                 //  position). Consider it as a cache for spans. We could use `StrongEmphasisSpan` | ||||
|                 //  here also, but I chose Bold to indicate that this span is not the same | ||||
|                 //  as in off-screen rendered markdown | ||||
|                 builder.persistSpan(Bold.class, Bold::new); | ||||
|             } | ||||
|         final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this)) | ||||
|                 .useEditHandler(new AbstractEditHandler<StrongEmphasisSpan>() { | ||||
|                     @Override | ||||
|                     public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) { | ||||
|                         // Here we define which span is _persisted_ in EditText, it is not removed | ||||
|                         //  from EditText between text changes, but instead - reused (by changing | ||||
|                         //  position). Consider it as a cache for spans. We could use `StrongEmphasisSpan` | ||||
|                         //  here also, but I chose Bold to indicate that this span is not the same | ||||
|                         //  as in off-screen rendered markdown | ||||
|                         builder.persistSpan(Bold.class, Bold::new); | ||||
|                     } | ||||
| 
 | ||||
|             @Override | ||||
|             public void handleMarkdownSpan( | ||||
|                     @NonNull PersistedSpans persistedSpans, | ||||
|                     @NonNull Editable editable, | ||||
|                     @NonNull String input, | ||||
|                     @NonNull StrongEmphasisSpan span, | ||||
|                     int spanStart, | ||||
|                     int spanTextLength) { | ||||
|                 // Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4) | ||||
|                 //  because multiple inline markdown nodes can refer to the same text. | ||||
|                 //  For example, `**_~~hey~~_**` - we will receive `**_~~` in this method, | ||||
|                 //  and thus will have to manually find actual position in raw user input | ||||
|                 final MarkwonEditorUtils.Match match = | ||||
|                         MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__"); | ||||
|                 if (match != null) { | ||||
|                     editable.setSpan( | ||||
|                             // we handle StrongEmphasisSpan and represent it with Bold in EditText | ||||
|                             //  we still could use StrongEmphasisSpan, but it must be accessed | ||||
|                             //  via persistedSpans | ||||
|                             persistedSpans.get(Bold.class), | ||||
|                             match.start(), | ||||
|                             match.end(), | ||||
|                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|                     @Override | ||||
|                     public void handleMarkdownSpan( | ||||
|                             @NonNull PersistedSpans persistedSpans, | ||||
|                             @NonNull Editable editable, | ||||
|                             @NonNull String input, | ||||
|                             @NonNull StrongEmphasisSpan span, | ||||
|                             int spanStart, | ||||
|                             int spanTextLength) { | ||||
|                         // Unfortunately we cannot hardcode delimiters length here (aka spanTextLength + 4) | ||||
|                         //  because multiple inline markdown nodes can refer to the same text. | ||||
|                         //  For example, `**_~~hey~~_**` - we will receive `**_~~` in this method, | ||||
|                         //  and thus will have to manually find actual position in raw user input | ||||
|                         final MarkwonEditorUtils.Match match = | ||||
|                                 MarkwonEditorUtils.findDelimited(input, spanStart, "**", "__"); | ||||
|                         if (match != null) { | ||||
|                             editable.setSpan( | ||||
|                                     // we handle StrongEmphasisSpan and represent it with Bold in EditText | ||||
|                                     //  we still could use StrongEmphasisSpan, but it must be accessed | ||||
|                                     //  via persistedSpans | ||||
|                                     persistedSpans.get(Bold.class), | ||||
|                                     match.start(), | ||||
|                                     match.end(), | ||||
|                                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ||||
|                             ); | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|             @NonNull | ||||
|             @Override | ||||
|             public Class<StrongEmphasisSpan> markdownSpanType() { | ||||
|                 return StrongEmphasisSpan.class; | ||||
|             } | ||||
|         }) | ||||
|         .build(); | ||||
|                     @NonNull | ||||
|                     @Override | ||||
|                     public Class<StrongEmphasisSpan> markdownSpanType() { | ||||
|                         return StrongEmphasisSpan.class; | ||||
|                     } | ||||
|                 }) | ||||
|                 .build(); | ||||
| 
 | ||||
|         editText.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(editor)); | ||||
|     } | ||||
| @ -171,14 +176,27 @@ final MarkwonEditor editor = MarkwonEditor.builder(Markwon.create(this)) | ||||
|         // for links to be clickable | ||||
|         editText.setMovementMethod(LinkMovementMethod.getInstance()); | ||||
| 
 | ||||
|         final InlineParserFactory inlineParserFactory = MarkwonInlineParser.factoryBuilder() | ||||
|                 .includeDefaults() | ||||
|                 // no inline images will be parsed | ||||
|                 .excludeInlineProcessor(BangInlineProcessor.class) | ||||
|                 // no html tags will be parsed | ||||
|                 .excludeInlineProcessor(HtmlInlineProcessor.class) | ||||
|                 // no entities will be parsed (aka `&` etc) | ||||
|                 .excludeInlineProcessor(EntityInlineProcessor.class) | ||||
|                 .build(); | ||||
| 
 | ||||
|         final Markwon markwon = Markwon.builder(this) | ||||
|                 .usePlugin(StrikethroughPlugin.create()) | ||||
|                 .usePlugin(LinkifyPlugin.create()) | ||||
|                 .usePlugin(new AbstractMarkwonPlugin() { | ||||
|                     @Override | ||||
|                     public void configureParser(@NonNull Parser.Builder builder) { | ||||
| 
 | ||||
|                         // disable all commonmark-java blocks, only inlines will be parsed | ||||
| //                        builder.enabledBlockTypes(Collections.emptySet()); | ||||
| 
 | ||||
|                         builder.inlineParserFactory(inlineParserFactory); | ||||
|                     } | ||||
|                 }) | ||||
|                 .build(); | ||||
|  | ||||
| @ -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
	 Dimitry Ivanov
						Dimitry Ivanov