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