2019-11-13 13:38:59 +03:00

914 lines
30 KiB
Java

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.util.Escaping;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
import org.commonmark.node.Text;
import org.commonmark.parser.InlineParser;
import org.commonmark.parser.InlineParserContext;
import org.commonmark.parser.InlineParserFactory;
import org.commonmark.parser.delimiter.DelimiterProcessor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static io.noties.markwon.inlineparser.InlineParserUtils.mergeChildTextNodes;
import static io.noties.markwon.inlineparser.InlineParserUtils.mergeTextNodesBetweenExclusive;
/**
* @see #factoryBuilder()
* @see FactoryBuilder
* @since 4.2.0-SNAPSHOT
*/
public class MarkwonInlineParser implements InlineParser, ReferenceParser, MarkwonInlineParserContext {
public interface FactoryBuilder {
/**
* @see InlineProcessor
*/
@NonNull
FactoryBuilder addInlineProcessor(@NonNull InlineProcessor processor);
/**
* @see AsteriskDelimiterProcessor
* @see UnderscoreDelimiterProcessor
*/
@NonNull
FactoryBuilder addDelimiterProcessor(@NonNull DelimiterProcessor processor);
/**
* Indicate if markdown references are enabled. {@code referencesEnabled=true} if {@link #includeDefaults()}
* was called
*/
@NonNull
FactoryBuilder referencesEnabled(boolean referencesEnabled);
/**
* Includes all default delimiter and inline processors, and sets {@code referencesEnabled=true}.
* Useful with subsequent calls to {@link #excludeInlineProcessor(Class)} or {@link #excludeDelimiterProcessor(Class)}
*/
@NonNull
FactoryBuilder includeDefaults();
@NonNull
FactoryBuilder excludeInlineProcessor(@NonNull Class<? extends InlineProcessor> processor);
@NonNull
FactoryBuilder excludeDelimiterProcessor(@NonNull Class<? extends DelimiterProcessor> processor);
@NonNull
InlineParserFactory build();
}
@NonNull
public static FactoryBuilder factoryBuilder() {
return new FactoryBuilderImpl();
}
private static final String ESCAPED_CHAR = "\\\\" + Escaping.ESCAPABLE;
private static final String ASCII_PUNCTUATION = "!\"#\\$%&'\\(\\)\\*\\+,\\-\\./:;<=>\\?@\\[\\\\\\]\\^_`\\{\\|\\}~";
private static final Pattern PUNCTUATION = Pattern
.compile("^[" + ASCII_PUNCTUATION + "\\p{Pc}\\p{Pd}\\p{Pe}\\p{Pf}\\p{Pi}\\p{Po}\\p{Ps}]");
private static final Pattern LINK_TITLE = Pattern.compile(
"^(?:\"(" + ESCAPED_CHAR + "|[^\"\\x00])*\"" +
'|' +
"'(" + ESCAPED_CHAR + "|[^'\\x00])*'" +
'|' +
"\\((" + ESCAPED_CHAR + "|[^)\\x00])*\\))");
private static final Pattern LINK_DESTINATION_BRACES = Pattern.compile("^(?:[<](?:[^<> \\t\\n\\\\]|\\\\.)*[>])");
private static final Pattern LINK_LABEL = Pattern.compile("^\\[(?:[^\\\\\\[\\]]|\\\\.)*\\]");
private static final Pattern SPNL = Pattern.compile("^ *(?:\n *)?");
private static final Pattern UNICODE_WHITESPACE_CHAR = Pattern.compile("^[\\p{Zs}\t\r\n\f]");
private static final Pattern LINE_END = Pattern.compile("^ *(?:\n|$)");
static final Pattern ESCAPABLE = Pattern.compile('^' + Escaping.ESCAPABLE);
static final Pattern WHITESPACE = Pattern.compile("\\s+");
private final boolean referencesEnabled;
private final BitSet specialCharacters;
private final Map<Character, List<InlineProcessor>> inlineProcessors;
private final Map<Character, DelimiterProcessor> delimiterProcessors;
private Node block;
private String input;
private int index;
/**
* Link references by ID, needs to be built up using parseReference before calling parse.
*/
private Map<String, Link> referenceMap = new HashMap<>(1);
/**
* Top delimiter (emphasis, strong emphasis or custom emphasis). (Brackets are on a separate stack, different
* from the algorithm described in the spec.)
*/
private Delimiter lastDelimiter;
/**
* Top opening bracket (<code>[</code> or <code>![)</code>).
*/
private Bracket lastBracket;
// 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.specialCharacters = calculateSpecialCharacters(
this.inlineProcessors.keySet(),
this.delimiterProcessors.keySet());
}
@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;
}
private static Map<Character, DelimiterProcessor> calculateDelimiterProcessors(List<DelimiterProcessor> delimiterProcessors) {
Map<Character, DelimiterProcessor> map = new HashMap<>();
addDelimiterProcessors(delimiterProcessors, map);
return map;
}
private static void addDelimiterProcessors(Iterable<DelimiterProcessor> delimiterProcessors, Map<Character, DelimiterProcessor> map) {
for (DelimiterProcessor delimiterProcessor : delimiterProcessors) {
char opening = delimiterProcessor.getOpeningCharacter();
char closing = delimiterProcessor.getClosingCharacter();
if (opening == closing) {
DelimiterProcessor old = map.get(opening);
if (old != null && old.getOpeningCharacter() == old.getClosingCharacter()) {
StaggeredDelimiterProcessor s;
if (old instanceof StaggeredDelimiterProcessor) {
s = (StaggeredDelimiterProcessor) old;
} else {
s = new StaggeredDelimiterProcessor(opening);
s.add(old);
}
s.add(delimiterProcessor);
map.put(opening, s);
} else {
addDelimiterProcessorForChar(opening, delimiterProcessor, map);
}
} else {
addDelimiterProcessorForChar(opening, delimiterProcessor, map);
addDelimiterProcessorForChar(closing, delimiterProcessor, map);
}
}
}
private static void addDelimiterProcessorForChar(char delimiterChar, DelimiterProcessor toAdd, Map<Character, DelimiterProcessor> delimiterProcessors) {
DelimiterProcessor existing = delimiterProcessors.put(delimiterChar, toAdd);
if (existing != null) {
throw new IllegalArgumentException("Delimiter processor conflict with delimiter char '" + delimiterChar + "'");
}
}
/**
* Parse content in block into inline children, using reference map to resolve references.
*/
@Override
public void parse(String content, Node block) {
this.block = block;
this.input = content.trim();
this.index = 0;
this.lastDelimiter = null;
this.lastBracket = null;
boolean moreToParse;
do {
moreToParse = parseInline();
} while (moreToParse);
processDelimiters(null);
mergeChildTextNodes(block);
}
/**
* Attempt to parse a link reference, modifying the internal reference map.
*/
@Override
public int parseReference(String s) {
if (!referencesEnabled) {
return 0;
}
this.input = s;
this.index = 0;
String dest;
String title;
int matchChars;
int startIndex = index;
// label:
matchChars = parseLinkLabel();
if (matchChars == 0) {
return 0;
}
String rawLabel = input.substring(0, matchChars);
// colon:
if (peek() != ':') {
return 0;
}
index++;
// link url
spnl();
dest = parseLinkDestination();
if (dest == null || dest.length() == 0) {
return 0;
}
int beforeTitle = index;
spnl();
title = parseLinkTitle();
if (title == null) {
// rewind before spaces
index = beforeTitle;
}
boolean atLineEnd = true;
if (index != input.length() && match(LINE_END) == null) {
if (title == null) {
atLineEnd = false;
} else {
// the potential title we found is not at the line end,
// but it could still be a legal link reference if we
// discard the title
title = null;
// rewind before spaces
index = beforeTitle;
// and instead check if the link URL is at the line end
atLineEnd = match(LINE_END) != null;
}
}
if (!atLineEnd) {
return 0;
}
String normalizedLabel = Escaping.normalizeReference(rawLabel);
if (normalizedLabel.isEmpty()) {
return 0;
}
if (!referenceMap.containsKey(normalizedLabel)) {
Link link = new Link(dest, title);
referenceMap.put(normalizedLabel, link);
}
return index - startIndex;
}
@Override
@NonNull
public Text appendText(@NonNull CharSequence text, int beginIndex, int endIndex) {
return appendText(text.subSequence(beginIndex, endIndex));
}
@Override
@NonNull
public Text appendText(@NonNull CharSequence text) {
Text node = new Text(text.toString());
appendNode(node);
return node;
}
@Override
public void appendNode(@NonNull Node node) {
block.appendChild(node);
}
/**
* Parse the next inline element in subject, advancing input index.
* On success, add the result to block's children and return true.
* On failure, return false.
*/
private boolean parseInline() {
final char c = peek();
if (c == '\0') {
return false;
}
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;
}
}
} else {
final DelimiterProcessor delimiterProcessor = delimiterProcessors.get(c);
if (delimiterProcessor != null) {
res = parseDelimiters(delimiterProcessor, c);
} else {
res = parseString();
}
}
if (!res) {
index++;
// When we get here, it's only for a single special character that turned out to not have a special meaning.
// So we shouldn't have a single surrogate here, hence it should be ok to turn it into a String.
String literal = String.valueOf(c);
appendText(literal);
}
return true;
}
/**
* If RE matches at current index in the input, advance index and return the match; otherwise return null.
*/
@Override
@Nullable
public String match(@NonNull 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;
}
}
/**
* Returns the char at the current input index, or {@code '\0'} in case there are no more characters.
*/
@Override
public char peek() {
if (index < input.length()) {
return input.charAt(index);
} else {
return '\0';
}
}
@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.
*/
@Override
public boolean spnl() {
match(SPNL);
return true;
}
/**
* Attempt to parse delimiters like emphasis, strong emphasis or custom delimiters.
*/
private boolean parseDelimiters(DelimiterProcessor delimiterProcessor, char delimiterChar) {
DelimiterData res = scanDelimiters(delimiterProcessor, delimiterChar);
if (res == null) {
return false;
}
int length = res.count;
int startIndex = index;
index += length;
Text node = appendText(input, startIndex, index);
// Add entry to stack for this opener
lastDelimiter = new Delimiter(node, delimiterChar, res.canOpen, res.canClose, lastDelimiter);
lastDelimiter.length = length;
lastDelimiter.originalLength = length;
if (lastDelimiter.previous != null) {
lastDelimiter.previous.next = lastDelimiter;
}
return true;
}
/**
* Attempt to parse link destination, returning the string or null if no match.
*/
@Override
@Nullable
public 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));
}
}
private 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.
*/
@Override
@Nullable
public 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.
*/
@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) {
return 0;
} else {
return m.length();
}
}
/**
* Parse a run of ordinary characters, or a single character with a special meaning in markdown, as a plain string.
*/
private boolean parseString() {
int begin = index;
int length = input.length();
while (index != length) {
if (specialCharacters.get(input.charAt(index))) {
break;
}
index++;
}
if (begin != index) {
appendText(input, begin, index);
return true;
} else {
return false;
}
}
/**
* Scan a sequence of characters with code delimiterChar, and return information about the number of delimiters
* and whether they are positioned such that they can open and/or close emphasis or strong emphasis.
*
* @return information about delimiter run, or {@code null}
*/
private DelimiterData scanDelimiters(DelimiterProcessor delimiterProcessor, char delimiterChar) {
int startIndex = index;
int delimiterCount = 0;
while (peek() == delimiterChar) {
delimiterCount++;
index++;
}
if (delimiterCount < delimiterProcessor.getMinLength()) {
index = startIndex;
return null;
}
String before = startIndex == 0 ? "\n" :
input.substring(startIndex - 1, startIndex);
char charAfter = peek();
String after = charAfter == '\0' ? "\n" :
String.valueOf(charAfter);
// We could be more lazy here, in most cases we don't need to do every match case.
boolean beforeIsPunctuation = PUNCTUATION.matcher(before).matches();
boolean beforeIsWhitespace = UNICODE_WHITESPACE_CHAR.matcher(before).matches();
boolean afterIsPunctuation = PUNCTUATION.matcher(after).matches();
boolean afterIsWhitespace = UNICODE_WHITESPACE_CHAR.matcher(after).matches();
boolean leftFlanking = !afterIsWhitespace &&
(!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation);
boolean rightFlanking = !beforeIsWhitespace &&
(!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation);
boolean canOpen;
boolean canClose;
if (delimiterChar == '_') {
canOpen = leftFlanking && (!rightFlanking || beforeIsPunctuation);
canClose = rightFlanking && (!leftFlanking || afterIsPunctuation);
} else {
canOpen = leftFlanking && delimiterChar == delimiterProcessor.getOpeningCharacter();
canClose = rightFlanking && delimiterChar == delimiterProcessor.getClosingCharacter();
}
index = startIndex;
return new DelimiterData(delimiterCount, canOpen, canClose);
}
@Override
public 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
while (lastDelimiter != null && lastDelimiter != stackBottom) {
removeDelimiterKeepNode(lastDelimiter);
}
}
private 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*`.
*/
private 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`.
*/
private void removeDelimiterKeepNode(Delimiter delim) {
removeDelimiter(delim);
}
private void removeDelimiter(Delimiter delim) {
if (delim.previous != null) {
delim.previous.next = delim.next;
}
if (delim.next == null) {
// top of stack
lastDelimiter = delim.previous;
} else {
delim.next.previous = delim.previous;
}
}
private static class DelimiterData {
final int count;
final boolean canClose;
final boolean canOpen;
DelimiterData(int count, boolean canOpen, boolean canClose) {
this.count = count;
this.canOpen = canOpen;
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);
}
}
}