diff --git a/commonmark/pom.xml b/commonmark/pom.xml
index 03fcc85ff..834fc8626 100644
--- a/commonmark/pom.xml
+++ b/commonmark/pom.xml
@@ -27,6 +27,11 @@
jmh-generator-annprocess
test
+
+ org.mockito
+ mockito-core
+ test
+
diff --git a/commonmark/src/main/java/org/commonmark/experimental/InlineParserImpl.java b/commonmark/src/main/java/org/commonmark/experimental/InlineParserImpl.java
new file mode 100644
index 000000000..155f3698e
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/InlineParserImpl.java
@@ -0,0 +1,263 @@
+package org.commonmark.experimental;
+
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
+import org.commonmark.parser.InlineParser;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.commonmark.experimental.TextIdentifier.INVALID_INDEX;
+
+public class InlineParserImpl implements InlineParser {
+ private final Builder.NodeSetupSingleInstance nodeSetupSingleInstance;
+ private final Map nodeSetupsByCharacterTrigger;
+ private final Set nodeSetupActives = new HashSet<>();
+
+ private InlineParserImpl(Builder.NodeSetupSingleInstance nodeSetupSingleInstance,
+ Map nodeSetupsByCharacterTrigger) {
+ this.nodeSetupSingleInstance = nodeSetupSingleInstance;
+ this.nodeSetupsByCharacterTrigger = nodeSetupsByCharacterTrigger;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ public void parse(String input, Node block) {
+ nodeSetupActives.clear();
+
+ final int length = input.length();
+ final PreNode[] positionsReservedByNodePriority = new PreNode[length];
+ int index = 0;
+
+ while (index < length) {
+ final char character = input.charAt(index);
+
+ NodeGroupHandler startTrigger = nodeSetupsByCharacterTrigger.get(character);
+ if (startTrigger != null) {
+ nodeSetupActives.add(startTrigger);
+ }
+
+ for (NodeGroupHandler nodeSetupHandler : nodeSetupActives) {
+ nodeSetupHandler.check(input, character, index, positionsReservedByNodePriority);
+ }
+
+ index++;
+ }
+
+ processNodesInLine(input, positionsReservedByNodePriority, block);
+ }
+
+ private void processNodesInLine(String text, PreNode[] positionsReservedByPriority, Node block) {
+ int index = 0;
+ int lastIndexEmpty = 0;
+
+ while (index < text.length()) {
+ PreNode preNode = positionsReservedByPriority[index];
+
+ if (preNode != null) {
+ if (lastIndexEmpty != index) {
+ block.appendChild(nodeSetupSingleInstance.getNodeCreator()
+ .build(text.substring(lastIndexEmpty, index), null));
+ }
+
+ block.appendChild(preNode.getNodeCreator()
+ .build(text.substring(preNode.getStartIndex(), preNode.getEndIndex()),
+ preNode.getInternalBlocks()));
+
+ lastIndexEmpty = preNode.getEndIndex();
+ index = preNode.getEndIndex();
+ } else {
+ index++;
+ }
+ }
+
+ if (lastIndexEmpty != index) {
+ block.appendChild(nodeSetupSingleInstance.getNodeCreator()
+ .build(text.substring(lastIndexEmpty, index), null));
+ }
+ }
+
+ private static class DefaultLiteralNodeSetup implements NodeSetup {
+ @Override
+ public NodeCreator nodeCreator() {
+ return new NodeCreator() {
+ @Override
+ public Node build(String textFound, NodePatternIdentifier.InternalBlocks[] internalBlocks) {
+ return new Text(textFound.trim());
+ }
+ };
+ }
+ }
+
+ public static class Builder {
+ private final List nodeSetups = new ArrayList<>();
+ private NodeSetup literalNodeSetup = new DefaultLiteralNodeSetup();
+
+ public Builder nodeSetup(TextNodeIdentifierSetup... extensions) {
+ nodeSetups.addAll(Arrays.asList(extensions));
+ return this;
+ }
+
+ public Builder nodeSetup(List extensions) {
+ nodeSetups.addAll(extensions);
+ return this;
+ }
+
+ public InlineParserImpl build() {
+ return new InlineParserImpl(literalNodeSetupSingle(literalNodeSetup), bootParse(nodeSetups));
+ }
+
+ private NodeSetupSingleInstance literalNodeSetupSingle(NodeSetup literalNodeSetup) {
+ return new NodeSetupSingleInstance(literalNodeSetup.nodeCreator());
+ }
+
+ private Map bootParse(List nodeSetups) {
+ orderHighestPriorityFirst(nodeSetups);
+
+ Map nodeSetupsByStartCharacter = new HashMap<>(nodeSetups.size());
+
+ for (TextNodeIdentifierSetup nodeSetup : nodeSetups) {
+ TextIdentifier textIdentifier = nodeSetup.textIdentifier();
+
+ NodeBreakLinePattern preProcessLine = textIdentifier.getNodeBreakLinePattern();
+
+ NodeGroupHandler nodeGroupHandler = nodeSetupsByStartCharacter.get(preProcessLine.characterTrigger());
+ if (nodeGroupHandler == null) {
+ nodeGroupHandler = new NodeGroupHandler();
+ }
+
+ nodeGroupHandler.add(new NodeSetupSingleInstance(textIdentifier,
+ nodeSetup.nodeCreator(),
+ nodeSetup.priority()));
+
+ nodeSetupsByStartCharacter.put(preProcessLine.characterTrigger(), nodeGroupHandler);
+ }
+ return nodeSetupsByStartCharacter;
+ }
+
+ private void orderHighestPriorityFirst(List nodeSetups) {
+ Comparator nodeSetupComparator = new Comparator() {
+ @Override
+ public int compare(TextNodeIdentifierSetup o1, TextNodeIdentifierSetup o2) {
+ return Integer.compare(o1.priority(), o2.priority());
+ }
+ };
+ Collections.sort(nodeSetups, Collections.reverseOrder(nodeSetupComparator));
+ }
+
+ public Builder literalNodeSetup(NodeSetup literalNodeSetup) {
+ this.literalNodeSetup = literalNodeSetup;
+ return this;
+ }
+
+ private static class NodeSetupSingleInstance {
+ private final TextIdentifier textIdentifier;
+ private final int priority;
+ private final NodeCreator nodeCreator;
+
+ public NodeSetupSingleInstance(TextIdentifier textIdentifier, NodeCreator nodeCreator, int priority) {
+ this.textIdentifier = textIdentifier;
+ this.nodeCreator = nodeCreator;
+ this.priority = priority;
+ }
+
+ public NodeSetupSingleInstance(NodeCreator nodeCreator) {
+ this(null, nodeCreator, 0);
+ }
+
+ public TextIdentifier getTextIdentifier() {
+ return textIdentifier;
+ }
+
+ public int getPriority() {
+ return priority;
+ }
+
+ public NodeCreator getNodeCreator() {
+ return nodeCreator;
+ }
+ }
+ }
+
+ private static class NodeGroupHandler {
+ private final List nodeSetups = new ArrayList<>();
+ private final NodeIdentifier nodeIdentifier = new NodeIdentifier();
+ private int lastIndex = INVALID_INDEX;
+
+ public void check(final String text, final char character, final int index,
+ final PreNode[] positionsReservedByPriority) {
+ nodeIdentifier.setPositions(positionsReservedByPriority);
+
+ for (Builder.NodeSetupSingleInstance nodeSetup : nodeSetups) {
+ TextIdentifier textIdentifier = nodeSetup.getTextIdentifier();
+ if (lastIndex > index) {
+ textIdentifier.reset();
+ }
+
+ nodeIdentifier.setNodeSetup(nodeSetup);
+ textIdentifier.checkByCharacter(text, character, index, nodeIdentifier);
+ }
+
+ lastIndex = index;
+ }
+
+ public void add(Builder.NodeSetupSingleInstance nodeSetup) {
+ nodeSetups.add(nodeSetup);
+ }
+ }
+
+ private static class NodeIdentifier implements NodePatternIdentifier {
+ private PreNode[] positionsReservedByPriority;
+ private Builder.NodeSetupSingleInstance nodeSetup;
+
+ @Override
+ public void found(int startIndex, int endIndex, InternalBlocks[] internalBlocks) {
+ boolean occupied = false;
+ int currentPriority = nodeSetup.getPriority();
+
+ int j = endIndex;
+ while (j > startIndex) {
+ PreNode preNode = positionsReservedByPriority[--j];
+
+ if (preNode != null) {
+ j = preNode.getStartIndex();
+
+ if (preNode.getPriority() >= currentPriority) {
+ occupied = true;
+ break;
+ }
+ }
+ }
+
+ if (!occupied) {
+ PreNode preNode = new PreNode(nodeSetup.getNodeCreator(),
+ nodeSetup.getPriority(),
+ startIndex,
+ endIndex,
+ internalBlocks
+ );
+ for (int i = startIndex; i < endIndex; i++) {
+ positionsReservedByPriority[i] = preNode;
+ }
+ }
+ }
+
+ public void setPositions(PreNode[] positionsReservedByPriority) {
+ this.positionsReservedByPriority = positionsReservedByPriority;
+ }
+
+ public void setNodeSetup(Builder.NodeSetupSingleInstance nodeSetup) {
+ this.nodeSetup = nodeSetup;
+ }
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/InlineParserNodeSetupFactory.java b/commonmark/src/main/java/org/commonmark/experimental/InlineParserNodeSetupFactory.java
new file mode 100644
index 000000000..8dd4a0aee
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/InlineParserNodeSetupFactory.java
@@ -0,0 +1,34 @@
+package org.commonmark.experimental;
+
+import org.commonmark.experimental.setup.EmphasisNodeSetup;
+import org.commonmark.experimental.setup.ImageLogoNodeSetup;
+import org.commonmark.experimental.setup.LinkNodeSetup;
+import org.commonmark.experimental.setup.StrongEmphasisNodeSetup;
+import org.commonmark.parser.InlineParser;
+import org.commonmark.parser.InlineParserContext;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class InlineParserNodeSetupFactory implements org.commonmark.parser.InlineParserFactory {
+ private final InlineParserImpl inlineParser;
+
+ @Override
+ public InlineParser create(InlineParserContext inlineParserContext) {
+ return inlineParser;
+ }
+
+ public InlineParserNodeSetupFactory() {
+ this(new ArrayList());
+ }
+
+ public InlineParserNodeSetupFactory(List nodeSetupExtensions) {
+ inlineParser = InlineParserImpl.builder()
+ .nodeSetup(new ImageLogoNodeSetup())
+ .nodeSetup(new LinkNodeSetup())
+ .nodeSetup(new StrongEmphasisNodeSetup())
+ .nodeSetup(new EmphasisNodeSetup())
+ .nodeSetup(nodeSetupExtensions)
+ .build();
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/NodeBreakLinePattern.java b/commonmark/src/main/java/org/commonmark/experimental/NodeBreakLinePattern.java
new file mode 100644
index 000000000..ab767c797
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/NodeBreakLinePattern.java
@@ -0,0 +1,5 @@
+package org.commonmark.experimental;
+
+public interface NodeBreakLinePattern {
+ char characterTrigger();
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/NodeCreator.java b/commonmark/src/main/java/org/commonmark/experimental/NodeCreator.java
new file mode 100644
index 000000000..9a73f06fa
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/NodeCreator.java
@@ -0,0 +1,8 @@
+package org.commonmark.experimental;
+
+import org.commonmark.experimental.NodePatternIdentifier.InternalBlocks;
+import org.commonmark.node.Node;
+
+public interface NodeCreator {
+ Node build(String textFound, InternalBlocks[] internalBlocks);
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/NodePatternIdentifier.java b/commonmark/src/main/java/org/commonmark/experimental/NodePatternIdentifier.java
new file mode 100644
index 000000000..bd33a9797
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/NodePatternIdentifier.java
@@ -0,0 +1,23 @@
+package org.commonmark.experimental;
+
+public interface NodePatternIdentifier {
+ void found(int startIndex, int endIndex, InternalBlocks[] internalBlocks);
+
+ class InternalBlocks {
+ private final int relativeStartIndex;
+ private final int relativeEndIndex;
+
+ public InternalBlocks(int relativeStartIndex, int relativeEndIndex) {
+ this.relativeStartIndex = relativeStartIndex;
+ this.relativeEndIndex = relativeEndIndex;
+ }
+
+ public int getRelativeStartIndex() {
+ return relativeStartIndex;
+ }
+
+ public int getRelativeEndIndex() {
+ return relativeEndIndex;
+ }
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/NodeSetup.java b/commonmark/src/main/java/org/commonmark/experimental/NodeSetup.java
new file mode 100644
index 000000000..581697a88
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/NodeSetup.java
@@ -0,0 +1,5 @@
+package org.commonmark.experimental;
+
+public interface NodeSetup {
+ NodeCreator nodeCreator();
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/PreNode.java b/commonmark/src/main/java/org/commonmark/experimental/PreNode.java
new file mode 100644
index 000000000..3ca815699
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/PreNode.java
@@ -0,0 +1,38 @@
+package org.commonmark.experimental;
+
+public class PreNode {
+ private final NodeCreator nodeCreator;
+ private final int startIndex;
+ private final int endIndex;
+ private final int priority;
+ private final NodePatternIdentifier.InternalBlocks[] internalBlocks;
+
+ public PreNode(NodeCreator nodeCreator, int priority, int startIndex, int endIndex,
+ NodePatternIdentifier.InternalBlocks[] internalBlocks) {
+ this.nodeCreator = nodeCreator;
+ this.startIndex = startIndex;
+ this.endIndex = endIndex;
+ this.priority = priority;
+ this.internalBlocks = internalBlocks;
+ }
+
+ public int getStartIndex() {
+ return startIndex;
+ }
+
+ public int getEndIndex() {
+ return endIndex;
+ }
+
+ public int getPriority() {
+ return priority;
+ }
+
+ public NodeCreator getNodeCreator() {
+ return nodeCreator;
+ }
+
+ public NodePatternIdentifier.InternalBlocks[] getInternalBlocks() {
+ return internalBlocks;
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/TextIdentifier.java b/commonmark/src/main/java/org/commonmark/experimental/TextIdentifier.java
new file mode 100644
index 000000000..ad5546795
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/TextIdentifier.java
@@ -0,0 +1,26 @@
+package org.commonmark.experimental;
+
+public abstract class TextIdentifier {
+ public static final char INVALID_CHAR = '\0';
+ public static final int INVALID_INDEX = Integer.MIN_VALUE;
+
+ private final NodeBreakLinePattern nodeBreakLinePattern;
+ protected int startIndex = INVALID_INDEX;
+ protected int endIndex = INVALID_INDEX;
+
+ public TextIdentifier(NodeBreakLinePattern nodeBreakLinePattern) {
+ this.nodeBreakLinePattern = nodeBreakLinePattern;
+ }
+
+ public abstract void checkByCharacter(String text, char character, int index,
+ NodePatternIdentifier nodePatternIdentifier);
+
+ protected void reset() {
+ startIndex = INVALID_INDEX;
+ endIndex = INVALID_INDEX;
+ }
+
+ public NodeBreakLinePattern getNodeBreakLinePattern() {
+ return nodeBreakLinePattern;
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/TextNodeIdentifierSetup.java b/commonmark/src/main/java/org/commonmark/experimental/TextNodeIdentifierSetup.java
new file mode 100644
index 000000000..d14ad21c7
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/TextNodeIdentifierSetup.java
@@ -0,0 +1,17 @@
+package org.commonmark.experimental;
+
+public interface TextNodeIdentifierSetup extends NodeSetup {
+ TextIdentifier textIdentifier();
+
+ int priority();
+
+ class DefaultPriority {
+ public static final int DEFAULT = 0;
+ public static final int REPEATABLE_SYMBOL = 10;
+ public static final int BRACKET_SYMBOL = 20;
+ public static final int BRACKET_PLUS_START_SYMBOL = 30;
+
+ private DefaultPriority() {
+ }
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/extractor/BracketContainerExtractor.java b/commonmark/src/main/java/org/commonmark/experimental/extractor/BracketContainerExtractor.java
new file mode 100644
index 000000000..1228add0a
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/extractor/BracketContainerExtractor.java
@@ -0,0 +1,31 @@
+package org.commonmark.experimental.extractor;
+
+import org.commonmark.experimental.NodePatternIdentifier.InternalBlocks;
+import org.commonmark.experimental.identifier.BracketContainerPattern;
+
+public class BracketContainerExtractor {
+ public static final String[] EMPTY_STRING = new String[0];
+
+ private BracketContainerExtractor() {
+ }
+
+ public static String[] from(String text,
+ BracketContainerPattern bracketContainerPattern,
+ InternalBlocks[] internalBlocks) {
+ if (text == null || internalBlocks.length == 0) {
+ return EMPTY_STRING;
+ }
+
+ int factorBegin = bracketContainerPattern.isStartBySingleSymbol() ? 1 : 0;
+ String[] contents = new String[internalBlocks.length];
+ for (int i = 0; i < internalBlocks.length; i++) {
+ int beginIndex = internalBlocks[i].getRelativeStartIndex() + factorBegin + 1;
+
+ contents[i] = text.substring(
+ beginIndex,
+ internalBlocks[i].getRelativeEndIndex() - 1);
+ factorBegin = 0;
+ }
+ return contents;
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/extractor/RepeatableSymbolContainerExtractor.java b/commonmark/src/main/java/org/commonmark/experimental/extractor/RepeatableSymbolContainerExtractor.java
new file mode 100644
index 000000000..48f8fe468
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/extractor/RepeatableSymbolContainerExtractor.java
@@ -0,0 +1,17 @@
+package org.commonmark.experimental.extractor;
+
+import org.commonmark.experimental.identifier.RepeatableSymbolContainerPattern;
+
+public class RepeatableSymbolContainerExtractor {
+ private RepeatableSymbolContainerExtractor() {
+ }
+
+ public static String from(String text, RepeatableSymbolContainerPattern repeatableSymbolContainerPattern) {
+ if (text == null || text.length() < repeatableSymbolContainerPattern.getSize() * 2) {
+ return "";
+ }
+
+ return text.substring(repeatableSymbolContainerPattern.getSize(),
+ text.length() - repeatableSymbolContainerPattern.getSize());
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/extractor/SingleSymbolContainerExtractor.java b/commonmark/src/main/java/org/commonmark/experimental/extractor/SingleSymbolContainerExtractor.java
new file mode 100644
index 000000000..fb2796838
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/extractor/SingleSymbolContainerExtractor.java
@@ -0,0 +1,13 @@
+package org.commonmark.experimental.extractor;
+
+public class SingleSymbolContainerExtractor {
+ private SingleSymbolContainerExtractor() {
+ }
+
+ public static String from(String text) {
+ if (text == null || text.length() < 2) {
+ return "";
+ }
+ return text.substring(1, text.length() - 1);
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/identifier/BracketContainerIdentifier.java b/commonmark/src/main/java/org/commonmark/experimental/identifier/BracketContainerIdentifier.java
new file mode 100644
index 000000000..5b1214063
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/identifier/BracketContainerIdentifier.java
@@ -0,0 +1,101 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodePatternIdentifier;
+import org.commonmark.experimental.NodePatternIdentifier.InternalBlocks;
+import org.commonmark.experimental.TextIdentifier;
+
+import java.util.List;
+
+public class BracketContainerIdentifier extends TextIdentifier {
+ private final List openCloses;
+ private final BracketContainerPattern nodeBreakLinePattern;
+ private InternalBlocks[] internalBlocks;
+ private int countBrackets = 0;
+ private int indexOpenClose = 0;
+ private boolean nextCharacterOpenExpected = false;
+ private boolean lastGroupClosed = false;
+ private BracketContainerPattern.OpenClose currentOpenClose;
+
+ public BracketContainerIdentifier(BracketContainerPattern nodeBreakLinePattern) {
+ super(nodeBreakLinePattern);
+ this.openCloses = nodeBreakLinePattern.getOpenCloses();
+ this.nodeBreakLinePattern = nodeBreakLinePattern;
+ this.currentOpenClose = openCloses.get(indexOpenClose);
+ this.internalBlocks = new InternalBlocks[openCloses.size()];
+ }
+
+ @Override
+ public void checkByCharacter(String text, char character, int index, NodePatternIdentifier nodePatternIdentifier) {
+ boolean isOpenCharacter = currentOpenClose.getOpen() == character;
+
+ checkNextCharacterIsOpenSymbol(index, isOpenCharacter);
+
+ if (nodeBreakLinePattern.characterTrigger() == character
+ && (startIndex == INVALID_INDEX || countBrackets == 0)) {
+ startIndex = index;
+ nextCharacterOpenExpected = nodeBreakLinePattern.isStartBySingleSymbol();
+ }
+
+ if (startIndex == INVALID_INDEX || !isCurrentGroupContinuation(isOpenCharacter)) {
+ return;
+ }
+
+ boolean countedAny = false;
+ if (isOpenCharacter) {
+ countBrackets++;
+ countedAny = true;
+ } else if (currentOpenClose.getClose() == character) {
+ countBrackets--;
+ countedAny = true;
+ }
+
+ if (!countedAny || countBrackets != 0) {
+ return;
+ }
+
+ int startIndexGroup = indexOpenClose == 0
+ ? 0
+ : internalBlocks[indexOpenClose - 1].getRelativeEndIndex();
+
+ internalBlocks[indexOpenClose] = new InternalBlocks(startIndexGroup, index - startIndex + 1);
+
+ if (indexOpenClose < openCloses.size() - 1) {
+ indexOpenClose++;
+ currentOpenClose = openCloses.get(indexOpenClose);
+ lastGroupClosed = true;
+ } else {
+ endIndex = index + 1;
+
+ nodePatternIdentifier.found(startIndex, endIndex, internalBlocks);
+ reset();
+ }
+ }
+
+ private void checkNextCharacterIsOpenSymbol(int index, boolean isOpenCharacter) {
+ if (nextCharacterOpenExpected && startIndex == index - 1 && !isOpenCharacter) {
+ startIndex = INVALID_INDEX;
+ nextCharacterOpenExpected = false;
+ }
+ }
+
+ private boolean isCurrentGroupContinuation(final boolean isOpenCharacter) {
+ if (lastGroupClosed && !isOpenCharacter) {
+ reset();
+ return false;
+ } else {
+ lastGroupClosed = false;
+ return true;
+ }
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ this.countBrackets = 0;
+ this.indexOpenClose = 0;
+ this.currentOpenClose = openCloses.get(indexOpenClose);
+ this.lastGroupClosed = false;
+ this.nextCharacterOpenExpected = false;
+ this.internalBlocks = new InternalBlocks[openCloses.size()];
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/identifier/BracketContainerPattern.java b/commonmark/src/main/java/org/commonmark/experimental/identifier/BracketContainerPattern.java
new file mode 100644
index 000000000..c4f3fcad1
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/identifier/BracketContainerPattern.java
@@ -0,0 +1,77 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodeBreakLinePattern;
+
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+public class BracketContainerPattern implements NodeBreakLinePattern {
+ public static final char INVALID_SYMBOL = '\0';
+ private final char startSymbol;
+ private final boolean startBySingleSymbol;
+ private final List openCloses;
+
+ private BracketContainerPattern(char startSymbol, OpenClose... openClose) {
+ this.openCloses = asList(openClose);
+ this.startBySingleSymbol = startSymbol != INVALID_SYMBOL;
+ this.startSymbol = getStartSymbol(startSymbol);
+ }
+
+ private char getStartSymbol(char startSymbol) {
+ if (startSymbol != INVALID_SYMBOL) {
+ return startSymbol;
+ }
+
+ if (!openCloses.isEmpty()) {
+ return openCloses.get(0).open;
+ }
+ return INVALID_SYMBOL;
+ }
+
+ @Override
+ public char characterTrigger() {
+ return startSymbol;
+ }
+
+ public List getOpenCloses() {
+ return openCloses;
+ }
+
+ public boolean isStartBySingleSymbol() {
+ return startBySingleSymbol;
+ }
+
+ public static class OpenClose {
+ private final char open;
+ private final char close;
+
+ private OpenClose(char open, char close) {
+ this.open = open;
+ this.close = close;
+ }
+
+ public char getOpen() {
+ return open;
+ }
+
+ public char getClose() {
+ return close;
+ }
+
+ public static OpenClose of(char open, char close) {
+ return new OpenClose(open, close);
+ }
+ }
+
+ public static BracketContainerPattern of(OpenClose... openClose) {
+ return BracketContainerPattern.of(INVALID_SYMBOL, openClose);
+ }
+
+ public static BracketContainerPattern of(char startSymbol, OpenClose... openClose) {
+ if (openClose.length == 0) {
+ throw new IllegalArgumentException("openClose must contain at least 1 configuration");
+ }
+ return new BracketContainerPattern(startSymbol, openClose);
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/identifier/RepeatableSymbolContainerIdentifier.java b/commonmark/src/main/java/org/commonmark/experimental/identifier/RepeatableSymbolContainerIdentifier.java
new file mode 100644
index 000000000..c5b525b4a
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/identifier/RepeatableSymbolContainerIdentifier.java
@@ -0,0 +1,74 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodePatternIdentifier;
+import org.commonmark.experimental.TextIdentifier;
+
+public class RepeatableSymbolContainerIdentifier extends TextIdentifier {
+ private final RepeatableSymbolContainerPattern nodeBreakLinePattern;
+ private final int patternSizeLessOne;
+ private int countStartSymbols;
+ private int countEndSymbols;
+
+ public RepeatableSymbolContainerIdentifier(RepeatableSymbolContainerPattern nodeBreakLinePattern) {
+ super(nodeBreakLinePattern);
+ this.nodeBreakLinePattern = nodeBreakLinePattern;
+ this.patternSizeLessOne = nodeBreakLinePattern.getSize() - 1;
+ }
+
+ @Override
+ public void checkByCharacter(String text, char character, int index, NodePatternIdentifier nodePatternIdentifier) {
+
+ final boolean symbolMatch = character == nodeBreakLinePattern.characterTrigger();
+
+ resetCounterIfSymbolDoesNotMatch(symbolMatch);
+
+ if (startIndex == INVALID_INDEX && symbolMatch) {
+ if (countStartSymbols == patternSizeLessOne) {
+ startIndex = index - countStartSymbols;
+ } else {
+ countStartSymbols++;
+ }
+ } else if (isCharacterAfterStartSymbolsSpace(character, index)) {
+ reset();
+ } else if (symbolMatch && isContentGreaterThanMinSize(index)) {
+ if (countEndSymbols == patternSizeLessOne) {
+ if (!isLastCharacterBeforeCloseSymbolSpace(text, index)) {
+ endIndex = index + 1;
+
+ nodePatternIdentifier.found(startIndex, endIndex, null);
+ }
+ reset();
+ } else {
+ countEndSymbols++;
+ }
+ }
+ }
+
+ private boolean isLastCharacterBeforeCloseSymbolSpace(String text, int index) {
+ return text.charAt(index - patternSizeLessOne - 1) == ' ';
+ }
+
+ private boolean isContentGreaterThanMinSize(int index) {
+ return index - startIndex - countStartSymbols - 1 > nodeBreakLinePattern.getMinSize();
+ }
+
+ private boolean isCharacterAfterStartSymbolsSpace(char character, int index) {
+ return index == startIndex + patternSizeLessOne + 1 && character == ' ';
+ }
+
+ private void resetCounterIfSymbolDoesNotMatch(boolean symbolMatch) {
+ if (countStartSymbols > 0 && !symbolMatch) {
+ countStartSymbols = 0;
+ }
+ if (countEndSymbols > 0 && !symbolMatch) {
+ countEndSymbols = 0;
+ }
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ countStartSymbols = 0;
+ countEndSymbols = 0;
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/identifier/RepeatableSymbolContainerPattern.java b/commonmark/src/main/java/org/commonmark/experimental/identifier/RepeatableSymbolContainerPattern.java
new file mode 100644
index 000000000..4dbf78c3e
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/identifier/RepeatableSymbolContainerPattern.java
@@ -0,0 +1,46 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodeBreakLinePattern;
+
+public class RepeatableSymbolContainerPattern implements NodeBreakLinePattern {
+ private final char symbol;
+ private final int size;
+ private final int minSize;
+ private final String literalSymbolContainer;
+
+ private RepeatableSymbolContainerPattern(char symbol, int size) {
+ this.symbol = symbol;
+ this.size = size;
+ this.minSize = 1;
+ this.literalSymbolContainer = buildLiteralText();
+ }
+
+ public static RepeatableSymbolContainerPattern of(char symbol, int size) {
+ return new RepeatableSymbolContainerPattern(symbol, size);
+ }
+
+ private String buildLiteralText() {
+ char[] repeatedCharacters = new char[size];
+ for (int i = 0; i < size; i++) {
+ repeatedCharacters[i] = symbol;
+ }
+ return new String(repeatedCharacters);
+ }
+
+ @Override
+ public char characterTrigger() {
+ return symbol;
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ public int getMinSize() {
+ return minSize;
+ }
+
+ public String literalSymbolContainer() {
+ return literalSymbolContainer;
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/identifier/SingleSymbolContainerIdentifier.java b/commonmark/src/main/java/org/commonmark/experimental/identifier/SingleSymbolContainerIdentifier.java
new file mode 100644
index 000000000..78a6884d9
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/identifier/SingleSymbolContainerIdentifier.java
@@ -0,0 +1,45 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodePatternIdentifier;
+import org.commonmark.experimental.TextIdentifier;
+
+public class SingleSymbolContainerIdentifier extends TextIdentifier {
+ private final SingleSymbolContainerPattern nodeBreakLinePattern;
+ private char lastCharacter = INVALID_CHAR;
+
+ public SingleSymbolContainerIdentifier(SingleSymbolContainerPattern nodeBreakLinePattern) {
+ super(nodeBreakLinePattern);
+ this.nodeBreakLinePattern = nodeBreakLinePattern;
+ }
+
+ @Override
+ public void checkByCharacter(String text, char character, int index, NodePatternIdentifier nodePatternIdentifier) {
+ if (character == nodeBreakLinePattern.characterTrigger()) {
+ if (startIndex == INVALID_INDEX) {
+ startIndex = index;
+ } else {
+ if (lastCharacter == ' ' || index - startIndex <= nodeBreakLinePattern.getMinSize()) {
+ reset();
+ startIndex = index;
+ } else {
+ endIndex = index + 1;
+ }
+
+ if (endIndex != INVALID_INDEX) {
+ nodePatternIdentifier.found(startIndex, endIndex, null);
+ reset();
+ }
+ }
+ } else if (index == startIndex + 1 && character == ' ') {
+ reset();
+ }
+
+ lastCharacter = character;
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ lastCharacter = '\0';
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/identifier/SingleSymbolContainerPattern.java b/commonmark/src/main/java/org/commonmark/experimental/identifier/SingleSymbolContainerPattern.java
new file mode 100644
index 000000000..a0724b004
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/identifier/SingleSymbolContainerPattern.java
@@ -0,0 +1,34 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodeBreakLinePattern;
+
+public class SingleSymbolContainerPattern implements NodeBreakLinePattern {
+ private final char symbol;
+ private final int minSize;
+
+ private SingleSymbolContainerPattern(char symbol) {
+ this(symbol, 1);
+ }
+
+ private SingleSymbolContainerPattern(char symbol, int minSize) {
+ this.symbol = symbol;
+ this.minSize = minSize;
+ }
+
+ public static SingleSymbolContainerPattern of(char symbol) {
+ return new SingleSymbolContainerPattern(symbol);
+ }
+
+ public static SingleSymbolContainerPattern of(char symbol, int minSize) {
+ return new SingleSymbolContainerPattern(symbol, minSize);
+ }
+
+ @Override
+ public char characterTrigger() {
+ return symbol;
+ }
+
+ public int getMinSize() {
+ return minSize;
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/identifier/StartSymbolIdentifier.java b/commonmark/src/main/java/org/commonmark/experimental/identifier/StartSymbolIdentifier.java
new file mode 100644
index 000000000..2305de2fe
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/identifier/StartSymbolIdentifier.java
@@ -0,0 +1,54 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodePatternIdentifier;
+import org.commonmark.experimental.TextIdentifier;
+
+public class StartSymbolIdentifier extends TextIdentifier {
+ private final StartSymbolPattern nodeBreakLinePattern;
+
+ public StartSymbolIdentifier(StartSymbolPattern nodeBreakLinePattern) {
+ super(nodeBreakLinePattern);
+ this.nodeBreakLinePattern = nodeBreakLinePattern;
+ }
+
+ @Override
+ public void checkByCharacter(String text, char character, int index, NodePatternIdentifier nodePatternIdentifier) {
+
+ if (startIndex == INVALID_INDEX
+ && character == nodeBreakLinePattern.characterTrigger()
+ && isFoundStart(text, index)
+ ) {
+ startIndex = index;
+ } else if (startIndex != INVALID_INDEX
+ && endIndex == INVALID_INDEX
+ && isFoundEnd(text, index)) {
+ endIndex = index + 1;
+
+ nodePatternIdentifier.found(startIndex, endIndex, null);
+ reset();
+ }
+ }
+
+ private boolean isFoundStart(String text, int index) {
+ return index == 0
+ || isNotDigitAndIsNotLetter(text.charAt(index - 1));
+ }
+
+ private boolean isFoundEnd(String text, int index) {
+ int lastIndex = text.length() - 1;
+ return index == lastIndex
+ || (index < lastIndex
+ && isNotInDontStopList(text.charAt(index + 1))
+ && isNotDigitAndIsNotLetter(text.charAt(index + 1))
+ );
+ }
+
+ private boolean isNotInDontStopList(char charAt) {
+ return !nodeBreakLinePattern.getDontStopAt().get(charAt);
+ }
+
+ private boolean isNotDigitAndIsNotLetter(char character) {
+ return !Character.isDigit(character)
+ && !Character.isLetter(character);
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/identifier/StartSymbolPattern.java b/commonmark/src/main/java/org/commonmark/experimental/identifier/StartSymbolPattern.java
new file mode 100644
index 000000000..767531ff4
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/identifier/StartSymbolPattern.java
@@ -0,0 +1,35 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodeBreakLinePattern;
+
+import java.util.BitSet;
+
+public class StartSymbolPattern implements NodeBreakLinePattern {
+ private final char startSymbol;
+ private final BitSet dontStopAt;
+
+ public StartSymbolPattern(char startSymbol) {
+ this(startSymbol, new char[0]);
+ }
+
+ public StartSymbolPattern(char startSymbol, char... dontStopAt) {
+ this.startSymbol = startSymbol;
+ this.dontStopAt = new BitSet();
+ fillDontStopCharacterSet(dontStopAt);
+ }
+
+ public BitSet getDontStopAt() {
+ return dontStopAt;
+ }
+
+ private void fillDontStopCharacterSet(char[] dontStopAt) {
+ for (Character character : dontStopAt) {
+ this.dontStopAt.set(character);
+ }
+ }
+
+ @Override
+ public char characterTrigger() {
+ return startSymbol;
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/setup/EmphasisNodeSetup.java b/commonmark/src/main/java/org/commonmark/experimental/setup/EmphasisNodeSetup.java
new file mode 100644
index 000000000..028a869db
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/setup/EmphasisNodeSetup.java
@@ -0,0 +1,39 @@
+package org.commonmark.experimental.setup;
+
+import org.commonmark.experimental.NodeCreator;
+import org.commonmark.experimental.NodePatternIdentifier.InternalBlocks;
+import org.commonmark.experimental.TextIdentifier;
+import org.commonmark.experimental.TextNodeIdentifierSetup;
+import org.commonmark.experimental.extractor.SingleSymbolContainerExtractor;
+import org.commonmark.experimental.identifier.SingleSymbolContainerIdentifier;
+import org.commonmark.experimental.identifier.SingleSymbolContainerPattern;
+import org.commonmark.node.Emphasis;
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
+
+public class EmphasisNodeSetup implements TextNodeIdentifierSetup {
+
+ private static final SingleSymbolContainerPattern EMPHASIS_PATTERN = SingleSymbolContainerPattern.of('*');
+
+ @Override
+ public TextIdentifier textIdentifier() {
+ return new SingleSymbolContainerIdentifier(EMPHASIS_PATTERN);
+ }
+
+ @Override
+ public int priority() {
+ return DefaultPriority.DEFAULT;
+ }
+
+ @Override
+ public NodeCreator nodeCreator() {
+ return new NodeCreator() {
+ @Override
+ public Node build(String textFound, InternalBlocks[] internalBlocks) {
+ Emphasis emphasis = new Emphasis("*");
+ emphasis.appendChild(new Text(SingleSymbolContainerExtractor.from(textFound)));
+ return emphasis;
+ }
+ };
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/setup/ImageLogoNodeSetup.java b/commonmark/src/main/java/org/commonmark/experimental/setup/ImageLogoNodeSetup.java
new file mode 100644
index 000000000..5165a731b
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/setup/ImageLogoNodeSetup.java
@@ -0,0 +1,41 @@
+package org.commonmark.experimental.setup;
+
+import org.commonmark.experimental.NodeCreator;
+import org.commonmark.experimental.NodePatternIdentifier.InternalBlocks;
+import org.commonmark.experimental.TextIdentifier;
+import org.commonmark.experimental.TextNodeIdentifierSetup;
+import org.commonmark.experimental.extractor.BracketContainerExtractor;
+import org.commonmark.experimental.identifier.BracketContainerIdentifier;
+import org.commonmark.experimental.identifier.BracketContainerPattern;
+import org.commonmark.experimental.identifier.BracketContainerPattern.OpenClose;
+import org.commonmark.node.Image;
+import org.commonmark.node.Node;
+
+public class ImageLogoNodeSetup implements TextNodeIdentifierSetup {
+
+ private static final BracketContainerPattern LOGO_IMAGE_PATTERN = BracketContainerPattern.of(
+ '!',
+ OpenClose.of('[', ']'),
+ OpenClose.of('(', ')'));
+
+ @Override
+ public TextIdentifier textIdentifier() {
+ return new BracketContainerIdentifier(LOGO_IMAGE_PATTERN);
+ }
+
+ @Override
+ public int priority() {
+ return DefaultPriority.BRACKET_PLUS_START_SYMBOL;
+ }
+
+ @Override
+ public NodeCreator nodeCreator() {
+ return new NodeCreator() {
+ @Override
+ public Node build(String textFound, InternalBlocks[] internalBlocks) {
+ String[] content = BracketContainerExtractor.from(textFound, LOGO_IMAGE_PATTERN, internalBlocks);
+ return new Image(content[1], content[0]);
+ }
+ };
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/setup/LinkNodeSetup.java b/commonmark/src/main/java/org/commonmark/experimental/setup/LinkNodeSetup.java
new file mode 100644
index 000000000..53cebdea1
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/setup/LinkNodeSetup.java
@@ -0,0 +1,40 @@
+package org.commonmark.experimental.setup;
+
+import org.commonmark.experimental.NodeCreator;
+import org.commonmark.experimental.NodePatternIdentifier.InternalBlocks;
+import org.commonmark.experimental.TextIdentifier;
+import org.commonmark.experimental.TextNodeIdentifierSetup;
+import org.commonmark.experimental.extractor.BracketContainerExtractor;
+import org.commonmark.experimental.identifier.BracketContainerIdentifier;
+import org.commonmark.experimental.identifier.BracketContainerPattern;
+import org.commonmark.experimental.identifier.BracketContainerPattern.OpenClose;
+import org.commonmark.node.Link;
+import org.commonmark.node.Node;
+
+public class LinkNodeSetup implements TextNodeIdentifierSetup {
+
+ private static final BracketContainerPattern LINK_PATTERN = BracketContainerPattern.of(
+ OpenClose.of('[', ']'),
+ OpenClose.of('(', ')'));
+
+ @Override
+ public TextIdentifier textIdentifier() {
+ return new BracketContainerIdentifier(LINK_PATTERN);
+ }
+
+ @Override
+ public int priority() {
+ return DefaultPriority.BRACKET_SYMBOL;
+ }
+
+ @Override
+ public NodeCreator nodeCreator() {
+ return new NodeCreator() {
+ @Override
+ public Node build(String textFound, InternalBlocks[] internalBlocks) {
+ String[] content = BracketContainerExtractor.from(textFound, LINK_PATTERN, internalBlocks);
+ return new Link(content[1], content[0]);
+ }
+ };
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/experimental/setup/StrongEmphasisNodeSetup.java b/commonmark/src/main/java/org/commonmark/experimental/setup/StrongEmphasisNodeSetup.java
new file mode 100644
index 000000000..dd48d2e29
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/experimental/setup/StrongEmphasisNodeSetup.java
@@ -0,0 +1,41 @@
+package org.commonmark.experimental.setup;
+
+import org.commonmark.experimental.NodeCreator;
+import org.commonmark.experimental.NodePatternIdentifier.InternalBlocks;
+import org.commonmark.experimental.TextIdentifier;
+import org.commonmark.experimental.TextNodeIdentifierSetup;
+import org.commonmark.experimental.extractor.RepeatableSymbolContainerExtractor;
+import org.commonmark.experimental.identifier.RepeatableSymbolContainerIdentifier;
+import org.commonmark.experimental.identifier.RepeatableSymbolContainerPattern;
+import org.commonmark.node.Node;
+import org.commonmark.node.StrongEmphasis;
+import org.commonmark.node.Text;
+
+public class StrongEmphasisNodeSetup implements TextNodeIdentifierSetup {
+
+ private static final RepeatableSymbolContainerPattern STRONG_EMPHASIS_PATTERN =
+ RepeatableSymbolContainerPattern.of('*', 2);
+
+ @Override
+ public TextIdentifier textIdentifier() {
+ return new RepeatableSymbolContainerIdentifier(STRONG_EMPHASIS_PATTERN);
+ }
+
+ @Override
+ public int priority() {
+ return DefaultPriority.REPEATABLE_SYMBOL;
+ }
+
+ @Override
+ public NodeCreator nodeCreator() {
+ return new NodeCreator() {
+ @Override
+ public Node build(String textFound, InternalBlocks[] internalBlocks) {
+ StrongEmphasis emphasis = new StrongEmphasis(STRONG_EMPHASIS_PATTERN.literalSymbolContainer());
+ emphasis.appendChild(
+ new Text(RepeatableSymbolContainerExtractor.from(textFound, STRONG_EMPHASIS_PATTERN)));
+ return emphasis;
+ }
+ };
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/parser/Parser.java b/commonmark/src/main/java/org/commonmark/parser/Parser.java
index 5e15158ad..e02d7dfe2 100644
--- a/commonmark/src/main/java/org/commonmark/parser/Parser.java
+++ b/commonmark/src/main/java/org/commonmark/parser/Parser.java
@@ -1,6 +1,7 @@
package org.commonmark.parser;
import org.commonmark.Extension;
+import org.commonmark.experimental.InlineParserNodeSetupFactory;
import org.commonmark.internal.DocumentParser;
import org.commonmark.internal.InlineParserContextImpl;
import org.commonmark.internal.InlineParserImpl;
diff --git a/commonmark/src/test/java/org/commonmark/experimental/InlineParserImplMultipleNodesSetupTest.java b/commonmark/src/test/java/org/commonmark/experimental/InlineParserImplMultipleNodesSetupTest.java
new file mode 100644
index 000000000..604e7798e
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/InlineParserImplMultipleNodesSetupTest.java
@@ -0,0 +1,144 @@
+package org.commonmark.experimental;
+
+import org.commonmark.experimental.NodePatternIdentifier.InternalBlocks;
+import org.commonmark.experimental.identifier.RepeatableSymbolContainerIdentifier;
+import org.commonmark.experimental.identifier.RepeatableSymbolContainerPattern;
+import org.commonmark.experimental.identifier.SingleSymbolContainerIdentifier;
+import org.commonmark.experimental.identifier.SingleSymbolContainerPattern;
+import org.commonmark.experimental.identifier.StartSymbolIdentifier;
+import org.commonmark.experimental.identifier.StartSymbolPattern;
+import org.commonmark.node.Node;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+public class InlineParserImplMultipleNodesSetupTest {
+ private NodeCreator nodeCreatorOne;
+ private NodeCreator nodeCreatorTwo;
+
+ private NodeSetup literalNodeSetup;
+ private Node nodeParent;
+
+ @Before
+ public void setUp() {
+ nodeParent = mock(Node.class);
+ nodeCreatorOne = mock(NodeCreator.class);
+ nodeCreatorTwo = mock(NodeCreator.class);
+
+ literalNodeSetup = mock(NodeSetup.class);
+ when(literalNodeSetup.nodeCreator()).thenReturn(mock(NodeCreator.class));
+ }
+
+ @Test
+ public void shouldConsiderTwoNodesSetup() {
+ setupParser(new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('*')),
+ new StartSymbolIdentifier(new StartSymbolPattern('@')))
+ .parse("*image.gif* @second", nodeParent);
+
+ verify(nodeCreatorOne).build(eq("*image.gif*"), eq((InternalBlocks[]) null));
+ verify(nodeCreatorTwo).build(eq("@second"), eq((InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldConsiderTwoNodeSetupWithSameStartSymbolAndSamePriorityTheFirstToAdded() {
+ setupParser(0, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('*')),
+ 0, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('*')))
+ .parse("*image.gif*", nodeParent);
+
+ verify(nodeCreatorOne).build(eq("*image.gif*"), eq((InternalBlocks[]) null));
+ verifyNoInteractions(nodeCreatorTwo);
+ }
+
+ @Test
+ public void shouldConsiderTwoNodeSetupWithSameStartSymbolWinningTheHighPriority() {
+ setupParser(0, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('~')),
+ 10, new RepeatableSymbolContainerIdentifier(RepeatableSymbolContainerPattern.of('~', 2)))
+ .parse("~image.gif~ ~~second.jpg~~", nodeParent);
+
+ verify(nodeCreatorOne).build(eq("~image.gif~"), eq((InternalBlocks[]) null));
+ verify(nodeCreatorTwo).build(eq("~~second.jpg~~"), eq((InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldConsiderTheNodeSetupByPriorityForOverlapConflict() {
+ setupParser(1, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('`')),
+ 0, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('~')))
+ .parse("`the ~image.gif~ here`", nodeParent);
+
+ verify(nodeCreatorOne).build(eq("`the ~image.gif~ here`"), eq((InternalBlocks[]) null));
+ verifyNoInteractions(nodeCreatorTwo);
+ }
+
+ @Test
+ public void shouldConsiderTheNodeSetupByPriorityForLineConflictForInternalComponent() {
+ setupParser(0, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('`')),
+ 1, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('~')))
+ .parse("`the ~image.gif~ here`", nodeParent);
+
+ verifyNoInteractions(nodeCreatorOne);
+ verify(nodeCreatorTwo).build(eq("~image.gif~"), eq((InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldConsiderTheNodeSetupByPriorityForLineConflictForOverlapByLeftSide() {
+ setupParser(1, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('`')),
+ 0, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('~')))
+ .parse("`the ~image.gif here` another~", nodeParent);
+
+ verify(nodeCreatorOne).build(eq("`the ~image.gif here`"), eq((InternalBlocks[]) null));
+ verifyNoInteractions(nodeCreatorTwo);
+ }
+
+ @Test
+ public void shouldConsiderTheNodeSetupByPriorityForLineConflictMultipleOccurrences() {
+ setupParser(1, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('`')),
+ 0, new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('~')))
+ .parse("~the `image.gif` here `second.jpg` another~", nodeParent);
+
+ verify(nodeCreatorOne).build(eq("`image.gif`"), eq((InternalBlocks[]) null));
+ verify(nodeCreatorOne).build(eq("`second.jpg`"), eq((InternalBlocks[]) null));
+ verifyNoInteractions(nodeCreatorTwo);
+ }
+
+ private InlineParserImpl setupParser(final int priorityOne, final TextIdentifier textIdentifierOne,
+ final int priorityTwo, final TextIdentifier textIdentifierTwo) {
+ TextNodeIdentifierSetup nodeSetupOne = textNodeIdentifierSetup(textIdentifierOne, priorityOne, nodeCreatorOne);
+ TextNodeIdentifierSetup nodeSetupTwo = textNodeIdentifierSetup(textIdentifierTwo, priorityTwo, nodeCreatorTwo);
+
+ return InlineParserImpl.builder()
+ .literalNodeSetup(literalNodeSetup)
+ .nodeSetup(nodeSetupOne, nodeSetupTwo)
+ .build();
+ }
+
+ private InlineParserImpl setupParser(final TextIdentifier textIdentifierOne,
+ final TextIdentifier textIdentifierTwo) {
+ return setupParser(0, textIdentifierOne, 0, textIdentifierTwo);
+ }
+
+ private TextNodeIdentifierSetup textNodeIdentifierSetup(final TextIdentifier textIdentifierOne,
+ final int priority,
+ final NodeCreator nodeCreatorOne) {
+ return new TextNodeIdentifierSetup() {
+ @Override
+ public TextIdentifier textIdentifier() {
+ return textIdentifierOne;
+ }
+
+ @Override
+ public int priority() {
+ return priority;
+ }
+
+ @Override
+ public NodeCreator nodeCreator() {
+ return nodeCreatorOne;
+ }
+ };
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/InlineParserImplTest.java b/commonmark/src/test/java/org/commonmark/experimental/InlineParserImplTest.java
new file mode 100644
index 000000000..6c7c0a3db
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/InlineParserImplTest.java
@@ -0,0 +1,123 @@
+package org.commonmark.experimental;
+
+import org.commonmark.experimental.identifier.SingleSymbolContainerIdentifier;
+import org.commonmark.experimental.identifier.SingleSymbolContainerPattern;
+import org.commonmark.experimental.identifier.StartSymbolIdentifier;
+import org.commonmark.experimental.identifier.StartSymbolPattern;
+import org.commonmark.node.Node;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class InlineParserImplTest {
+ private NodeCreator nodeCreator;
+ private NodeCreator literalNodeCreator;
+ private NodeSetup literalNodeSetup;
+ private Node nodeParent;
+
+ @Before
+ public void setUp() {
+ nodeParent = mock(Node.class);
+ nodeCreator = mock(NodeCreator.class);
+ literalNodeCreator = mock(NodeCreator.class);
+ literalNodeSetup = mock(NodeSetup.class);
+ when(literalNodeSetup.nodeCreator()).thenReturn(literalNodeCreator);
+ }
+
+ @Test
+ public void shouldParseTextByNodeSetup() {
+ setupParser(new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('~')))
+ .parse("some ~image.gif~ text", nodeParent);
+
+ verify(nodeCreator).build(eq("~image.gif~"), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldNoticeNodesMultipleTimes() {
+ setupParser(new StartSymbolIdentifier(new StartSymbolPattern('/', '.')))
+ .parse("/image.gif /something.jpg", nodeParent);
+
+ verify(nodeCreator).build(eq("/image.gif"), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ verify(nodeCreator).build(eq("/something.jpg"), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldConsiderStartSymbolBetweenWords() {
+ setupParser(new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('~')))
+ .parse("~image.gif~ something~second.jpg~", nodeParent);
+
+ verify(nodeCreator).build(eq("~image.gif~"), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ verify(nodeCreator).build(eq("~second.jpg~"), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldResetConfigurationWhenReadLineASecondTime() {
+ InlineParserImpl inlineParser = setupParser(new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('*')));
+ inlineParser.parse("some *image text", nodeParent);
+ inlineParser.parse("some *second.gif* text", nodeParent);
+
+ verify(nodeCreator).build(eq("*second.gif*"), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldTakeLiteralBeforeNodeFound() {
+ setupParser(new StartSymbolIdentifier(new StartSymbolPattern('~')))
+ .parse("some ~node", nodeParent);
+
+ verify(literalNodeCreator).build(eq("some "), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldTakeLiteralAfterNodeFound() {
+ setupParser(new StartSymbolIdentifier(new StartSymbolPattern('~')))
+ .parse("~node after", nodeParent);
+
+ verify(literalNodeCreator).build(eq(" after"), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldTakeLiteralBetweenNodes() {
+ setupParser(new StartSymbolIdentifier(new StartSymbolPattern('~')))
+ .parse("~nodeone between ~nodetwo", nodeParent);
+
+ verify(literalNodeCreator).build(eq(" between "), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ }
+
+ @Test
+ public void shouldTakeLiteralBetweenNodesInTheEndOfTheLine() {
+ setupParser(new StartSymbolIdentifier(new StartSymbolPattern('~')))
+ .parse(" ~nodeone between ~nodetwo after", nodeParent);
+
+ verify(literalNodeCreator).build(eq(" "), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ verify(literalNodeCreator).build(eq(" between "), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ verify(literalNodeCreator).build(eq(" after"), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ }
+
+ private InlineParserImpl setupParser(final TextIdentifier textIdentifier) {
+ TextNodeIdentifierSetup nodeSetup = new TextNodeIdentifierSetup() {
+ @Override
+ public TextIdentifier textIdentifier() {
+ return textIdentifier;
+ }
+
+ @Override
+ public int priority() {
+ return 0;
+ }
+
+ @Override
+ public NodeCreator nodeCreator() {
+ return nodeCreator;
+ }
+ };
+
+ return InlineParserImpl.builder()
+ .literalNodeSetup(literalNodeSetup)
+ .nodeSetup(nodeSetup)
+ .build();
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/InlineParserNodeSetupFactoryTest.java b/commonmark/src/test/java/org/commonmark/experimental/InlineParserNodeSetupFactoryTest.java
new file mode 100644
index 000000000..e53a8f51f
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/InlineParserNodeSetupFactoryTest.java
@@ -0,0 +1,138 @@
+package org.commonmark.experimental;
+
+
+import org.commonmark.node.Emphasis;
+import org.commonmark.node.Image;
+import org.commonmark.node.Link;
+import org.commonmark.node.LinkReferenceDefinition;
+import org.commonmark.node.Node;
+import org.commonmark.node.Paragraph;
+import org.commonmark.node.StrongEmphasis;
+import org.commonmark.node.Text;
+import org.commonmark.parser.InlineParser;
+import org.commonmark.parser.InlineParserContext;
+import org.commonmark.parser.delimiter.DelimiterProcessor;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class InlineParserNodeSetupFactoryTest {
+ private InlineParser inlineParser;
+ private Paragraph paragraph = new Paragraph();
+
+ @Before
+ public void setUp() {
+ inlineParser = new InlineParserNodeSetupFactory().create(new InlineParserContext() {
+ @Override
+ public List getCustomDelimiterProcessors() {
+ return null;
+ }
+
+ @Override
+ public LinkReferenceDefinition getLinkReferenceDefinition(String label) {
+ return null;
+ }
+ });
+
+ paragraph = new Paragraph();
+ }
+
+ @Test
+ public void shouldTakeLiteralTextInParagraph() {
+ inlineParser.parse("easy test", paragraph);
+
+ assertThat(paragraph.getFirstChild(), instanceOf(Text.class));
+ assertThat(((Text) paragraph.getFirstChild()).getLiteral(), is("easy test"));
+ }
+
+ @Test
+ public void shouldNoticeImageLogoNode() {
+ inlineParser.parse("", paragraph);
+
+ assertThat(paragraph.getFirstChild(), instanceOf(Image.class));
+ assertThat(((Image) paragraph.getFirstChild()).getTitle(), is("foo"));
+ assertThat(((Image) paragraph.getFirstChild()).getDestination(), is("/train.jpg"));
+ }
+
+ @Test
+ public void shouldNoticeLink() {
+ inlineParser.parse("[foo](a.com)", paragraph);
+
+ assertThat(paragraph.getFirstChild(), instanceOf(Link.class));
+ assertThat(((Link) paragraph.getFirstChild()).getTitle(), is("foo"));
+ assertThat(((Link) paragraph.getFirstChild()).getDestination(), is("a.com"));
+ }
+
+ @Test
+ public void shouldNoticeEmphasisText() {
+ inlineParser.parse("This is *Sparta*", paragraph);
+
+ assertThat(paragraph.getFirstChild(), instanceOf(Text.class));
+ assertThat(((Text) paragraph.getFirstChild()).getLiteral(), is("This is"));
+ assertThat(paragraph.getFirstChild().getNext(), instanceOf(Emphasis.class));
+ assertThat(((Emphasis) paragraph.getFirstChild().getNext()).getOpeningDelimiter(), is("*"));
+ }
+
+ @Test
+ public void shouldNoticeStrongEmphasisText() {
+ inlineParser.parse("This is **Strong**", paragraph);
+
+ assertThat(paragraph.getFirstChild(), instanceOf(Text.class));
+ assertThat(((Text) paragraph.getFirstChild()).getLiteral(), is("This is"));
+ assertThat(paragraph.getFirstChild().getNext(), instanceOf(StrongEmphasis.class));
+ assertThat(((StrongEmphasis) paragraph.getFirstChild().getNext()).getOpeningDelimiter(), is("**"));
+ }
+
+ @Test
+ public void shouldAddExtraNodeSetupConfiguration() {
+ final Node nodeToCheck = mock(Node.class);
+ TextNodeIdentifierSetup textNodeIdentifierSetup = new TextNodeIdentifierSetup() {
+ @Override
+ public NodeCreator nodeCreator() {
+ return new NodeCreator() {
+ @Override
+ public Node build(String textFound, NodePatternIdentifier.InternalBlocks[] internalBlocks) {
+ return nodeToCheck;
+ }
+ };
+ }
+
+ @Override
+ public TextIdentifier textIdentifier() {
+ return new TextIdentifier(new NodeBreakLinePattern() {
+ @Override
+ public char characterTrigger() {
+ return 'a';
+ }
+ }) {
+ @Override
+ public void checkByCharacter(String text, char character, int index, NodePatternIdentifier nodePatternIdentifier) {
+ nodePatternIdentifier.found(0, 3, null);
+ }
+ };
+ }
+
+ @Override
+ public int priority() {
+ return -10000;
+ }
+ };
+
+ List nodeSetupExtensions = new ArrayList<>();
+ nodeSetupExtensions.add(textNodeIdentifierSetup);
+ InlineParserNodeSetupFactory inlineParserNodeSetupFactory =
+ new InlineParserNodeSetupFactory(nodeSetupExtensions);
+
+ InlineParser inlineParser = inlineParserNodeSetupFactory.create(null);
+ inlineParser.parse("abc", paragraph);
+
+ assertThat(paragraph.getFirstChild(), is(nodeToCheck));
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/ParseBenchmark.java b/commonmark/src/test/java/org/commonmark/experimental/ParseBenchmark.java
new file mode 100644
index 000000000..da7efa60c
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/ParseBenchmark.java
@@ -0,0 +1,48 @@
+package org.commonmark.experimental;
+
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+import java.util.concurrent.TimeUnit;
+
+@BenchmarkMode(Mode.SingleShotTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Measurement(batchSize = 10000, iterations = 5)
+@Warmup(batchSize = 10000, iterations = 5)
+public class ParseBenchmark {
+ public static final String lineToBeParsed = "Some text  *another* **point** [link](a.com)";
+
+ private static final Parser regularParser = Parser.builder().build();
+ private static final Parser nodeSetupFashionParser = Parser.builder()
+ .inlineParserFactory(new InlineParserNodeSetupFactory())
+ .build();
+
+ public static void main(String[] args) throws RunnerException {
+ Options opt = new OptionsBuilder()
+ .include(".*" + ParseBenchmark.class.getSimpleName())
+ .build();
+
+ new Runner(opt).run();
+ }
+
+ @Benchmark
+ public Node currentRegularParser() {
+ return regularParser.parse(lineToBeParsed);
+ }
+
+ @Benchmark
+ public Node newFashionParser() {
+ return nodeSetupFashionParser.parse(lineToBeParsed);
+ }
+
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/extractor/BracketContainerExtractorTest.java b/commonmark/src/test/java/org/commonmark/experimental/extractor/BracketContainerExtractorTest.java
new file mode 100644
index 000000000..c8a86bec3
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/extractor/BracketContainerExtractorTest.java
@@ -0,0 +1,77 @@
+package org.commonmark.experimental.extractor;
+
+import org.commonmark.experimental.identifier.BracketContainerPattern;
+import org.junit.Test;
+
+import static org.commonmark.experimental.NodePatternIdentifier.InternalBlocks;
+import static org.commonmark.experimental.identifier.BracketContainerPattern.OpenClose;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class BracketContainerExtractorTest {
+ @Test
+ public void shouldExtractJustTextSkippingTheContainers() {
+ String[] content = BracketContainerExtractor.from("(some)",
+ BracketContainerPattern.of(OpenClose.of('(', ')')),
+ new InternalBlocks[]{new InternalBlocks(0, 6)});
+
+ assertThat(content.length, is(1));
+ assertThat(content[0], is("some"));
+ }
+
+ @Test
+ public void shouldExtractJustTextSkippingTheContainersForMultipleBrackets() {
+ String[] content = BracketContainerExtractor.from("[test()[]](some()[])",
+ BracketContainerPattern.of(
+ OpenClose.of('[', ']'),
+ OpenClose.of('(', ')')),
+ new InternalBlocks[]{
+ new InternalBlocks(0, 10),
+ new InternalBlocks(10, 20)
+ });
+
+ assertThat(content.length, is(2));
+ assertThat(content[0], is("test()[]"));
+ assertThat(content[1], is("some()[]"));
+ }
+
+ @Test
+ public void shouldExtractJustTextSkippingTheContainersAndFirstSymbolForMultipleBrackets() {
+ String[] content = BracketContainerExtractor.from("![test()[]](some()[])",
+ BracketContainerPattern.of(
+ '!',
+ OpenClose.of('[', ']'),
+ OpenClose.of('(', ')')),
+ new InternalBlocks[]{
+ new InternalBlocks(0, 11),
+ new InternalBlocks(11, 21)
+ });
+
+ assertThat(content.length, is(2));
+ assertThat(content[0], is("test()[]"));
+ assertThat(content[1], is("some()[]"));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfNotNullText() {
+ String[] content = BracketContainerExtractor.from(null,
+ BracketContainerPattern.of(OpenClose.of('[', ']')),
+ new InternalBlocks[]{
+ new InternalBlocks(0, 6)
+ });
+
+ assertThat(content.length, is(0));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfNoContent() {
+ String[] content = BracketContainerExtractor.from("()",
+ BracketContainerPattern.of(OpenClose.of('(', ')')),
+ new InternalBlocks[]{
+ new InternalBlocks(0, 2)
+ });
+
+ assertThat(content.length, is(1));
+ assertThat(content[0], is(""));
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/extractor/RepeatableSymbolContainerExtractorTest.java b/commonmark/src/test/java/org/commonmark/experimental/extractor/RepeatableSymbolContainerExtractorTest.java
new file mode 100644
index 000000000..6fb609a2f
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/extractor/RepeatableSymbolContainerExtractorTest.java
@@ -0,0 +1,47 @@
+package org.commonmark.experimental.extractor;
+
+import org.commonmark.experimental.identifier.RepeatableSymbolContainerPattern;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+public class RepeatableSymbolContainerExtractorTest {
+ @Test
+ public void shouldExtractJustTextSkippingTheFirstCharacter() {
+ assertThat(RepeatableSymbolContainerExtractor
+ .from("~~some~~", RepeatableSymbolContainerPattern.of('~', 2)),
+ is("some"));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfNotNullText() {
+ assertThat(RepeatableSymbolContainerExtractor
+ .from(null, RepeatableSymbolContainerPattern.of('~', 2)),
+ is(""));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfEmptyText() {
+ assertThat(RepeatableSymbolContainerExtractor
+ .from("", RepeatableSymbolContainerPattern.of('~', 2)),
+ is(""));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfNoContent() {
+ assertThat(RepeatableSymbolContainerExtractor
+ .from("~~~~", RepeatableSymbolContainerPattern.of('~', 2)),
+ is(""));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfLessThenMinSizeCharacter() {
+ assertThat(RepeatableSymbolContainerExtractor
+ .from("~~~", RepeatableSymbolContainerPattern.of('~', 2)),
+ is(""));
+ assertThat(RepeatableSymbolContainerExtractor
+ .from("~~", RepeatableSymbolContainerPattern.of('~', 2)),
+ is(""));
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/extractor/SingleSymbolContainerExtractorTest.java b/commonmark/src/test/java/org/commonmark/experimental/extractor/SingleSymbolContainerExtractorTest.java
new file mode 100644
index 000000000..d46f9e045
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/extractor/SingleSymbolContainerExtractorTest.java
@@ -0,0 +1,33 @@
+package org.commonmark.experimental.extractor;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class SingleSymbolContainerExtractorTest {
+ @Test
+ public void shouldExtractJustTextSkippingTheFirstCharacter() {
+ assertThat(SingleSymbolContainerExtractor.from("~some~"), is("some"));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfNotNullText() {
+ assertThat(SingleSymbolContainerExtractor.from(null), is(""));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfNoContent() {
+ assertThat(SingleSymbolContainerExtractor.from("~~"), is(""));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfOneCharacter() {
+ assertThat(SingleSymbolContainerExtractor.from("~"), is(""));
+ }
+
+ @Test
+ public void shouldReturnEmptyIfEmptyText() {
+ assertThat(SingleSymbolContainerExtractor.from(""), is(""));
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/identifier/BracketContainerIdentifierTest.java b/commonmark/src/test/java/org/commonmark/experimental/identifier/BracketContainerIdentifierTest.java
new file mode 100644
index 000000000..dcd1f4263
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/identifier/BracketContainerIdentifierTest.java
@@ -0,0 +1,300 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodePatternIdentifier;
+import org.commonmark.experimental.identifier.BracketContainerPattern.OpenClose;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.commonmark.experimental.identifier.TextIdentifierBaseTest.readLine;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+public class BracketContainerIdentifierTest {
+ private NodePatternIdentifier nodePatternIdentifier;
+ private BracketContainerIdentifier textIdentifier;
+ private ArgumentCaptor argumentCaptor;
+
+ @Before
+ public void setUp() {
+ argumentCaptor = ArgumentCaptor.forClass(NodePatternIdentifier.InternalBlocks[].class);
+ nodePatternIdentifier = mock(NodePatternIdentifier.class);
+ textIdentifier = new BracketContainerIdentifier(
+ BracketContainerPattern.of(OpenClose.of('[', ']')));
+ }
+
+ @Test
+ public void shouldIdentifySimpleBracket() {
+ readLine("[i]", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(3), argumentCaptor.capture());
+ assertThat(argumentCaptor.getValue().length, is(1));
+ assertThat(argumentCaptor.getValue()[0].getRelativeStartIndex(), is(0));
+ assertThat(argumentCaptor.getValue()[0].getRelativeEndIndex(), is(3));
+ }
+
+ @Test
+ public void shouldIdentifyInternalBlockPositionAsRelativeIndex() {
+ readLine("some [i] end", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(5), eq(8), argumentCaptor.capture());
+
+ assertThat(argumentCaptor.getValue().length, is(1));
+ assertThat(argumentCaptor.getValue()[0].getRelativeStartIndex(), is(0));
+ assertThat(argumentCaptor.getValue()[0].getRelativeEndIndex(), is(3));
+ }
+
+ @Test
+ public void shouldIdentifySimpleBracketWithEmptyContent() {
+ readLine("[]", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(2), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldNotIdentifyIfJustOpenBracket() {
+ readLine("[i", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyIfJustCloseBracket() {
+ readLine("i]", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldIdentifyTwiceIfTwiceInTheLine() {
+ readLine("[i][b]", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(3), argumentCaptor.capture());
+ assertThat(argumentCaptor.getValue().length, is(1));
+ assertThat(argumentCaptor.getValue()[0].getRelativeStartIndex(), is(0));
+ assertThat(argumentCaptor.getValue()[0].getRelativeEndIndex(), is(3));
+
+ verify(nodePatternIdentifier).found(eq(3), eq(6), argumentCaptor.capture());
+ assertThat(argumentCaptor.getValue().length, is(1));
+ assertThat(argumentCaptor.getValue()[0].getRelativeStartIndex(), is(0));
+ assertThat(argumentCaptor.getValue()[0].getRelativeEndIndex(), is(3));
+ }
+
+ @Test
+ public void shouldIdentifyTwiceIfTwiceInTheLineWithSpaces() {
+ readLine("[i] [b]", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(3), any(NodePatternIdentifier.InternalBlocks[].class));
+ verify(nodePatternIdentifier).found(eq(4), eq(7), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldNotIdentifyIfNotBalancedBracket() {
+ readLine("[i[]", textIdentifier, nodePatternIdentifier);
+ readLine("[[i]", textIdentifier, nodePatternIdentifier);
+ readLine("[[]", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyStartInvertedWithEndSymbol() {
+ readLine("][", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldIdentifyAfterFirstCloseBracket() {
+ readLine("][i][", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(1), eq(4), any(NodePatternIdentifier.InternalBlocks[].class));
+ verify(nodePatternIdentifier, times(1)).found(anyInt(), anyInt(), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketConfiguration() {
+ bracketIdentifierDoubleSymbol();
+
+ readLine("[i](b)", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(6), argumentCaptor.capture());
+ assertThat(argumentCaptor.getValue().length, is(2));
+ assertThat(argumentCaptor.getValue()[0].getRelativeStartIndex(), is(0));
+ assertThat(argumentCaptor.getValue()[0].getRelativeEndIndex(), is(3));
+ assertThat(argumentCaptor.getValue()[1].getRelativeStartIndex(), is(3));
+ assertThat(argumentCaptor.getValue()[1].getRelativeEndIndex(), is(6));
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketConfigurationTwiceInLine() {
+ bracketIdentifierDoubleSymbol();
+
+ readLine("[i](b) [d](e)", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(6), any(NodePatternIdentifier.InternalBlocks[].class));
+ verify(nodePatternIdentifier).found(eq(7), eq(13), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldIdentifyBracketWithBalancedBracketsForBothGroupsTwiceInline() {
+ bracketIdentifierDoubleSymbol();
+
+ readLine("[i](b[]()) [e[]](()b)", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(10), any(NodePatternIdentifier.InternalBlocks[].class));
+ verify(nodePatternIdentifier).found(eq(11), eq(21), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldConsiderNotBalancedWithSymbolOverlapBetweenGroups() {
+ bracketIdentifierDoubleSymbol();
+
+ readLine("[i(](b) [e](b)", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(7), any(NodePatternIdentifier.InternalBlocks[].class));
+ verify(nodePatternIdentifier).found(eq(8), eq(14), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketConfigurationInFirstCloseIfFinalNotBalanced() {
+ bracketIdentifierDoubleSymbol();
+
+ readLine("[i](b))", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(6), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldNotIdentifyIfNotBalancedSecondGroup() {
+ bracketIdentifierDoubleSymbol();
+
+ readLine("[i](b()", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyIfInvertedGroupOrder() {
+ bracketIdentifierDoubleSymbol();
+
+ readLine("(b)[i]", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyIfBetweenGroupsHasSomeExtraCharacter() {
+ bracketIdentifierDoubleSymbol();
+
+ readLine("[i] (b)", textIdentifier, nodePatternIdentifier);
+ readLine("[i]|(b)", textIdentifier, nodePatternIdentifier);
+ readLine("[i]$(b)", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ private void bracketIdentifierDoubleSymbol() {
+ textIdentifier = new BracketContainerIdentifier(
+ BracketContainerPattern.of(
+ OpenClose.of('[', ']'),
+ OpenClose.of('(', ')')));
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketConfigurationStartingByCharacter() {
+ bracketIdentifierDoubleSymbolStartWith();
+
+ readLine("", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(7), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketConfigurationByInternalGroups() {
+ bracketIdentifierDoubleSymbolStartWith();
+
+ readLine("", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(7), argumentCaptor.capture());
+ assertThat(argumentCaptor.getValue().length, is(2));
+ assertThat(argumentCaptor.getValue()[0].getRelativeStartIndex(), is(0));
+ assertThat(argumentCaptor.getValue()[0].getRelativeEndIndex(), is(4));
+ assertThat(argumentCaptor.getValue()[1].getRelativeStartIndex(), is(4));
+ assertThat(argumentCaptor.getValue()[1].getRelativeEndIndex(), is(7));
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketConfigurationWithInternalGroupsWithRelativePosition() {
+ bracketIdentifierDoubleSymbolStartWith();
+
+ readLine("some  foo", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(5), eq(18), argumentCaptor.capture());
+ assertThat(argumentCaptor.getValue().length, is(2));
+ assertThat(argumentCaptor.getValue()[0].getRelativeStartIndex(), is(0));
+ assertThat(argumentCaptor.getValue()[0].getRelativeEndIndex(), is(7));
+ assertThat(argumentCaptor.getValue()[1].getRelativeStartIndex(), is(7));
+ assertThat(argumentCaptor.getValue()[1].getRelativeEndIndex(), is(13));
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketConfigurationStartingByCharacterWithContentStartSymbol() {
+ bracketIdentifierDoubleSymbolStartWith();
+
+ readLine("", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(0), eq(11), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldNotIdentifyDoubleBracketConfigurationStartingByCharacterWhenHasSpace() {
+ bracketIdentifierDoubleSymbolStartWith();
+
+ readLine("! [i](b)", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketConfigurationStartingByCharacterWithContentStartSymbolInSecondOccurrence() {
+ bracketIdentifierDoubleSymbolStartWith();
+
+ readLine("! [i](b) ", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(9), eq(16), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketStartAfterSymbolStart() {
+ bracketIdentifierDoubleSymbolStartWith();
+
+ readLine("!", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(1), eq(8), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ @Test
+ public void shouldIdentifyDoubleBracketStartAfterSymbolAndSpaceStart() {
+ bracketIdentifierDoubleSymbolStartWith();
+
+ readLine("! ", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(eq(2), eq(9), any(NodePatternIdentifier.InternalBlocks[].class));
+ }
+
+ private void bracketIdentifierDoubleSymbolStartWith() {
+ textIdentifier = new BracketContainerIdentifier(
+ BracketContainerPattern.of(
+ '!',
+ OpenClose.of('[', ']'),
+ OpenClose.of('(', ')')));
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/identifier/RepeatableSymbolContainerIdentifierTest.java b/commonmark/src/test/java/org/commonmark/experimental/identifier/RepeatableSymbolContainerIdentifierTest.java
new file mode 100644
index 000000000..29362a18d
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/identifier/RepeatableSymbolContainerIdentifierTest.java
@@ -0,0 +1,129 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodePatternIdentifier;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.commonmark.experimental.identifier.TextIdentifierBaseTest.readLine;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+public class RepeatableSymbolContainerIdentifierTest {
+ private NodePatternIdentifier nodePatternIdentifier;
+ private RepeatableSymbolContainerIdentifier textIdentifier;
+
+ @Before
+ public void setUp() {
+ nodePatternIdentifier = mock(NodePatternIdentifier.class);
+ textIdentifier = new RepeatableSymbolContainerIdentifier(
+ RepeatableSymbolContainerPattern.of('~', 2));
+ }
+
+ @Test
+ public void shouldIdentifyMultipleCharacter() {
+ readLine("some ~~image.gif~~ text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 18, null);
+ }
+
+ @Test
+ public void shouldIdentifyMultipleCharacterWheContentSizeIsOne() {
+ readLine("~~i~~", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(0, 5, null);
+ }
+
+ @Test
+ public void shouldNotIdentifyMultipleCharacterIfContainerSymbolsIsSingle() {
+ readLine("some ~image.gif~ text", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyMultipleCharacterIfNotInSequenceFromTheBegin() {
+ readLine("some ~ ~image.gif~~ text", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyMultipleCharacterIfNotInSequenceInTheEnd() {
+ readLine("some ~~image.gif~ ~ text", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyIfEmptyTextBetweenCharacters() {
+ readLine("~~~~", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyIfSpaceAfterStartSymbols() {
+ readLine("~~ i~~", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyIfSpaceBeforeEndSymbols() {
+ readLine("~~i ~~", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldIdentifyAfterFirstSymbolWithSpaces() {
+ readLine("~~ ~~i~~", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(3, 8, null);
+ }
+
+ @Test
+ public void shouldIdentifyMultipleCharacterMultipleTimes() {
+ readLine("some ~~image.gif~~ and ~~second.jpg~~ text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 18, null);
+ verify(nodePatternIdentifier).found(23, 37, null);
+ }
+
+ @Test
+ public void shouldIdentifyMultipleCharacterWhenStartingInTheLineBegin() {
+ readLine("~~image.gif~~ text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(0, 13, null);
+ }
+
+ @Test
+ public void shouldIdentifyMultipleCharacterWhenEndingTheLine() {
+ readLine("text ~~image.gif~~", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 18, null);
+ }
+
+ @Test
+ public void shouldIdentifyMultipleCharacterBetweenWords() {
+ readLine("some~~image.gif~~thing", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(4, 17, null);
+ }
+
+ @Test
+ public void supportMultipleSymbolsAsSetup() {
+ textIdentifier = new RepeatableSymbolContainerIdentifier(
+ RepeatableSymbolContainerPattern.of('*', 4));
+
+ readLine("some ****image.gif**** thing **foo**", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 22, null);
+ verify(nodePatternIdentifier, times(1))
+ .found(anyInt(), anyInt(), eq((NodePatternIdentifier.InternalBlocks[]) null));
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/identifier/SingleSymbolContainerIdentifierTest.java b/commonmark/src/test/java/org/commonmark/experimental/identifier/SingleSymbolContainerIdentifierTest.java
new file mode 100644
index 000000000..9f7f93786
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/identifier/SingleSymbolContainerIdentifierTest.java
@@ -0,0 +1,130 @@
+package org.commonmark.experimental.identifier;
+
+
+import org.commonmark.experimental.NodePatternIdentifier;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.commonmark.experimental.identifier.TextIdentifierBaseTest.readLine;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+public class SingleSymbolContainerIdentifierTest {
+ private NodePatternIdentifier nodePatternIdentifier;
+ private SingleSymbolContainerIdentifier textIdentifier;
+
+ @Before
+ public void setUp() {
+ nodePatternIdentifier = mock(NodePatternIdentifier.class);
+ textIdentifier = new SingleSymbolContainerIdentifier(SingleSymbolContainerPattern.of('~'));
+ }
+
+ @Test
+ public void shouldIdentifySingleCharacter() {
+ readLine("some ~image.gif~ text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 16, null);
+ }
+
+ @Test
+ public void shouldNotIdentifySingleCharacterInTextWithoutOpenSymbol() {
+ readLine("some image.gif~ text", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifySingleCharacterInTextWithoutClosedCharacter() {
+ readLine("some ~image.gif text", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifySingleCharacterInTextWithoutAnyCharacter() {
+ readLine("some image.gif text", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldIdentifySingleCharacterBetweenSymbols() {
+ readLine("~i~", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(0, 3, null);
+ }
+
+ @Test
+ public void shouldNotIdentifySingleCharacterIfEmptyTextBetweenCharacters() {
+ readLine("~~", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldIdentifyContentAfterEmptySymbolWithSpace() {
+ readLine("~ ~some~", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(2, 8, null);
+ }
+
+ @Test
+ public void shouldNotIdentifyIfSpaceAfterStartSymbol() {
+ readLine("~ some~", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldNotIdentifyIfSpaceBeforeEndSymbol() {
+ readLine("~some ~", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+
+ @Test
+ public void shouldIdentifyContentAfterFirstSymbolWithSpace() {
+ readLine("~foo ~some~", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 11, null);
+ }
+
+ @Test
+ public void shouldIdentifySingleCharacterIfEmptyTextBetweenCharactersWhenMinSizeZero() {
+ textIdentifier = new SingleSymbolContainerIdentifier(
+ SingleSymbolContainerPattern.of('~', 0));
+ readLine("~~", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(0, 2, null);
+ }
+
+ @Test
+ public void shouldIdentifySingleCharacterMultipleTimes() {
+ readLine("some ~image.gif~ and ~second.jpg~ text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 16, null);
+ verify(nodePatternIdentifier).found(21, 33, null);
+ }
+
+ @Test
+ public void shouldIdentifySingleCharacterWhenStartingInTheLine() {
+ readLine("~image.gif~ text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(0, 11, null);
+ }
+
+ @Test
+ public void shouldIdentifySingleCharacterWhenEndingTheLine() {
+ readLine("text ~image.gif~", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 16, null);
+ }
+
+ @Test
+ public void shouldIdentifySingleCharacterBetweenWords() {
+ readLine("some~image.gif~thing", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(4, 15, null);
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/identifier/StartSymbolIdentifierTest.java b/commonmark/src/test/java/org/commonmark/experimental/identifier/StartSymbolIdentifierTest.java
new file mode 100644
index 000000000..91a0dac7a
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/identifier/StartSymbolIdentifierTest.java
@@ -0,0 +1,81 @@
+package org.commonmark.experimental.identifier;
+
+
+import org.commonmark.experimental.NodePatternIdentifier;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.commonmark.experimental.identifier.TextIdentifierBaseTest.readLine;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+public class StartSymbolIdentifierTest {
+ private NodePatternIdentifier nodePatternIdentifier;
+ private StartSymbolIdentifier textIdentifier;
+
+ @Before
+ public void setUp() {
+ nodePatternIdentifier = mock(NodePatternIdentifier.class);
+ textIdentifier = new StartSymbolIdentifier(new StartSymbolPattern('@', '/'));
+ }
+
+ @Test
+ public void shouldIdentifyStartCharacterUntilSpace() {
+ readLine("some @some text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 10, null);
+ }
+
+ @Test
+ public void shouldIdentifyStartCharacterWithSlashInTheText() {
+ readLine("some @github/support text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 20, null);
+ }
+
+ @Test
+ public void shouldIdentifyStartCharacterUntilSpaceBySpecialCharacter() {
+ textIdentifier = new StartSymbolIdentifier(new StartSymbolPattern('&'));
+
+ readLine("some &123 text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 9, null);
+ }
+
+ @Test
+ public void shouldIdentifyStartCharacterUntilSignal() {
+ readLine("some @some. Text after", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 10, null);
+ }
+
+ @Test
+ public void shouldIdentifyStartCharacterMultipleTimes() {
+ readLine("some @some and @any text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 10, null);
+ verify(nodePatternIdentifier).found(15, 19, null);
+ }
+
+ @Test
+ public void shouldIdentifyStartSymbolWhenStartingInTheLine() {
+ readLine("@some text", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(0, 5, null);
+ }
+
+ @Test
+ public void shouldIdentifyStartCharacterWhenEndingTheLine() {
+ readLine("text @some", textIdentifier, nodePatternIdentifier);
+
+ verify(nodePatternIdentifier).found(5, 10, null);
+ }
+
+ @Test
+ public void shouldNotIdentifyStartSymbolWhenCharacterBetweenWords() {
+ readLine("some@image.gifthing some", textIdentifier, nodePatternIdentifier);
+
+ verifyNoInteractions(nodePatternIdentifier);
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/experimental/identifier/TextIdentifierBaseTest.java b/commonmark/src/test/java/org/commonmark/experimental/identifier/TextIdentifierBaseTest.java
new file mode 100644
index 000000000..d32ae7564
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/experimental/identifier/TextIdentifierBaseTest.java
@@ -0,0 +1,18 @@
+package org.commonmark.experimental.identifier;
+
+import org.commonmark.experimental.NodePatternIdentifier;
+import org.commonmark.experimental.TextIdentifier;
+
+public class TextIdentifierBaseTest {
+ public static void readLine(String text, TextIdentifier textIdentifier, NodePatternIdentifier nodePatternIdentifier) {
+ int index = 0;
+
+ while (index < text.length()) {
+ char character = text.charAt(index);
+
+ textIdentifier.checkByCharacter(text, character, index, nodePatternIdentifier);
+
+ index++;
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index a16eb9913..9e5973e7f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -153,6 +153,12 @@
jmh-generator-annprocess
1.17.5
+
+ org.mockito
+ mockito-core
+ 3.3.3
+ test
+