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("![foo](/train.jpg)", 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 ![text](/url.png) *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("![i](b)", textIdentifier, nodePatternIdentifier); + + verify(nodePatternIdentifier).found(eq(0), eq(7), any(NodePatternIdentifier.InternalBlocks[].class)); + } + + @Test + public void shouldIdentifyDoubleBracketConfigurationByInternalGroups() { + bracketIdentifierDoubleSymbolStartWith(); + + readLine("![i](b)", 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 ![abcs](defg) 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("![!i!](!b!)", 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) ![a](c)", textIdentifier, nodePatternIdentifier); + + verify(nodePatternIdentifier).found(eq(9), eq(16), any(NodePatternIdentifier.InternalBlocks[].class)); + } + + @Test + public void shouldIdentifyDoubleBracketStartAfterSymbolStart() { + bracketIdentifierDoubleSymbolStartWith(); + + readLine("!![i](b)", textIdentifier, nodePatternIdentifier); + + verify(nodePatternIdentifier).found(eq(1), eq(8), any(NodePatternIdentifier.InternalBlocks[].class)); + } + + @Test + public void shouldIdentifyDoubleBracketStartAfterSymbolAndSpaceStart() { + bracketIdentifierDoubleSymbolStartWith(); + + readLine("! ![i](b)", 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 +