diff --git a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughDelimiterProcessor.java b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughDelimiterProcessor.java index 7d54eedf2..de2073487 100644 --- a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughDelimiterProcessor.java +++ b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughDelimiterProcessor.java @@ -53,4 +53,10 @@ public int process(DelimiterRun openingRun, DelimiterRun closingRun) { return 0; } } + + + @Override + public int process(DelimiterRun openingRun, DelimiterRun closingRun, String prefix) { + return process(openingRun, closingRun); + } } diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java index a8eedb7a6..b978ef95b 100644 --- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java +++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java @@ -45,7 +45,7 @@ public BlockContinue tryContinue(ParserState state) { @Override public void addLine(SourceLine line) { - rowLines.add(line); + rowLines.add(line.getLiteralLine()); } @Override diff --git a/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesDelimiterProcessor.java b/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesDelimiterProcessor.java index a584948e3..9f3b6e9bb 100644 --- a/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesDelimiterProcessor.java +++ b/commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesDelimiterProcessor.java @@ -85,4 +85,9 @@ public int process(DelimiterRun openingRun, DelimiterRun closingRun) { return 1; } + + @Override + public int process(DelimiterRun openingRun, DelimiterRun closingRun, String prefix) { + return process(openingRun, closingRun); + } } diff --git a/commonmark-ext-ins/src/main/java/org/commonmark/ext/ins/internal/InsDelimiterProcessor.java b/commonmark-ext-ins/src/main/java/org/commonmark/ext/ins/internal/InsDelimiterProcessor.java index b0bfb4c6e..b112f510f 100644 --- a/commonmark-ext-ins/src/main/java/org/commonmark/ext/ins/internal/InsDelimiterProcessor.java +++ b/commonmark-ext-ins/src/main/java/org/commonmark/ext/ins/internal/InsDelimiterProcessor.java @@ -53,4 +53,9 @@ public int process(DelimiterRun openingRun, DelimiterRun closingRun) { return 0; } } + + @Override + public int process(DelimiterRun openingRun, DelimiterRun closingRun, String prefix) { + return process(openingRun, closingRun); + } } diff --git a/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java b/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java index 00cdbc11e..901d6a04e 100644 --- a/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java @@ -9,6 +9,16 @@ public class BlockQuoteParser extends AbstractBlockParser { private final BlockQuote block = new BlockQuote(); + // Preserve original default constructor by explicitly defining one + public BlockQuoteParser() { + super(); + } + + public BlockQuoteParser(String preMarkerWhitespace, String postMarkerWhitespace) { + block.setPreMarkerWhitespace(preMarkerWhitespace); + block.setPostMarkerWhitespace(postMarkerWhitespace); + } + @Override public boolean isContainer() { return true; @@ -47,13 +57,24 @@ private static boolean isMarker(ParserState state, int index) { public static class Factory extends AbstractBlockParserFactory { public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { int nextNonSpace = state.getNextNonSpaceIndex(); + String preMarkerWhitespace = ""; + + if(nextNonSpace > 0) { + preMarkerWhitespace = Parsing.collectWhitespaceBackwards(state.getLine().getContent(), nextNonSpace - 1, 0); + } + if (isMarker(state, nextNonSpace)) { int newColumn = state.getColumn() + state.getIndent() + 1; + // optional following space or tab + String postMarkerWhitespace = ""; + if (Parsing.isSpaceOrTab(state.getLine().getContent(), nextNonSpace + 1)) { + postMarkerWhitespace = Parsing.collectWhitespace(state.getLine().getContent(), nextNonSpace + 1, state.getLine().getContent().length() - 1); newColumn++; } - return BlockStart.of(new BlockQuoteParser()).atColumn(newColumn); + + return BlockStart.of(new BlockQuoteParser(preMarkerWhitespace, postMarkerWhitespace)).atColumn(newColumn); } else { return BlockStart.none(); } diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java index ed8ae7412..210843fca 100644 --- a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java @@ -1,15 +1,43 @@ package org.commonmark.internal; -import org.commonmark.internal.util.Parsing; -import org.commonmark.node.*; -import org.commonmark.parser.*; -import org.commonmark.parser.block.*; -import org.commonmark.parser.delimiter.DelimiterProcessor; - import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.commonmark.internal.inline.BlankLineParser; +import org.commonmark.internal.util.Parsing; +import org.commonmark.node.Block; +import org.commonmark.node.BlockQuote; +import org.commonmark.node.Document; +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.Heading; +import org.commonmark.node.HtmlBlock; +import org.commonmark.node.IndentedCodeBlock; +import org.commonmark.node.LinkReferenceDefinition; +import org.commonmark.node.ListBlock; +import org.commonmark.node.Paragraph; +import org.commonmark.node.SourceSpan; +import org.commonmark.node.ThematicBreak; +import org.commonmark.parser.IncludeSourceSpans; +import org.commonmark.parser.InlineParser; +import org.commonmark.parser.InlineParserFactory; +import org.commonmark.parser.SourceLine; +import org.commonmark.parser.SourceLines; +import org.commonmark.parser.block.BlockContinue; +import org.commonmark.parser.block.BlockParser; +import org.commonmark.parser.block.BlockParserFactory; +import org.commonmark.parser.block.BlockStart; +import org.commonmark.parser.block.MatchedBlockParser; +import org.commonmark.parser.block.ParserState; +import org.commonmark.parser.delimiter.DelimiterProcessor; public class DocumentParser implements ParserState { @@ -275,13 +303,19 @@ private void parseLine(CharSequence ln) { addLine(); } else { - // finalize any blocks not matched if (unmatchedBlocks > 0) { closeBlockParsers(unmatchedBlocks); } if (!blockParser.isContainer()) { + // Capture any blank lines within a separate node for roundtrip purposes + // BlankLine nodes do _not_ interrupt the current node type's processor + if(isBlank()) { + BlankLineParser blankLineParser = new BlankLineParser(ln.toString()); + addChild(new OpenBlockParser(blankLineParser, lastIndex)); + } + addLine(); } else if (!isBlank()) { // create paragraph container for line @@ -297,6 +331,13 @@ private void parseLine(CharSequence ln) { // // The first line does not start a paragraph yet, but we still want to record source positions. addSourceSpans(); + + // Capture any blank lines within a separate node for roundtrip purposes + // BlankLine nodes do _not_ interrupt the current node type's processor + if(isBlank()) { + BlankLineParser blankLineParser = new BlankLineParser(ln.toString()); + addChild(new OpenBlockParser(blankLineParser, lastIndex)); + } } } } @@ -403,18 +444,23 @@ private void addLine() { } sb.append(rest); content = sb.toString(); - } else if (index == 0) { - content = line.getContent(); } else { - content = line.getContent().subSequence(index, line.getContent().length()); + content = line.getContent().toString(); } + SourceSpan sourceSpan = null; if (includeSourceSpans == IncludeSourceSpans.BLOCKS_AND_INLINES) { // Note that if we're in a partially-consumed tab, the length here corresponds to the content but not to the // actual source length. That sounds like a problem, but I haven't found a test case where it matters (yet). - sourceSpan = SourceSpan.of(lineIndex, index, content.length()); + sourceSpan = SourceSpan.of(lineIndex, index, line.getLiteralLine().getContent().length()); } - getActiveBlockParser().addLine(SourceLine.of(content, sourceSpan)); + + if(!columnIsInTab) { + getActiveBlockParser().addLine(SourceLine.of(content, sourceSpan, index)); + }else { + getActiveBlockParser().addLine(SourceLine.of(content, sourceSpan, 0)); + } + addSourceSpans(); } @@ -425,6 +471,11 @@ private void addSourceSpans() { OpenBlockParser openBlockParser = openBlockParsers.get(i); int blockIndex = openBlockParser.sourceIndex; int length = line.getContent().length() - blockIndex; + + if(line.getLiteralIndex() != 0) { + length = line.getLiteralLine().getContent().length() - blockIndex; + } + if (length != 0) { openBlockParser.blockParser.addSourceSpan(SourceSpan.of(lineIndex, blockIndex, length)); } @@ -481,12 +532,21 @@ private void processInlines() { * its parent, and so on until we find a block that can accept children. */ private void addChild(OpenBlockParser openBlockParser) { - while (!getActiveBlockParser().canContain(openBlockParser.blockParser.getBlock())) { - closeBlockParsers(1); + // BlankLine nodes do _not_ interrupt the current node type's processor, as + // they are non-standard CommonMark nodes meant only for roundtrip processing + if(!(openBlockParser.blockParser instanceof BlankLineParser)) { + while (!getActiveBlockParser().canContain(openBlockParser.blockParser.getBlock())) { + closeBlockParsers(1); + } } getActiveBlockParser().getBlock().appendChild(openBlockParser.blockParser.getBlock()); - activateBlockParser(openBlockParser); + + // BlankLine nodes do _not_ become the active parser, as this would interrupt the + // parser before it + if(!(openBlockParser.blockParser instanceof BlankLineParser)) { + activateBlockParser(openBlockParser); + } } private void activateBlockParser(OpenBlockParser openBlockParser) { @@ -520,6 +580,16 @@ private Block prepareActiveBlockParserForReplacement() { private Document finalizeAndProcess() { closeBlockParsers(openBlockParsers.size()); processInlines(); + Document docNode = documentBlockParser.getBlock(); + String whitespaceEndOfDocument = ""; + + // CommonMark test cases end with newlines, indicating that this should be standard + // in roundtrip scenarios. If a post-document whitespace doesn't exist, add it. + if(whitespaceEndOfDocument.isEmpty()) { + whitespaceEndOfDocument = "\n"; + } + + docNode.setEndOfDocumentWhitespace(whitespaceEndOfDocument); return documentBlockParser.getBlock(); } diff --git a/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java b/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java index 2d7d2c0c9..9f9982592 100644 --- a/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java @@ -4,9 +4,12 @@ import org.commonmark.node.Block; import org.commonmark.node.FencedCodeBlock; import org.commonmark.parser.SourceLine; -import org.commonmark.parser.block.*; - -import static org.commonmark.internal.util.Escaping.unescapeString; +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; public class FencedCodeBlockParser extends AbstractBlockParser { @@ -14,11 +17,15 @@ public class FencedCodeBlockParser extends AbstractBlockParser { private String firstLine; private StringBuilder otherLines = new StringBuilder(); + private StringBuilder rawLines = new StringBuilder(); public FencedCodeBlockParser(char fenceChar, int fenceLength, int fenceIndent) { block.setFenceChar(fenceChar); - block.setFenceLength(fenceLength); - block.setFenceIndent(fenceIndent); + block.setStartFenceLength(fenceLength); + + // Fenced code blocks can't be indented by newlines or tabs, so it's safe to assume + // spaces for the indentation + block.setPreStartFenceWhitespace(Parsing.generateSpaces(fenceIndent)); } @Override @@ -32,11 +39,20 @@ public BlockContinue tryContinue(ParserState state) { int newIndex = state.getIndex(); CharSequence line = state.getLine().getContent(); if (state.getIndent() < Parsing.CODE_BLOCK_INDENT && nextNonSpace < line.length() && line.charAt(nextNonSpace) == block.getFenceChar() && isClosing(line, nextNonSpace)) { + // Capture whitespace before closing fence (if any) for roundtrip purposes + if(nextNonSpace > 0) { + block.setPreEndFenceWhitespace(Parsing.collectWhitespace(line, 0, nextNonSpace)); + } + // closing fence - we're at end of line, so we can finalize now return BlockContinue.finished(); } else { + // Capture line before optional spaces are removed + rawLines.append(line.toString()); + rawLines.append('\n'); + // skip optional spaces of fence indent - int i = block.getFenceIndent(); + int i = block.getStartFenceIndent(); int length = line.length(); while (i > 0 && newIndex < length && line.charAt(newIndex) == ' ') { newIndex++; @@ -49,9 +65,24 @@ public BlockContinue tryContinue(ParserState state) { @Override public void addLine(SourceLine line) { if (firstLine == null) { - firstLine = line.getContent().toString(); + + // Raw line is the same as literal line, so just use it + if(line.getLiteralIndex() == 0) { + firstLine = line.getContent().toString(); + } + // Raw line differs from literal line, so preserve literal line + else { + firstLine = line.substring(line.getLiteralIndex(), line.getContent().length()).getContent().toString(); + } } else { - otherLines.append(line.getContent()); + // Raw line is the same as literal line, so just use it + if(line.getLiteralIndex() == 0) { + otherLines.append(line.getContent()); + } + // Raw line differs from literal line, so preserve literal line + else { + otherLines.append(line.getLiteralLine().getContent()); + } otherLines.append('\n'); } } @@ -59,7 +90,22 @@ public void addLine(SourceLine line) { @Override public void closeBlock() { // first line becomes info string - block.setInfo(unescapeString(firstLine.trim())); + // It is not trimmed or escaped here to allow roundtrip processing + block.setInfo(firstLine); + + // Remove final newline in raw content, if it exists + if(rawLines != null && rawLines.length() != 0) { + if(rawLines.charAt(rawLines.length() - 1) == '\n') { + rawLines.deleteCharAt(rawLines.length() - 1); + } + } + + // Capture raw content (no indentation removed) between end of info string + // and beginning of end fence + block.setRaw(rawLines.toString()); + + // Capture content (without indentation) between end of info string + // and beginning of end fence block.setLiteral(otherLines.toString()); } @@ -72,10 +118,14 @@ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockPar return BlockStart.none(); } + if(indent == 0) { + indent = Parsing.collectWhitespace(state.getLine().getContent(), 0, state.getLine().getContent().length()).length(); + } + int nextNonSpace = state.getNextNonSpaceIndex(); FencedCodeBlockParser blockParser = checkOpener(state.getLine().getContent(), nextNonSpace, indent); if (blockParser != null) { - return BlockStart.of(blockParser).atIndex(nextNonSpace + blockParser.block.getFenceLength()); + return BlockStart.of(blockParser).atIndex(nextNonSpace + blockParser.block.getStartFenceLength()); } else { return BlockStart.none(); } @@ -120,13 +170,22 @@ private static FencedCodeBlockParser checkOpener(CharSequence line, int index, i // code fence. private boolean isClosing(CharSequence line, int index) { char fenceChar = block.getFenceChar(); - int fenceLength = block.getFenceLength(); + int fenceLength = block.getStartFenceLength(); int fences = Parsing.skip(fenceChar, line, index, line.length()) - index; if (fences < fenceLength) { return false; + }else { + block.setEndFenceLength(fences); } + // spec: The closing code fence [...] may be followed only by spaces, which are ignored. int after = Parsing.skipSpaceTab(line, index + fences, line.length()); + + // Capture post-fence spaces (if any) for roundtrip purposes + if(after > 0 && after != (index + fences)) { + block.setPostBlockWhitespace(Parsing.collectWhitespaceBackwards(line, line.length() - 1, 0)); + } + return after == line.length(); } } diff --git a/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java b/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java index 81c60d0d1..fd3b642bc 100644 --- a/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java @@ -8,7 +8,12 @@ import org.commonmark.parser.InlineParser; import org.commonmark.parser.SourceLine; import org.commonmark.parser.SourceLines; -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; public class HeadingParser extends AbstractBlockParser { @@ -17,8 +22,19 @@ public class HeadingParser extends AbstractBlockParser { public HeadingParser(int level, SourceLines content) { block.setLevel(level); + block.setSymbolType('#'); this.content = content; } + + public HeadingParser(int level, SourceLines content, char symbolType, int numEndingSymbols, String whitespacePreBlock, String whitespacePreContent, String whitespacePostContent, String whitespacePostBlock) { + this(level, content); + block.setSymbolType(symbolType); + block.setNumEndingSymbol(numEndingSymbols); + block.setPreBlockWhitespace(whitespacePreBlock); + block.setPreContentWhitespace(whitespacePreContent); + block.setPostContentWhitespace(whitespacePostContent); + block.setPostBlockWhitespace(whitespacePostBlock); + } @Override public Block getBlock() { @@ -46,8 +62,24 @@ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockPar SourceLine line = state.getLine(); int nextNonSpace = state.getNextNonSpaceIndex(); + + String preBlockWhitespace = ""; + if (line.getContent().charAt(nextNonSpace) == '#') { - HeadingParser atxHeading = getAtxHeading(line.substring(nextNonSpace, line.getContent().length())); + + // Account for block-in-a-block instances during roundtrips, like Heading inside a List + if(nextNonSpace > 0) { + preBlockWhitespace = Parsing.collectWhitespaceBackwards(line.getContent(), nextNonSpace - 1, 0); + + // If these values don't match, it's a block-in-a-block scenario because + // there's another character before the whitespace + if(preBlockWhitespace.length() != nextNonSpace) { + preBlockWhitespace = ""; + } + } + + HeadingParser atxHeading = getAtxHeading(line.substring(nextNonSpace - preBlockWhitespace.length(), line.getContent().length()), nextNonSpace); + if (atxHeading != null) { return BlockStart.of(atxHeading).atIndex(line.getContent().length()); } @@ -55,9 +87,42 @@ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockPar int setextHeadingLevel = getSetextHeadingLevel(line.getContent(), nextNonSpace); if (setextHeadingLevel > 0) { + int numEndingSymbols = line.getContent().toString().trim().length(); + + // AST: Setext headings have a slightly different twist on + // whitespace, see the
Headingclass for details + String postContentWhitespace = Parsing.collectWhitespace(line.getContent(), 0, line.getContent().length()); + String postBlockWhitespace = Parsing.collectWhitespaceBackwards(line.getContent(), line.getContent().length() - 1, 0); + + // If the setext had valid content, the current matched block + // parser will be the paragraph parser. Setext headings use + // Text (not Paragraph) in the final output, but it's still + // possible to gather the right whitespace from Paragraph. + boolean isMatchedParagraph = matchedBlockParser.getMatchedBlockParser() instanceof ParagraphParser; + + if(isMatchedParagraph) { + SourceLines rawLines = ((ParagraphParser)matchedBlockParser.getMatchedBlockParser()).getRawParagraphLines(); + + // In some instances, the paragraph object exists but has not yet been populated. + // However, the link reference definition parser will still have the paragraph's + // lines ready for capture. If no whitespace is available yet, capture any initial + // whitespace in the first paragraph line. + if(preBlockWhitespace.isEmpty() && !rawLines.isEmpty()) { + CharSequence rawParagraphSequence = rawLines.getLines().get(0).getContent(); + preBlockWhitespace = Parsing.collectWhitespace(rawParagraphSequence.toString(), 0, rawParagraphSequence.length()); + } + } + + char symbolType; + if(setextHeadingLevel == 1) { + symbolType = '='; + }else { + symbolType = '-'; + } + SourceLines paragraph = matchedBlockParser.getParagraphLines(); if (!paragraph.isEmpty()) { - return BlockStart.of(new HeadingParser(setextHeadingLevel, paragraph)) + return BlockStart.of(new HeadingParser(setextHeadingLevel, paragraph, symbolType, numEndingSymbols, preBlockWhitespace, "", postContentWhitespace, postBlockWhitespace)) .atIndex(line.getContent().length()) .replaceActiveBlockParser(); } @@ -72,7 +137,14 @@ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockPar // sequence of # characters must be followed by a space or by the end of line. The optional closing sequence of #s // must be preceded by a space and may be followed by spaces only. private static HeadingParser getAtxHeading(SourceLine line) { + return getAtxHeading(line, 0); + } + + private static HeadingParser getAtxHeading(SourceLine line, int nextNonSpace) { + String preBlockWhitespace = ""; + Scanner scanner = Scanner.of(SourceLines.of(line)); + preBlockWhitespace = scanner.whitespaceAsString(); int level = scanner.matchMultiple('#'); if (level == 0 || level > 6) { @@ -89,23 +161,28 @@ private static HeadingParser getAtxHeading(SourceLine line) { return null; } - scanner.whitespace(); + String preContentWhitespace = scanner.whitespaceAsString(); Position start = scanner.position(); Position end = start; boolean hashCanEnd = true; + int numEndingSymbols = 0; + StringBuilder postContentWhitespace = new StringBuilder(); + String postBlockWhitespace = ""; while (scanner.hasNext()) { char c = scanner.peek(); switch (c) { case '#': if (hashCanEnd) { - scanner.matchMultiple('#'); - int whitespace = scanner.whitespace(); + numEndingSymbols = scanner.matchMultiple('#'); + String whitespace = scanner.whitespaceAsString(); // If there's other characters, the hashes and spaces were part of the heading if (scanner.hasNext()) { + numEndingSymbols = 0; + postContentWhitespace.setLength(0); end = scanner.position(); } - hashCanEnd = whitespace > 0; + hashCanEnd = whitespace.length() > 0; } else { scanner.next(); end = scanner.position(); @@ -114,21 +191,29 @@ private static HeadingParser getAtxHeading(SourceLine line) { case ' ': case '\t': hashCanEnd = true; + postContentWhitespace.append(c); scanner.next(); break; default: hashCanEnd = false; + postContentWhitespace.setLength(0); scanner.next(); end = scanner.position(); } } + + if(numEndingSymbols > 0) { + int beforePostBlockWhitespaceIndex = Parsing.skipSpaceTabBackwards(line.getContent(), line.getContent().length() - 1, 0); + postBlockWhitespace = line.getContent().subSequence(beforePostBlockWhitespaceIndex + 1, line.getContent().length()).toString(); + } SourceLines source = scanner.getSource(start, end); String content = source.getContent(); - if (content.isEmpty()) { - return new HeadingParser(level, SourceLines.empty()); + + if(content.isEmpty()) { + return new HeadingParser(level, SourceLines.empty(), '#', numEndingSymbols, preBlockWhitespace, preContentWhitespace, postContentWhitespace.toString(), postBlockWhitespace); } - return new HeadingParser(level, source); + return new HeadingParser(level, source, '#', numEndingSymbols, preBlockWhitespace, preContentWhitespace, postContentWhitespace.toString(), postBlockWhitespace); } // spec: A setext heading underline is a sequence of = characters or a sequence of - characters, with no more than diff --git a/commonmark/src/main/java/org/commonmark/internal/HtmlBlockParser.java b/commonmark/src/main/java/org/commonmark/internal/HtmlBlockParser.java index 0e2050567..2dc964217 100644 --- a/commonmark/src/main/java/org/commonmark/internal/HtmlBlockParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/HtmlBlockParser.java @@ -1,13 +1,18 @@ package org.commonmark.internal; +import java.util.regex.Pattern; + import org.commonmark.internal.util.Parsing; import org.commonmark.node.Block; import org.commonmark.node.HtmlBlock; import org.commonmark.node.Paragraph; import org.commonmark.parser.SourceLine; -import org.commonmark.parser.block.*; - -import java.util.regex.Pattern; +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; public class HtmlBlockParser extends AbstractBlockParser { @@ -64,6 +69,7 @@ public class HtmlBlockParser extends AbstractBlockParser { private boolean finished = false; private BlockContent content = new BlockContent(); + private BlockContent rawContent = new BlockContent(); private HtmlBlockParser(Pattern closingPattern) { this.closingPattern = closingPattern; @@ -90,7 +96,22 @@ public BlockContinue tryContinue(ParserState state) { @Override public void addLine(SourceLine line) { - content.add(line.getContent()); + // Raw line and literal line are the same + if(line.getLiteralIndex() == 0) { + content.add(line.getContent()); + rawContent.add(line.getContent()); + } + // Raw line and literal line are different + else { + content.add(line.getLiteralLine().getContent()); + + // Use the literal line for anything which may be a block quote + if(line != null && line.getContent().toString().contains(">")) { + rawContent.add(line.getContent()); + }else { + rawContent.add(line.getLiteralLine().getContent()); + } + } if (closingPattern != null && closingPattern.matcher(line.getContent()).find()) { finished = true; @@ -100,7 +121,9 @@ public void addLine(SourceLine line) { @Override public void closeBlock() { block.setLiteral(content.getString()); + block.setRaw(rawContent.getString()); content = null; + rawContent = null; } public static class Factory extends AbstractBlockParserFactory { diff --git a/commonmark/src/main/java/org/commonmark/internal/IndentedCodeBlockParser.java b/commonmark/src/main/java/org/commonmark/internal/IndentedCodeBlockParser.java index af74a587c..65f34e665 100644 --- a/commonmark/src/main/java/org/commonmark/internal/IndentedCodeBlockParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/IndentedCodeBlockParser.java @@ -1,20 +1,35 @@ package org.commonmark.internal; +import java.util.ArrayList; +import java.util.List; + import org.commonmark.internal.util.Parsing; import org.commonmark.node.Block; import org.commonmark.node.IndentedCodeBlock; import org.commonmark.node.Paragraph; import org.commonmark.parser.SourceLine; -import org.commonmark.parser.block.*; - -import java.util.ArrayList; -import java.util.List; +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; public class IndentedCodeBlockParser extends AbstractBlockParser { private final IndentedCodeBlock block = new IndentedCodeBlock(); private final List
ListBlockParser+ private boolean firstLineBlank = false; + public ListItemParser(int contentIndent) { this.contentIndent = contentIndent; + block.setRawNumber(""); + } + + public ListItemParser(int contentIndent, String currentRawNumber, boolean firstLineBlank, String whitespacePreMarker, String whitespacePostMarker) { + this(contentIndent); + block.setRawNumber(currentRawNumber); + this.firstLineBlank = firstLineBlank; + block.setPreMarkerWhitespace(whitespacePreMarker); + block.setPostMarkerWhitespace(whitespacePostMarker); } @Override @@ -52,18 +65,24 @@ public Block getBlock() { @Override public BlockContinue tryContinue(ParserState state) { if (state.isBlank()) { - if (block.getFirstChild() == null) { + if (block.getFirstChild() == null || block.getFirstChild() instanceof BlankLine) { // Blank line after empty list item return BlockContinue.none(); } else { Block activeBlock = state.getActiveBlockParser().getBlock(); // If the active block is a code block, blank lines in it should not affect if the list is tight. hadBlankLine = activeBlock instanceof Paragraph || activeBlock instanceof ListItem; + return BlockContinue.atIndex(state.getNextNonSpaceIndex()); } } if (state.getIndent() >= contentIndent) { + if(firstLineBlank) { + String whitespacePostMarker = block.whitespacePostMarker() + "\n"; + block.setPostMarkerWhitespace(whitespacePostMarker); + firstLineBlank = false; + } return BlockContinue.atColumn(state.getColumn() + contentIndent); } else { // Note: We'll hit this case for lazy continuation lines, they will get added later. diff --git a/commonmark/src/main/java/org/commonmark/internal/ParagraphParser.java b/commonmark/src/main/java/org/commonmark/internal/ParagraphParser.java index 89328ef2a..7a39a6c40 100644 --- a/commonmark/src/main/java/org/commonmark/internal/ParagraphParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/ParagraphParser.java @@ -1,5 +1,7 @@ package org.commonmark.internal; +import java.util.List; + import org.commonmark.node.Block; import org.commonmark.node.LinkReferenceDefinition; import org.commonmark.node.Paragraph; @@ -11,13 +13,20 @@ import org.commonmark.parser.block.BlockContinue; import org.commonmark.parser.block.ParserState; -import java.util.List; - public class ParagraphParser extends AbstractBlockParser { private final Paragraph block = new Paragraph(); private final LinkReferenceDefinitionParser linkReferenceDefinitionParser = new LinkReferenceDefinitionParser(); + // Preserve original default constructor by explicitly defining one + public ParagraphParser() { + super(); + } + + public ParagraphParser(String preBlockWhitespace) { + block.setPreBlockWhitespace(preBlockWhitespace); + } + @Override public boolean canHaveLazyContinuationLines() { return true; @@ -62,13 +71,21 @@ public void closeBlock() { public void parseInlines(InlineParser inlineParser) { SourceLines lines = linkReferenceDefinitionParser.getParagraphLines(); if (!lines.isEmpty()) { - inlineParser.parse(lines, block); + inlineParser.parse(getRawParagraphLines(), block); } } public SourceLines getParagraphLines() { return linkReferenceDefinitionParser.getParagraphLines(); } + + public SourceLines getRawParagraphLines() { + return linkReferenceDefinitionParser.getRawParagraphLines(); + } + + public void setRawParagraphLines(SourceLines rawParagraphLines) { + linkReferenceDefinitionParser.setRawParagraphLines(rawParagraphLines); + } public List
Nodes */ public static Iterable
List<SourceLine>, but with first line removed. + * If the list is already empty, return the list unchanged + * @return List of source lines, or a blank list + */ + public List
* Lazy continuation lines are lines that were rejected by this {@link #tryContinue(ParserState)} but didn't match * any other block parsers either. *
* If true is returned here, those lines will get added via {@link #addLine(SourceLine)}. For false, the block is * closed instead. + * @return true if the block can have lazy continuation lines. */ boolean canHaveLazyContinuationLines(); @@ -42,6 +42,7 @@ public interface BlockParser { * need to override this. * * @since 0.16.0 + * @param sourceSpan Source span to be added into the current block */ void addSourceSpan(SourceSpan sourceSpan); diff --git a/commonmark/src/main/java/org/commonmark/parser/delimiter/DelimiterProcessor.java b/commonmark/src/main/java/org/commonmark/parser/delimiter/DelimiterProcessor.java index 897943d66..00b51890e 100644 --- a/commonmark/src/main/java/org/commonmark/parser/delimiter/DelimiterProcessor.java +++ b/commonmark/src/main/java/org/commonmark/parser/delimiter/DelimiterProcessor.java @@ -23,6 +23,7 @@ public interface DelimiterProcessor { /** * Minimum number of delimiter characters that are needed to activate this. Must be at least 1. + * @return the minimum number of delimiter characters needed to activate the processor. */ int getMinLength(); @@ -41,4 +42,19 @@ public interface DelimiterProcessor { */ int process(DelimiterRun openingRun, DelimiterRun closingRun); + /** + * Process the delimiter runs. + *
+ * The processor can examine the runs and the nodes and decide if it wants to process or not. If not, it should not + * change any nodes and return 0. If yes, it should do the processing (wrapping nodes, etc) and then return how many + * delimiters were used. + *
+ * Note that removal (unlinking) of the used delimiter {@link Text} nodes is done by the caller. + * + * @param openingRun the opening delimiter run + * @param closingRun the closing delimiter run + * @param prefix whitespace before the delimiter run, if any + * @return how many delimiters were used; must not be greater than length of either opener or closer + */ + int process(DelimiterRun openingRun, DelimiterRun closingRun, String prefix); } diff --git a/commonmark/src/main/java/org/commonmark/parser/delimiter/DelimiterRun.java b/commonmark/src/main/java/org/commonmark/parser/delimiter/DelimiterRun.java index 578eac96b..dea13f4c9 100644 --- a/commonmark/src/main/java/org/commonmark/parser/delimiter/DelimiterRun.java +++ b/commonmark/src/main/java/org/commonmark/parser/delimiter/DelimiterRun.java @@ -44,6 +44,8 @@ public interface DelimiterRun { *
* For example, for a delimiter run {@code ***}, calling this with 1 would return the last {@code *}.
* Calling it with 2 would return the second last {@code *} and the last {@code *}.
+ * @param length Number of openers to capture
+ * @return Requested number of opening characters
*/
Iterable
* For example, for a delimiter run {@code ***}, calling this with 1 would return the first {@code *}.
* Calling it with 2 would return the first {@code *} and the second {@code *}.
+ * @param length Number of closers to capture
+ * @return Requested number of closing characters
*/
Iterable
+ * If multiple node renderers for the same node type are created, the one from the factory that was added first
+ * "wins". (This is how the rendering for core node types can be overridden; the default rendering comes last.)
+ *
+ * @param nodeRendererFactory the factory for creating a node renderer
+ * @return {@code this}
+ */
+ public Builder nodeRendererFactory(CommonmarkNodeRendererFactory nodeRendererFactory) {
+ this.nodeRendererFactories.add(nodeRendererFactory);
+ return this;
+ }
+
+ /**
+ * @param extensions extensions to use on this text content renderer
+ * @return {@code this}
+ */
+ public Builder extensions(Iterable extends Extension> extensions) {
+ for (Extension extension : extensions) {
+ if (extension instanceof CommonMarkRenderer.CommonMarkRendererExtension) {
+ CommonMarkRenderer.CommonMarkRendererExtension textContentRendererExtension =
+ (CommonMarkRenderer.CommonMarkRendererExtension) extension;
+ textContentRendererExtension.extend(this);
+ }
+ }
+ return this;
+ }
+ }
+
+ /**
+ * Extension for {@link CommonMarkRenderer}.
+ */
+ public interface CommonMarkRendererExtension extends Extension {
+ void extend(CommonMarkRenderer.Builder rendererBuilder);
+ }
+
+ private class RendererContext implements CommonMarkNodeRendererContext {
+ private final CommonMarkWriter commonMarkWriter;
+ private final NodeRendererMap nodeRendererMap = new NodeRendererMap();
+
+ private RendererContext(CommonMarkWriter commonMarkWriter) {
+ this.commonMarkWriter = commonMarkWriter;
+
+ // The first node renderer for a node type "wins".
+ for (int i = nodeRendererFactories.size() - 1; i >= 0; i--) {
+ CommonmarkNodeRendererFactory nodeRendererFactory = nodeRendererFactories.get(i);
+ NodeRenderer nodeRenderer = nodeRendererFactory.create(this);
+ nodeRendererMap.add(nodeRenderer);
+ }
+ }
+
+ @Override
+ public boolean stripNewlines() {
+ return stripNewlines;
+ }
+
+ @Override
+ public CommonMarkWriter getWriter() {
+ return commonMarkWriter;
+ }
+
+ @Override
+ public void render(Node node) {
+ nodeRendererMap.render(node);
+ }
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/renderer/roundtrip/CommonMarkWriter.java b/commonmark/src/main/java/org/commonmark/renderer/roundtrip/CommonMarkWriter.java
new file mode 100644
index 000000000..9d0fdbbd8
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/renderer/roundtrip/CommonMarkWriter.java
@@ -0,0 +1,65 @@
+package org.commonmark.renderer.roundtrip;
+
+import java.io.IOException;
+
+public class CommonMarkWriter {
+
+ private final Appendable buffer;
+
+ private char lastChar;
+
+ public CommonMarkWriter(Appendable out) {
+ buffer = out;
+ }
+
+ public void whitespace() {
+ if (lastChar != 0 && lastChar != ' ') {
+ append(' ');
+ }
+ }
+
+ public void colon() {
+ if (lastChar != 0 && lastChar != ':') {
+ append(':');
+ }
+ }
+
+ public void line() {
+ append('\n');
+ }
+
+ public void writeStripped(String s) {
+ append(s.replaceAll("[\\r\\n\\s]+", " "));
+ }
+
+ public void write(String s) {
+ append(s);
+ }
+
+ public void write(char c) {
+ append(c);
+ }
+
+ private void append(String s) {
+ try {
+ buffer.append(s);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ int length = s.length();
+ if (length != 0) {
+ lastChar = s.charAt(length - 1);
+ }
+ }
+
+ private void append(char c) {
+ try {
+ buffer.append(c);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ lastChar = c;
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/renderer/roundtrip/CommonmarkNodeRendererFactory.java b/commonmark/src/main/java/org/commonmark/renderer/roundtrip/CommonmarkNodeRendererFactory.java
new file mode 100644
index 000000000..9c3bd61f4
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/renderer/roundtrip/CommonmarkNodeRendererFactory.java
@@ -0,0 +1,17 @@
+package org.commonmark.renderer.roundtrip;
+
+import org.commonmark.renderer.NodeRenderer;
+
+/**
+ * Factory for instantiating new node renderers when rendering is done.
+ */
+public interface CommonmarkNodeRendererFactory {
+
+ /**
+ * Create a new node renderer for the specified rendering context.
+ *
+ * @param context the context for rendering (normally passed on to the node renderer)
+ * @return a node renderer
+ */
+ NodeRenderer create(CommonMarkNodeRendererContext context);
+}
diff --git a/commonmark/src/main/java/org/commonmark/renderer/roundtrip/CoreCommonMarkNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/roundtrip/CoreCommonMarkNodeRenderer.java
new file mode 100644
index 000000000..2f9707f05
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/renderer/roundtrip/CoreCommonMarkNodeRenderer.java
@@ -0,0 +1,561 @@
+package org.commonmark.renderer.roundtrip;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.commonmark.internal.renderer.text.BulletListHolder;
+import org.commonmark.internal.renderer.text.ListHolder;
+import org.commonmark.internal.renderer.text.OrderedListHolder;
+import org.commonmark.node.AbstractVisitor;
+import org.commonmark.node.BlankLine;
+import org.commonmark.node.BlockQuote;
+import org.commonmark.node.BulletList;
+import org.commonmark.node.Code;
+import org.commonmark.node.Document;
+import org.commonmark.node.Emphasis;
+import org.commonmark.node.FencedCodeBlock;
+import org.commonmark.node.HardLineBreak;
+import org.commonmark.node.Heading;
+import org.commonmark.node.HtmlBlock;
+import org.commonmark.node.HtmlInline;
+import org.commonmark.node.Image;
+import org.commonmark.node.IndentedCodeBlock;
+import org.commonmark.node.Link;
+import org.commonmark.node.LinkFormat.LinkType;
+import org.commonmark.node.LinkReferenceDefinition;
+import org.commonmark.node.ListBlock;
+import org.commonmark.node.ListItem;
+import org.commonmark.node.Node;
+import org.commonmark.node.OrderedList;
+import org.commonmark.node.Paragraph;
+import org.commonmark.node.SoftLineBreak;
+import org.commonmark.node.StrongEmphasis;
+import org.commonmark.node.Text;
+import org.commonmark.node.ThematicBreak;
+import org.commonmark.renderer.NodeRenderer;
+
+/**
+ * The node renderer that renders all the core nodes (comes last in the order of node renderers).
+ */
+public class CoreCommonMarkNodeRenderer extends AbstractVisitor implements NodeRenderer {
+
+ protected final CommonMarkNodeRendererContext context;
+ private final CommonMarkWriter textContent;
+
+ private ListHolder listHolder;
+
+ public CoreCommonMarkNodeRenderer(CommonMarkNodeRendererContext context) {
+ this.context = context;
+ this.textContent = context.getWriter();
+ }
+
+ @Override
+ public Set