diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/ParseBenchmark.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/ParseBenchmark.java new file mode 100644 index 000000000..bac622f0f --- /dev/null +++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/ParseBenchmark.java @@ -0,0 +1,96 @@ +package org.commonmark.integration; + +import org.commonmark.node.Image; +import org.commonmark.node.Node; +import org.commonmark.parser.InlineParser; +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.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@BenchmarkMode(Mode.SingleShotTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Measurement(batchSize = 10000, iterations = 5) +@Warmup(batchSize = 10000, iterations = 5) +public class ParseBenchmark { + private static final Parser regularParser = Parser.builder().build(); + + private static final InlineParser.NodeExtension nodeExtensionWithRegexFirst = new InlineParser.NodeExtension() { + private final Pattern pattern = Pattern.compile("~(?[a-zA-Z]+)~(?<destination>[\\/a-zA-Z.]+)"); + + @Override + public List<InlineBreakdown> lookup(String inline) { + List<InlineBreakdown> nodesBreakDown = new ArrayList<>(); + + Matcher matcher = pattern.matcher(inline); + while (matcher.find()) { + nodesBreakDown.add(InlineBreakdown.of( + new Image(matcher.group("destination"), matcher.group("title")), + matcher.start(), + matcher.end())); + } + return nodesBreakDown; + } + }; + private static final InlineParser.NodeExtension nodeExtensionWithRegexSecond = new InlineParser.NodeExtension() { + private final Pattern pattern = Pattern.compile("(?<destination>[\\/a-zA-Z.]+)\\|(?<title>[a-zA-Z]+)"); + + @Override + public List<InlineBreakdown> lookup(String inline) { + List<InlineBreakdown> nodesBreakDown = new ArrayList<>(); + + Matcher matcher = pattern.matcher(inline); + while (matcher.find()) { + nodesBreakDown.add(InlineBreakdown.of( + new Image(matcher.group("destination"), matcher.group("title")), + matcher.start(), + matcher.end())); + } + return nodesBreakDown; + } + }; + private static final Parser parserOneExtension = Parser.builder() + .nodeExtension(nodeExtensionWithRegexFirst) + .build(); + + private static final Parser parserTwoExtensions = Parser.builder() + .nodeExtension(nodeExtensionWithRegexFirst) + .nodeExtension(nodeExtensionWithRegexSecond) + .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 parseImageRegularUsage() { + return regularParser.parse("Some text ![text](/url.png) another point ![text](/url.png)"); + } + + @Benchmark + public Node parseImageByNodeExtension() { + return parserOneExtension.parse("Some text ~image~/url.png another point ~image~/url.png"); + } + + @Benchmark + public Node parseImageByTwoNodeExtension() { + return parserTwoExtensions.parse("Some text /url.png|something another point ~image~/url.png"); + } +} diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java index d1b43a7b4..bc208b214 100644 --- a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java @@ -63,6 +63,7 @@ public class DocumentParser implements ParserState { private final List<BlockParserFactory> blockParserFactories; private final InlineParserFactory inlineParserFactory; private final List<DelimiterProcessor> delimiterProcessors; + private final List<InlineParser.NodeExtension> nodeExtensions; private final DocumentBlockParser documentBlockParser; private final Map<String, LinkReferenceDefinition> definitions = new LinkedHashMap<>(); @@ -71,10 +72,12 @@ public class DocumentParser implements ParserState { private Set<BlockParser> allBlockParsers = new LinkedHashSet<>(); public DocumentParser(List<BlockParserFactory> blockParserFactories, InlineParserFactory inlineParserFactory, - List<DelimiterProcessor> delimiterProcessors) { + List<DelimiterProcessor> delimiterProcessors, + List<InlineParser.NodeExtension> nodeExtensions) { this.blockParserFactories = blockParserFactories; this.inlineParserFactory = inlineParserFactory; this.delimiterProcessors = delimiterProcessors; + this.nodeExtensions = nodeExtensions; this.documentBlockParser = new DocumentBlockParser(); activateBlockParser(this.documentBlockParser); @@ -415,7 +418,7 @@ private void addDefinitionsFrom(ParagraphParser paragraphParser) { * Walk through a block & children recursively, parsing string content into inline content where appropriate. */ private void processInlines() { - InlineParserContextImpl context = new InlineParserContextImpl(delimiterProcessors, definitions); + InlineParserContextImpl context = new InlineParserContextImpl(delimiterProcessors, definitions, nodeExtensions); InlineParser inlineParser = inlineParserFactory.create(context); for (BlockParser blockParser : allBlockParsers) { diff --git a/commonmark/src/main/java/org/commonmark/internal/InlineParserContextImpl.java b/commonmark/src/main/java/org/commonmark/internal/InlineParserContextImpl.java index bff085ad8..ba84bbb59 100644 --- a/commonmark/src/main/java/org/commonmark/internal/InlineParserContextImpl.java +++ b/commonmark/src/main/java/org/commonmark/internal/InlineParserContextImpl.java @@ -1,6 +1,7 @@ package org.commonmark.internal; import org.commonmark.node.LinkReferenceDefinition; +import org.commonmark.parser.InlineParser; import org.commonmark.parser.InlineParserContext; import org.commonmark.parser.delimiter.DelimiterProcessor; @@ -11,11 +12,14 @@ public class InlineParserContextImpl implements InlineParserContext { private final List<DelimiterProcessor> delimiterProcessors; private final Map<String, LinkReferenceDefinition> linkReferenceDefinitions; + private final List<InlineParser.NodeExtension> nodeExtensions; public InlineParserContextImpl(List<DelimiterProcessor> delimiterProcessors, - Map<String, LinkReferenceDefinition> linkReferenceDefinitions) { + Map<String, LinkReferenceDefinition> linkReferenceDefinitions, + List<InlineParser.NodeExtension> nodeExtensions) { this.delimiterProcessors = delimiterProcessors; this.linkReferenceDefinitions = linkReferenceDefinitions; + this.nodeExtensions = nodeExtensions; } @Override @@ -27,4 +31,9 @@ public List<DelimiterProcessor> getCustomDelimiterProcessors() { public LinkReferenceDefinition getLinkReferenceDefinition(String label) { return linkReferenceDefinitions.get(label); } + + @Override + public List<InlineParser.NodeExtension> nodeExtensions() { + return nodeExtensions; + } } diff --git a/commonmark/src/main/java/org/commonmark/internal/InlineParserImpl.java b/commonmark/src/main/java/org/commonmark/internal/InlineParserImpl.java index dfb50149a..68f5e775a 100644 --- a/commonmark/src/main/java/org/commonmark/internal/InlineParserImpl.java +++ b/commonmark/src/main/java/org/commonmark/internal/InlineParserImpl.java @@ -6,12 +6,29 @@ import org.commonmark.internal.util.Html5Entities; import org.commonmark.internal.util.LinkScanner; import org.commonmark.internal.util.Parsing; -import org.commonmark.node.*; +import org.commonmark.node.Code; +import org.commonmark.node.HardLineBreak; +import org.commonmark.node.HtmlInline; +import org.commonmark.node.Image; +import org.commonmark.node.Link; +import org.commonmark.node.LinkReferenceDefinition; +import org.commonmark.node.Node; +import org.commonmark.node.SoftLineBreak; +import org.commonmark.node.Text; import org.commonmark.parser.InlineParser; import org.commonmark.parser.InlineParserContext; import org.commonmark.parser.delimiter.DelimiterProcessor; -import java.util.*; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -56,6 +73,7 @@ public class InlineParserImpl implements InlineParser { private final BitSet delimiterCharacters; private final Map<Character, DelimiterProcessor> delimiterProcessors; private final InlineParserContext context; + private final List<NodeExtension> nodeExtensions; private String input; private int index; @@ -77,6 +95,7 @@ public InlineParserImpl(InlineParserContext inlineParserContext) { this.specialCharacters = calculateSpecialCharacters(delimiterCharacters); this.context = inlineParserContext; + this.nodeExtensions = inlineParserContext.nodeExtensions(); } public static BitSet calculateDelimiterCharacters(Set<Character> characters) { @@ -148,9 +167,11 @@ private static void addDelimiterProcessorForChar(char delimiterChar, DelimiterPr public void parse(String content, Node block) { reset(content.trim()); + ArrayDeque<NodeExtension.InlineBreakdown> customNodes = new ArrayDeque<>(customNodesByExtensions()); + Node previous = null; while (true) { - Node node = parseInline(previous); + Node node = parseInline(previous, customNodes); previous = node; if (node != null) { block.appendChild(node); @@ -184,7 +205,22 @@ private Text text(String text) { * On success, return the new inline node. * On failure, return null. */ - private Node parseInline(Node previous) { + private Node parseInline(Node previous, ArrayDeque<NodeExtension.InlineBreakdown> customNodes) { + NodeExtension.InlineBreakdown inlineBreakdown = customNodes.isEmpty() + ? NodeExtension.InlineBreakdown.EMPTY + : customNodes.getFirst(); + int nextBeginIndexForCustomNode = inlineBreakdown.getBeginIndex(); + + if (inlineBreakdown.getBeginIndex() == index) { + while (index < inlineBreakdown.getEndIndex()) { + index++; + } + return customNodes.pollFirst().getNode(); + } else if (inlineBreakdown.getBeginIndex() < index) { + customNodes.pollFirst(); + nextBeginIndexForCustomNode = input.length(); + } + char c = peek(); if (c == '\0') { return null; @@ -225,7 +261,7 @@ private Node parseInline(Node previous) { DelimiterProcessor delimiterProcessor = delimiterProcessors.get(c); node = parseDelimiters(delimiterProcessor, c); } else { - node = parseString(); + node = parseString(Math.min(nextBeginIndexForCustomNode, input.length())); } break; } @@ -384,6 +420,25 @@ private Node parseDelimiters(DelimiterProcessor delimiterProcessor, char delimit return node; } + private List<NodeExtension.InlineBreakdown> customNodesByExtensions() { + List<NodeExtension.InlineBreakdown> nodes = new ArrayList<>(); + + for (NodeExtension nodeExtension : nodeExtensions) { + List<NodeExtension.InlineBreakdown> inlineBreakdowns = nodeExtension.lookup(input); + if (inlineBreakdowns != null) { + nodes.addAll(inlineBreakdowns); + } + } + + Collections.sort(nodes, new Comparator<NodeExtension.InlineBreakdown>() { + @Override + public int compare(NodeExtension.InlineBreakdown node1, NodeExtension.InlineBreakdown node2) { + return Integer.compare(node1.getBeginIndex(), node2.getBeginIndex()); + } + }); + return nodes; + } + /** * Add open bracket to delimiter stack and add a text node to block's children. */ @@ -652,10 +707,10 @@ private Node parseEntity() { /** * Parse a run of ordinary characters, or a single character with a special meaning in markdown, as a plain string. + * @param length */ - private Node parseString() { + private Node parseString(int length) { int begin = index; - int length = input.length(); while (index != length) { if (specialCharacters.get(input.charAt(index))) { break; diff --git a/commonmark/src/main/java/org/commonmark/parser/InlineParser.java b/commonmark/src/main/java/org/commonmark/parser/InlineParser.java index 492c3cc8a..d9980e0a8 100644 --- a/commonmark/src/main/java/org/commonmark/parser/InlineParser.java +++ b/commonmark/src/main/java/org/commonmark/parser/InlineParser.java @@ -2,6 +2,8 @@ import org.commonmark.node.Node; +import java.util.List; + /** * Parser for inline content (text, links, emphasized text, etc). */ @@ -12,4 +14,65 @@ public interface InlineParser { * @param node the node to append resulting nodes to (as children) */ void parse(String input, Node node); + + /** + * Extension for inline content + */ + interface NodeExtension { + /** + * The listing contains the all nodes that should be applied to this current line with the respective line positions + * + * @param input is the content to be parsed as custom nodes + * @return list of nodes with their location in the current line + */ + List<InlineBreakdown> lookup(String input); + + class InlineBreakdown { + public static final InlineBreakdown EMPTY = + new InlineBreakdown(null, Integer.MAX_VALUE, Integer.MAX_VALUE); + private final Node node; + private final int beginIndex; + private final int endIndex; + + private InlineBreakdown(Node node, int beginIndex, int endIndex) { + this.node = node; + this.beginIndex = beginIndex; + this.endIndex = endIndex; + } + + public Node getNode() { + return this.node; + } + + public int getBeginIndex() { + return beginIndex; + } + + public int getEndIndex() { + return endIndex; + } + + + /** + * Inline Object to provide the {@code node} and node's line position + * @param node such as the {@code Image}, {@code Link}, etc. + * @param beginIndex as String.subString usage, the beginning index, inclusive. + * @param endIndex as String.subString usage, the ending index, exclusive. + * @exception IllegalArgumentException if the {@code endIndex} < {@code beginIndex} + * @return {@code InlineBreakdown} built, + * if {@code beginIndex} == {@code endIndex} it will considered empty object + */ + public static InlineBreakdown of(Node node, int beginIndex, int endIndex) { + if (endIndex < beginIndex){ + throw new IllegalArgumentException("beginIndex "+ beginIndex +" must be greater then endIndex " + endIndex); + } + + if (beginIndex == endIndex) { + return EMPTY; + } + + return new InlineBreakdown(node, beginIndex, endIndex); + } + } + } } diff --git a/commonmark/src/main/java/org/commonmark/parser/InlineParserContext.java b/commonmark/src/main/java/org/commonmark/parser/InlineParserContext.java index 467742e2c..c97c48aa8 100644 --- a/commonmark/src/main/java/org/commonmark/parser/InlineParserContext.java +++ b/commonmark/src/main/java/org/commonmark/parser/InlineParserContext.java @@ -22,4 +22,6 @@ public interface InlineParserContext { * @return the definition if one exists, {@code null} otherwise */ LinkReferenceDefinition getLinkReferenceDefinition(String label); + + List<InlineParser.NodeExtension> nodeExtensions(); } diff --git a/commonmark/src/main/java/org/commonmark/parser/Parser.java b/commonmark/src/main/java/org/commonmark/parser/Parser.java index 5e15158ad..ba1351210 100644 --- a/commonmark/src/main/java/org/commonmark/parser/Parser.java +++ b/commonmark/src/main/java/org/commonmark/parser/Parser.java @@ -31,17 +31,20 @@ public class Parser { private final List<DelimiterProcessor> delimiterProcessors; private final InlineParserFactory inlineParserFactory; private final List<PostProcessor> postProcessors; + private final List<InlineParser.NodeExtension> nodeExtensions; private Parser(Builder builder) { this.blockParserFactories = DocumentParser.calculateBlockParserFactories(builder.blockParserFactories, builder.enabledBlockTypes); this.inlineParserFactory = builder.getInlineParserFactory(); this.postProcessors = builder.postProcessors; this.delimiterProcessors = builder.delimiterProcessors; + this.nodeExtensions = builder.nodeExtensions; // Try to construct an inline parser. Invalid configuration might result in an exception, which we want to // detect as soon as possible. this.inlineParserFactory.create(new InlineParserContextImpl(delimiterProcessors, - Collections.<String, LinkReferenceDefinition>emptyMap())); + Collections.<String, LinkReferenceDefinition>emptyMap(), + nodeExtensions)); } /** @@ -99,7 +102,7 @@ public Node parseReader(Reader input) throws IOException { } private DocumentParser createDocumentParser() { - return new DocumentParser(blockParserFactories, inlineParserFactory, delimiterProcessors); + return new DocumentParser(blockParserFactories, inlineParserFactory, delimiterProcessors, nodeExtensions); } private Node postProcess(Node document) { @@ -115,6 +118,7 @@ private Node postProcess(Node document) { public static class Builder { private final List<BlockParserFactory> blockParserFactories = new ArrayList<>(); private final List<DelimiterProcessor> delimiterProcessors = new ArrayList<>(); + private final List<InlineParser.NodeExtension> nodeExtensions = new ArrayList<>(); private final List<PostProcessor> postProcessors = new ArrayList<>(); private Set<Class<? extends Block>> enabledBlockTypes = DocumentParser.getDefaultBlockParserTypes(); private InlineParserFactory inlineParserFactory; @@ -257,6 +261,11 @@ public InlineParser create(InlineParserContext inlineParserContext) { } }; } + + public Builder nodeExtension(InlineParser.NodeExtension nodeExtension) { + this.nodeExtensions.add(nodeExtension); + return this; + } } /** diff --git a/commonmark/src/test/java/org/commonmark/test/ParserTest.java b/commonmark/src/test/java/org/commonmark/test/ParserTest.java index e058de378..b0e752f32 100644 --- a/commonmark/src/test/java/org/commonmark/test/ParserTest.java +++ b/commonmark/src/test/java/org/commonmark/test/ParserTest.java @@ -1,11 +1,24 @@ package org.commonmark.test; -import org.commonmark.node.*; +import org.commonmark.node.Block; +import org.commonmark.node.BulletList; +import org.commonmark.node.CustomBlock; +import org.commonmark.node.Heading; +import org.commonmark.node.Image; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; +import org.commonmark.node.Text; +import org.commonmark.node.ThematicBreak; import org.commonmark.parser.InlineParser; import org.commonmark.parser.InlineParserContext; import org.commonmark.parser.InlineParserFactory; import org.commonmark.parser.Parser; -import org.commonmark.parser.block.*; +import org.commonmark.parser.block.AbstractBlockParser; +import org.commonmark.parser.block.AbstractBlockParserFactory; +import org.commonmark.parser.block.BlockContinue; +import org.commonmark.parser.block.BlockStart; +import org.commonmark.parser.block.MatchedBlockParser; +import org.commonmark.parser.block.ParserState; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.TestResources; import org.junit.Test; @@ -13,7 +26,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -23,7 +36,13 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; -import static org.hamcrest.CoreMatchers.*; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; @@ -35,7 +54,7 @@ public void ioReaderTest() throws IOException { InputStream input1 = TestResources.getSpec().openStream(); Node document1; - try (InputStreamReader reader = new InputStreamReader(input1, Charset.forName("UTF-8"))) { + try (InputStreamReader reader = new InputStreamReader(input1, StandardCharsets.UTF_8)) { document1 = parser.parseReader(reader); } @@ -122,9 +141,298 @@ public InlineParser create(InlineParserContext inlineParserContext) { assertThat(parser.parse(input).getFirstChild().getFirstChild(), instanceOf(ThematicBreak.class)); } + @Test + public void convertLineToCustomNodeByNodeExtensionHandler() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(new Image(), 0, 8)); + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("some text"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Image.class)); + } + + @Test + public void convertLineByNodeExtensionLeftDefaultTextIfEmptyListFromExtension() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return emptyList(); + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("some text"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild()), equalTo("some text")); + } + + @Test + public void convertLineByNodeExtensionLeftDefaultTextIfNulListFromExtension() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return null; + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("some text"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild()), equalTo("some text")); + } + + @Test + public void convertLineByNodeExtensionWithCustomNodeInTheLineBeginOneSingleCharacter() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(new Image(), 0, 1)); + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("C"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Image.class)); + assertThat(document.getFirstChild().getLastChild(), instanceOf(Image.class)); + } + + @Test + public void convertLineByNodeExtensionWithCustomNodeInTheLineBegin() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(new Image(), 0, 18)); + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("CONVERTED_TO_IMAGE some"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Image.class)); + assertThat(document.getFirstChild().getLastChild(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getLastChild()), equalTo(" some")); + } + + @Test + public void convertLineByNodeExtensionWithCustomNodeInTheLineEnd() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(new Image(), 5, 23)); + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("some CONVERTED_TO_IMAGE"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild()), equalTo("some ")); + assertThat(document.getFirstChild().getFirstChild().getNext(), instanceOf(Image.class)); + } + + @Test + public void convertLineByNodeExtensionWithCustomNodeInTheMiddle() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(new Image(), 5, 23)); + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("some CONVERTED_TO_IMAGE second"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild()), equalTo("some ")); + assertThat(document.getFirstChild().getFirstChild().getNext(), instanceOf(Image.class)); + assertThat(document.getFirstChild().getLastChild(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getLastChild()), equalTo(" second")); + } + + @Test + public void convertLineByNodeExtensionWithMoreThenOneOccurrence() { + final Node image1 = new Image(); + final Node image2 = new Image(); + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + List<InlineBreakdown> inlinesBreakdown = new ArrayList<>(); + inlinesBreakdown.add(InlineBreakdown.of(image1, 5, 25)); + inlinesBreakdown.add(InlineBreakdown.of(image2, 33, 53)); + return inlinesBreakdown; + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("some CONVERTED_TO_IMAGE_1 second CONVERTED_TO_IMAGE_2 third"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild()), equalTo("some ")); + assertThat(document.getFirstChild().getFirstChild().getNext(), equalTo(image1)); + assertThat(document.getFirstChild().getFirstChild().getNext().getNext(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild().getNext().getNext()), + equalTo(" second ")); + assertThat(document.getFirstChild().getFirstChild().getNext().getNext().getNext(), equalTo(image2)); + assertThat(document.getFirstChild().getFirstChild().getNext().getNext().getNext().getNext(), + instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild().getNext().getNext().getNext().getNext()), + equalTo(" third")); + } + + @Test + public void convertLineByNodeExtensionWithMoreThenOneOccurrenceSortingByIndex() { + final Node image1 = new Image(); + final Node image2 = new Image(); + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + List<InlineBreakdown> inlinesBreakdown = new ArrayList<>(); + inlinesBreakdown.add(InlineBreakdown.of(image2, 33, 53)); + inlinesBreakdown.add(InlineBreakdown.of(image1, 5, 25)); + return inlinesBreakdown; + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("some CONVERTED_TO_IMAGE_1 second CONVERTED_TO_IMAGE_2 third"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild()), equalTo("some ")); + assertThat(document.getFirstChild().getFirstChild().getNext(), equalTo(image1)); + assertThat(document.getFirstChild().getFirstChild().getNext().getNext(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild().getNext().getNext()), + equalTo(" second ")); + assertThat(document.getFirstChild().getFirstChild().getNext().getNext().getNext(), equalTo(image2)); + assertThat(document.getFirstChild().getFirstChild().getNext().getNext().getNext().getNext(), + instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild().getNext().getNext().getNext().getNext()), + equalTo(" third")); + } + + @Test + public void convertLineByNodeExtensionShouldIgnoreIfBeginIndexEqualEndIndex() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(new Image(), 5, 5)); + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("some second"); + assertThat(getLiteral(document.getFirstChild().getFirstChild()), equalTo("some second")); + } + + @Test(expected = IllegalArgumentException.class) + public void convertLineByNodeExtensionShouldRaiseExceptionIfBeginIndexLessThenEndIndex() { + InlineParser.NodeExtension.InlineBreakdown.of(new Image(), 4, 3); + } + + @Test + public void convertLineByNodeExtensionShouldIgnoreTheNextComponentIfStartingBeforeThePreviousEndIndex() { + final Node image1 = new Image(); + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + List<InlineBreakdown> inlinesBreakdown = new ArrayList<>(); + inlinesBreakdown.add(InlineBreakdown.of(image1, 0, 3)); + inlinesBreakdown.add(InlineBreakdown.of(new Image(), 2, 5)); + return inlinesBreakdown; + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("foo jack some"); + assertThat(document.getFirstChild().getFirstChild(), equalTo(image1)); + assertThat(getLiteral(document.getFirstChild().getFirstChild().getNext()), + equalTo(" jack some")); + } + + @Test + public void convertLineByNodeExtensionShouldIgnoreTheNextComponentIfStartingEqualTheNextOneInTheList() { + final Node image1 = new Image(); + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + List<InlineBreakdown> inlinesBreakdown = new ArrayList<>(); + inlinesBreakdown.add(InlineBreakdown.of(image1, 0, 3)); + inlinesBreakdown.add(InlineBreakdown.of(new Image(), 0, 4)); + return inlinesBreakdown; + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + + Node document = parser.parse("foo jack some"); + assertThat(document.getFirstChild().getFirstChild(), equalTo(image1)); + assertThat(getLiteral(document.getFirstChild().getFirstChild().getNext()), + equalTo(" jack some")); + } + + @Test + public void convertLineByNodeExtensionWithMoreThenExtension() { + final Node image1 = new Image(); + final Node image2 = new Image(); + InlineParser.NodeExtension nodeExtensionOne = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(image2, 33, 53)); + } + }; + InlineParser.NodeExtension nodeExtensionTwo = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(image1, 5, 25)); + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtensionOne).nodeExtension(nodeExtensionTwo).build(); + + Node document = parser.parse("some CONVERTED_TO_IMAGE_1 second CONVERTED_TO_IMAGE_2 third"); + assertThat(document.getFirstChild().getFirstChild(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild()), equalTo("some ")); + assertThat(document.getFirstChild().getFirstChild().getNext(), equalTo(image1)); + assertThat(document.getFirstChild().getFirstChild().getNext().getNext(), instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild().getNext().getNext()), + equalTo(" second ")); + assertThat(document.getFirstChild().getFirstChild().getNext().getNext().getNext(), equalTo(image2)); + assertThat(document.getFirstChild().getFirstChild().getNext().getNext().getNext().getNext(), + instanceOf(Text.class)); + assertThat(getLiteral(document.getFirstChild().getFirstChild().getNext().getNext().getNext().getNext()), + equalTo(" third")); + } + + @Test + public void convertLineByNodeExtensionShouldIgnoreTheNextComponentIfStartingEqualTheNextOneInTheExtensions() { + final Node image1 = new Image(); + InlineParser.NodeExtension nodeExtensionOne = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(image1, 0, 3)); + } + }; + InlineParser.NodeExtension nodeExtensionTwo = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(new Image(), 0, 4)); + } + }; + + Parser parser = Parser.builder().nodeExtension(nodeExtensionOne).nodeExtension(nodeExtensionTwo).build(); + + Node document = parser.parse("foo jack some"); + assertThat(document.getFirstChild().getFirstChild(), equalTo(image1)); + assertThat(getLiteral(document.getFirstChild().getFirstChild().getNext()), + equalTo(" jack some")); + } + @Test public void threading() throws Exception { - final Parser parser = Parser.builder().build(); + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(new Image("www.image", "some"), 5, 6)); + } + }; + + final Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); final String spec = TestResources.readAsString(TestResources.getSpec()); HtmlRenderer renderer = HtmlRenderer.builder().build(); @@ -136,7 +444,7 @@ public void threading() throws Exception { for (int i = 0; i < 40; i++) { Future<Node> future = executorService.submit(new Callable<Node>() { @Override - public Node call() throws Exception { + public Node call() { return parser.parse(spec); } }); @@ -154,7 +462,14 @@ private String firstText(Node n) { assertThat(n, notNullValue()); n = n.getFirstChild(); } - return ((Text) n).getLiteral(); + return getLiteral(n); + } + + private String getLiteral(Node node) { + if (node instanceof Text) { + return ((Text)node).getLiteral(); + } + return node.toString(); } private static class DashBlock extends CustomBlock { diff --git a/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java b/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java index 9ff646630..c0f029245 100644 --- a/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java +++ b/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java @@ -1,6 +1,7 @@ package org.commonmark.test; import org.commonmark.node.*; +import org.commonmark.parser.InlineParser; import org.commonmark.parser.Parser; import org.commonmark.renderer.NodeRenderer; import org.commonmark.renderer.html.*; @@ -11,11 +12,19 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import static java.util.Collections.singletonList; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; public class UsageExampleTest { @@ -62,6 +71,63 @@ public AttributeProvider create(AttributeProviderContext context) { renderer.render(document)); } + @Test + public void customizeNodeImageFromDifferentTextSyntax() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + @Override + public List<InlineBreakdown> lookup(String inline) { + return singletonList(InlineBreakdown.of(new Image("/url.png", "image"), 10, 25)); + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + HtmlRenderer renderer = HtmlRenderer.builder() + .attributeProviderFactory(new AttributeProviderFactory() { + public AttributeProvider create(AttributeProviderContext context) { + return new ImageAttributeProvider(); + } + }) + .build(); + + Node document = parser.parse("Some text ~image~/url.png"); + assertEquals("<p>Some text <img src=\"/url.png\" alt=\"\" title=\"image\" class=\"border\" /></p>\n", + renderer.render(document)); + } + + @Test + public void customizeNodesFromDifferentTextSyntaxUsingRegex() { + InlineParser.NodeExtension nodeExtension = new InlineParser.NodeExtension() { + Pattern pattern = Pattern.compile("~(?<title>[a-zA-Z]+)~(?<destination>[\\/a-zA-Z.]+)"); + + @Override + public List<InlineBreakdown> lookup(String inline) { + List<InlineBreakdown> nodesBreakDown = new ArrayList<>(); + + Matcher matcher = pattern.matcher(inline); + while (matcher.find()) { + nodesBreakDown.add(InlineBreakdown.of( + new Image(matcher.group("destination"), matcher.group("title")), + matcher.start(), + matcher.end())); + } + return nodesBreakDown; + } + }; + Parser parser = Parser.builder().nodeExtension(nodeExtension).build(); + HtmlRenderer renderer = HtmlRenderer.builder() + .attributeProviderFactory(new AttributeProviderFactory() { + public AttributeProvider create(AttributeProviderContext context) { + return new ImageAttributeProvider(); + } + }) + .build(); + + Node document = parser.parse("Some text ~image~/url.png anything ~SOME~/other.jpg the third one ~some~/other"); + assertEquals("<p>Some text <img src=\"/url.png\" alt=\"\" title=\"image\" class=\"border\" /> " + + "anything <img src=\"/other.jpg\" alt=\"\" title=\"SOME\" class=\"border\" /> " + + "the third one <img src=\"/other\" alt=\"\" title=\"some\" class=\"border\" /></p>\n", + renderer.render(document)); + } + @Test public void customizeRendering() { Parser parser = Parser.builder().build();