diff --git a/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/internal/HeadingIdAttributeProvider.java b/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/internal/HeadingIdAttributeProvider.java index 6b8792bd5..2ca9d08c3 100644 --- a/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/internal/HeadingIdAttributeProvider.java +++ b/commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/internal/HeadingIdAttributeProvider.java @@ -43,11 +43,11 @@ public void visit(Code code) { } }); - String finalString = ""; + StringBuilder sb = new StringBuilder(); for (String word : wordList) { - finalString += word; + sb.append(word); } - finalString = finalString.trim().toLowerCase(); + String finalString = sb.toString().trim().toLowerCase(); attributes.put("id", idGenerator.generateId(finalString)); } diff --git a/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java b/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java index 572c491f8..e27a59a80 100644 --- a/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java @@ -25,40 +25,46 @@ public BlockQuote getBlock() { return block; } + private static int calculateNewColumn(ParserState state, int nextNonSpace) { + int newColumn = state.getColumn() + state.getIndent() + 1; + + if (Characters.isSpaceOrTab(state.getLine().getContent(), nextNonSpace + 1)) { + newColumn++; + } + + return newColumn; + } + @Override public BlockContinue tryContinue(ParserState state) { int nextNonSpace = state.getNextNonSpaceIndex(); if (isMarker(state, nextNonSpace)) { - int newColumn = state.getColumn() + state.getIndent() + 1; - // optional following space or tab - if (Characters.isSpaceOrTab(state.getLine().getContent(), nextNonSpace + 1)) { - newColumn++; - } - return BlockContinue.atColumn(newColumn); - } else { - return BlockContinue.none(); + return BlockContinue.atColumn( + calculateNewColumn(state, nextNonSpace)); } - } + + return BlockContinue.none(); + } + private static boolean isMarker(ParserState state, int index) { CharSequence line = state.getLine().getContent(); return state.getIndent() < Parsing.CODE_BLOCK_INDENT && index < line.length() && line.charAt(index) == '>'; } + + public static class Factory extends AbstractBlockParserFactory { @Override public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { int nextNonSpace = state.getNextNonSpaceIndex(); if (isMarker(state, nextNonSpace)) { - int newColumn = state.getColumn() + state.getIndent() + 1; - // optional following space or tab - if (Characters.isSpaceOrTab(state.getLine().getContent(), nextNonSpace + 1)) { - newColumn++; - } - return BlockStart.of(new BlockQuoteParser()).atColumn(newColumn); - } else { - return BlockStart.none(); + return BlockStart.of(new BlockQuoteParser()) + .atColumn(BlockQuoteParser.calculateNewColumn(state, nextNonSpace)); + } + + return BlockStart.none(); } } } -} + diff --git a/commonmark/src/main/java/org/commonmark/internal/BlockStartImpl.java b/commonmark/src/main/java/org/commonmark/internal/BlockStartImpl.java index 516f944b2..039c88441 100644 --- a/commonmark/src/main/java/org/commonmark/internal/BlockStartImpl.java +++ b/commonmark/src/main/java/org/commonmark/internal/BlockStartImpl.java @@ -36,14 +36,14 @@ int getReplaceParagraphLines() { } @Override - public BlockStart atIndex(int newIndex) { - this.newIndex = newIndex; + public BlockStart atIndex(int targetIndex) { + this.newIndex = targetIndex; return this; } @Override - public BlockStart atColumn(int newColumn) { - this.newColumn = newColumn; + public BlockStart atColumn(int targetColumn) { + this.newColumn = targetColumn; return this; } diff --git a/commonmark/src/main/java/org/commonmark/internal/Delimiter.java b/commonmark/src/main/java/org/commonmark/internal/Delimiter.java index 9083ce3cb..c40ba5a4a 100644 --- a/commonmark/src/main/java/org/commonmark/internal/Delimiter.java +++ b/commonmark/src/main/java/org/commonmark/internal/Delimiter.java @@ -23,15 +23,19 @@ public class Delimiter implements DelimiterRun { public Delimiter previous; public Delimiter next; - public Delimiter(List characters, char delimiterChar, boolean canOpen, boolean canClose, Delimiter previous) { - this.characters = characters; - this.delimiterChar = delimiterChar; + public Delimiter(List delimiterTexts, + char delimiterMarker, + boolean canOpen, + boolean canClose, + Delimiter previous) { + + this.characters = delimiterTexts; + this.delimiterChar = delimiterMarker; this.canOpen = canOpen; this.canClose = canClose; this.previous = previous; - this.originalLength = characters.size(); + this.originalLength = delimiterTexts.size(); } - @Override public boolean canOpen() { return canOpen; diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java index 17e7b9c84..ea73a16bc 100644 --- a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java @@ -82,19 +82,20 @@ public class DocumentParser implements ParserState { private final List openBlockParsers = new ArrayList<>(); private final List allBlockParsers = new ArrayList<>(); - - public DocumentParser(List blockParserFactories, InlineParserFactory inlineParserFactory, - List inlineContentParserFactories, List delimiterProcessors, - List linkProcessors, Set linkMarkers, - IncludeSourceSpans includeSourceSpans, int maxOpenBlockParsers) { - this.blockParserFactories = blockParserFactories; - this.inlineParserFactory = inlineParserFactory; - this.inlineContentParserFactories = inlineContentParserFactories; - this.delimiterProcessors = delimiterProcessors; - this.linkProcessors = linkProcessors; - this.linkMarkers = linkMarkers; - this.includeSourceSpans = includeSourceSpans; - this.maxOpenBlockParsers = maxOpenBlockParsers; + private static final int TAB_SIZE = 4; + private static final int CODE_INDENT = 4; + private static final int NO_INDEX = -1; + private static final int NO_COLUMN = -1; + + public DocumentParser(DocumentParserConfig config) { + this.blockParserFactories = config.getBlockParserFactories(); + this.inlineParserFactory = config.getInlineParserFactory(); + this.inlineContentParserFactories = config.getInlineContentParserFactories(); + this.delimiterProcessors = config.getDelimiterProcessors(); + this.linkProcessors = config.getLinkProcessors(); + this.linkMarkers = config.getLinkMarkers(); + this.includeSourceSpans = config.getIncludeSourceSpans(); + this.maxOpenBlockParsers = config.getMaxOpenBlockParsers(); this.documentBlockParser = new DocumentBlockParser(); activateBlockParser(new OpenBlockParser(documentBlockParser, 0)); @@ -199,6 +200,13 @@ public BlockParser getActiveBlockParser() { * Analyze a line of text and update the document appropriately. We parse markdown text by calling this on each * line of input, then finalizing the document. */ + private boolean shouldFinalizeBlock(BlockContinueImpl blockContinue) { + return blockContinue.isFinalize(); + } + private void handleBlockFinalization(int index) { + addSourceSpans(); + closeBlockParsers(openBlockParsers.size() - index); + } private void parseLine(String ln, int inputIndex) { setLine(ln, inputIndex); @@ -214,14 +222,14 @@ private void parseLine(String ln, int inputIndex) { if (result instanceof BlockContinueImpl) { BlockContinueImpl blockContinue = (BlockContinueImpl) result; openBlockParser.sourceIndex = getIndex(); - if (blockContinue.isFinalize()) { - addSourceSpans(); - closeBlockParsers(openBlockParsers.size() - i); + if (shouldFinalizeBlock(blockContinue)) { + handleBlockFinalization(i); return; + } else { - if (blockContinue.getNewIndex() != -1) { + if (blockContinue.getNewIndex() != NO_INDEX) { setNewIndex(blockContinue.getNewIndex()); - } else if (blockContinue.getNewColumn() != -1) { + } else if (blockContinue.getNewColumn() != NO_COLUMN) { setNewColumn(blockContinue.getNewColumn()); } matches++; @@ -245,7 +253,7 @@ private void parseLine(String ln, int inputIndex) { findNextNonSpace(); // this is a little performance optimization: - if (isBlank() || (indent < Parsing.CODE_BLOCK_INDENT && Characters.isLetter(this.line.getContent(), nextNonSpace))) { + if (isBlank() || (indent < CODE_INDENT && Characters.isLetter(this.line.getContent(), nextNonSpace))) { setNewIndex(nextNonSpace); break; } @@ -359,7 +367,7 @@ private void findNextNonSpace() { continue; case '\t': i++; - cols += (4 - (cols % 4)); + cols += (TAB_SIZE - (cols % TAB_SIZE)); continue; } blank = false; @@ -604,3 +612,4 @@ private static class OpenBlockParser { } } } + diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentParserConfig.java b/commonmark/src/main/java/org/commonmark/internal/DocumentParserConfig.java new file mode 100644 index 000000000..a3fedfda3 --- /dev/null +++ b/commonmark/src/main/java/org/commonmark/internal/DocumentParserConfig.java @@ -0,0 +1,76 @@ +package org.commonmark.internal; + +import org.commonmark.parser.IncludeSourceSpans; +import org.commonmark.parser.InlineParserFactory; +import org.commonmark.parser.beta.InlineContentParserFactory; +import org.commonmark.parser.beta.LinkProcessor; +import org.commonmark.parser.block.BlockParserFactory; +import org.commonmark.parser.delimiter.DelimiterProcessor; + +import java.util.List; +import java.util.Set; + +public class DocumentParserConfig { + + private final List blockParserFactories; + private final InlineParserFactory inlineParserFactory; + private final List inlineContentParserFactories; + private final List delimiterProcessors; + private final List linkProcessors; + private final Set linkMarkers; + private final IncludeSourceSpans includeSourceSpans; + private final int maxOpenBlockParsers; + + public DocumentParserConfig( + List blockParserFactories, + InlineParserFactory inlineParserFactory, + List inlineContentParserFactories, + List delimiterProcessors, + List linkProcessors, + Set linkMarkers, + IncludeSourceSpans includeSourceSpans, + int maxOpenBlockParsers) { + + this.blockParserFactories = blockParserFactories; + this.inlineParserFactory = inlineParserFactory; + this.inlineContentParserFactories = inlineContentParserFactories; + this.delimiterProcessors = delimiterProcessors; + this.linkProcessors = linkProcessors; + this.linkMarkers = linkMarkers; + this.includeSourceSpans = includeSourceSpans; + this.maxOpenBlockParsers = maxOpenBlockParsers; + } + + // getters + public List getBlockParserFactories() { + return blockParserFactories; + } + + public InlineParserFactory getInlineParserFactory() { + return inlineParserFactory; + } + + public List getInlineContentParserFactories() { + return inlineContentParserFactories; + } + + public List getDelimiterProcessors() { + return delimiterProcessors; + } + + public List getLinkProcessors() { + return linkProcessors; + } + + public Set getLinkMarkers() { + return linkMarkers; + } + + public IncludeSourceSpans getIncludeSourceSpans() { + return includeSourceSpans; + } + + public int getMaxOpenBlockParsers() { + return maxOpenBlockParsers; + } +} diff --git a/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java b/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java index d550f1d25..5a8593c38 100644 --- a/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java @@ -14,6 +14,7 @@ public class FencedCodeBlockParser extends AbstractBlockParser { private final FencedCodeBlock block = new FencedCodeBlock(); private final char fenceChar; private final int openingFenceLength; + private static final int MIN_FENCE_LENGTH = 3; private String firstLine; private StringBuilder otherLines = new StringBuilder(); @@ -106,13 +107,13 @@ private static FencedCodeBlockParser checkOpener(CharSequence line, int index, i break loop; } } - if (backticks >= 3 && tildes == 0) { + if (backticks >= MIN_FENCE_LENGTH && tildes == 0) { // spec: If the info string comes after a backtick fence, it may not contain any backtick characters. if (Characters.find('`', line, index + backticks) != -1) { return null; } return new FencedCodeBlockParser('`', backticks, indent); - } else if (tildes >= 3 && backticks == 0) { + } else if (tildes >= MIN_FENCE_LENGTH && backticks == 0) { // spec: Info strings for tilde code blocks can contain backticks and tildes return new FencedCodeBlockParser('~', tildes, indent); } else { diff --git a/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java b/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java index 05f070137..bffd36244 100644 --- a/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java @@ -15,6 +15,7 @@ public class HeadingParser extends AbstractBlockParser { private final Heading block = new Heading(); private final SourceLines content; + private static final int MAX_HEADING_LEVEL = 6; public HeadingParser(int level, SourceLines content) { block.setLevel(level); @@ -76,7 +77,7 @@ private static HeadingParser getAtxHeading(SourceLine line) { Scanner scanner = Scanner.of(SourceLines.of(line)); int level = scanner.matchMultiple('#'); - if (level == 0 || level > 6) { + if (level == 0 || level > MAX_HEADING_LEVEL) { return null; } diff --git a/commonmark/src/main/java/org/commonmark/internal/HtmlBlockParser.java b/commonmark/src/main/java/org/commonmark/internal/HtmlBlockParser.java index 123d9ec1f..2d64c248d 100644 --- a/commonmark/src/main/java/org/commonmark/internal/HtmlBlockParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/HtmlBlockParser.java @@ -24,6 +24,8 @@ public class HtmlBlockParser extends AbstractBlockParser { private static final String OPENTAG = "<" + TAGNAME + ATTRIBUTE + "*" + "\\s*/?>"; private static final String CLOSETAG = "]"; + private static final int CODE_BLOCK_INDENT = 4; + private static final int MAX_HTML_BLOCK_TYPE = 7; private static final Pattern[][] BLOCK_PATTERNS = new Pattern[][]{ {null, null}, // not used (no type 0) @@ -124,8 +126,8 @@ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockPar int nextNonSpace = state.getNextNonSpaceIndex(); CharSequence line = state.getLine().getContent(); - if (state.getIndent() < 4 && line.charAt(nextNonSpace) == '<') { - for (int blockType = 1; blockType <= 7; blockType++) { + if (state.getIndent() < CODE_BLOCK_INDENT && line.charAt(nextNonSpace) == '<') { + for (int blockType = 1; blockType <= MAX_HTML_BLOCK_TYPE; blockType++) { // Type 7 can not interrupt a paragraph (not even a lazy one) if (blockType == 7 && ( matchedBlockParser.getMatchedBlockParser().getBlock() instanceof Paragraph || diff --git a/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java b/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java index 637d3b111..3987fc6a8 100644 --- a/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java @@ -28,7 +28,7 @@ public class LinkReferenceDefinitionParser { private StringBuilder label; private String destination; - private char titleDelimiter; + private TitleDelimiter titleDelimiter; private StringBuilder title; private boolean referenceValid = false; @@ -104,8 +104,12 @@ State getState() { } List removeLines(int lines) { - var removedSpans = Collections.unmodifiableList(new ArrayList<>( - sourceSpans.subList(Math.max(sourceSpans.size() - lines, 0), sourceSpans.size()))); + var removedSpans = List.copyOf( + sourceSpans.subList( + Math.max(sourceSpans.size() - lines, 0), + sourceSpans.size() + ) + ); removeLast(lines, paragraphLines); removeLast(lines, sourceSpans); return removedSpans; @@ -201,19 +205,10 @@ private boolean startTitle(Scanner scanner) { return true; } - titleDelimiter = '\0'; char c = scanner.peek(); - switch (c) { - case '"': - case '\'': - titleDelimiter = c; - break; - case '(': - titleDelimiter = ')'; - break; - } + titleDelimiter = TitleDelimiter.fromOpeningChar(c); - if (titleDelimiter != '\0') { + if (titleDelimiter != null) { state = State.TITLE; title = new StringBuilder(); scanner.next(); @@ -221,7 +216,7 @@ private boolean startTitle(Scanner scanner) { title.append('\n'); } } else { - // There might be another reference instead, try that for the same character. + state = State.START_DEFINITION; } return true; @@ -229,7 +224,7 @@ private boolean startTitle(Scanner scanner) { private boolean title(Scanner scanner) { Position start = scanner.position(); - if (!LinkScanner.scanLinkTitleContent(scanner, titleDelimiter)) { + if (!LinkScanner.scanLinkTitleContent(scanner, titleDelimiter.getClosingChar())) { // Invalid title, stop. Title collected so far must not be used. title = null; return false; @@ -303,4 +298,32 @@ enum State { // End state, no matter what kind of lines we add, they won't be references PARAGRAPH, } + private enum TitleDelimiter { + DOUBLE_QUOTE('"'), + SINGLE_QUOTE('\''), + PARENTHESIS(')'); + + private final char closingChar; + + TitleDelimiter(char closingChar) { + this.closingChar = closingChar; + } + + public char getClosingChar() { + return closingChar; + } + + public static TitleDelimiter fromOpeningChar(char c) { + switch (c) { + case '"': + return DOUBLE_QUOTE; + case '\'': + return SINGLE_QUOTE; + case '(': + return PARENTHESIS; + default: + return null; + } + } + } } diff --git a/commonmark/src/main/java/org/commonmark/node/Nodes.java b/commonmark/src/main/java/org/commonmark/node/Nodes.java index 22d5932af..2d3cd298f 100644 --- a/commonmark/src/main/java/org/commonmark/node/Nodes.java +++ b/commonmark/src/main/java/org/commonmark/node/Nodes.java @@ -57,10 +57,7 @@ public Node next() { return result; } - @Override - public void remove() { - throw new UnsupportedOperationException("remove"); - } + } } diff --git a/commonmark/src/main/java/org/commonmark/parser/Parser.java b/commonmark/src/main/java/org/commonmark/parser/Parser.java index 8faac789b..7cc627b7c 100644 --- a/commonmark/src/main/java/org/commonmark/parser/Parser.java +++ b/commonmark/src/main/java/org/commonmark/parser/Parser.java @@ -1,10 +1,7 @@ package org.commonmark.parser; import org.commonmark.Extension; -import org.commonmark.internal.Definitions; -import org.commonmark.internal.DocumentParser; -import org.commonmark.internal.InlineParserContextImpl; -import org.commonmark.internal.InlineParserImpl; +import org.commonmark.internal.*; import org.commonmark.node.*; import org.commonmark.parser.beta.LinkInfo; import org.commonmark.parser.beta.LinkProcessor; @@ -66,6 +63,10 @@ public static Builder builder() { return new Builder(); } + private Node processParsedDocument(Node document) { + return postProcess(document); + } + /** * Parse the specified input text into a tree of nodes. *

@@ -74,11 +75,12 @@ public static Builder builder() { * @param input the text to parse - must not be null * @return the root node */ + public Node parse(String input) { - Objects.requireNonNull(input, "input must not be null"); - DocumentParser documentParser = createDocumentParser(); - Node document = documentParser.parse(input); - return postProcess(document); + Objects.requireNonNull(input); + DocumentParser documentParser = createDocumentParser(); + + return processParsedDocument(documentParser.parse(input)); } /** @@ -100,15 +102,25 @@ public Node parse(String input) { * @throws IOException when reading throws an exception */ public Node parseReader(Reader input) throws IOException { - Objects.requireNonNull(input, "input must not be null"); + Objects.requireNonNull(input); DocumentParser documentParser = createDocumentParser(); - Node document = documentParser.parse(input); - return postProcess(document); + return processParsedDocument(documentParser.parse(input)); + } private DocumentParser createDocumentParser() { - return new DocumentParser(blockParserFactories, inlineParserFactory, inlineContentParserFactories, - delimiterProcessors, linkProcessors, linkMarkers, includeSourceSpans, maxOpenBlockParsers); + DocumentParserConfig config = new DocumentParserConfig( + blockParserFactories, + inlineParserFactory, + inlineContentParserFactories, + delimiterProcessors, + linkProcessors, + linkMarkers, + includeSourceSpans, + maxOpenBlockParsers + ); + + return new DocumentParser(config); } private Node postProcess(Node document) { @@ -147,13 +159,18 @@ public Parser build() { public Builder extensions(Iterable extensions) { Objects.requireNonNull(extensions, "extensions must not be null"); for (Extension extension : extensions) { + applyExtension(extension); + } + return this; + } + private void applyExtension(Extension extension){ if (extension instanceof ParserExtension) { ParserExtension parserExtension = (ParserExtension) extension; parserExtension.extend(this); } } - return this; - } + + /** * Describe the list of markdown features the parser will recognize and parse. @@ -217,12 +234,15 @@ public Builder includeSourceSpans(IncludeSourceSpans includeSourceSpans) { * @return {@code this} */ public Builder maxOpenBlockParsers(int maxOpenBlockParsers) { - if (maxOpenBlockParsers < 0) { - throw new IllegalArgumentException("maxOpenBlockParsers must be >= 0"); - } + validateMaxOpenBlockParsers(maxOpenBlockParsers); this.maxOpenBlockParsers = maxOpenBlockParsers; return this; } + private void validateMaxOpenBlockParsers(int value){ + if (value < 0) { + throw new IllegalArgumentException("maxOpenBlockParsers must be >= 0"); + } + } /** * Add a custom block parser factory. @@ -335,11 +355,10 @@ public Builder inlineParserFactory(InlineParserFactory inlineParserFactory) { } private InlineParserFactory getInlineParserFactory() { - if (inlineParserFactory != null) { - return inlineParserFactory; - } else { - return InlineParserImpl::new; - } + return inlineParserFactory != null + ? inlineParserFactory : + InlineParserImpl::new; + } } diff --git a/commonmark/src/main/java/org/commonmark/parser/beta/Scanner.java b/commonmark/src/main/java/org/commonmark/parser/beta/Scanner.java index 324639493..8da2943a4 100644 --- a/commonmark/src/main/java/org/commonmark/parser/beta/Scanner.java +++ b/commonmark/src/main/java/org/commonmark/parser/beta/Scanner.java @@ -193,34 +193,34 @@ public int whitespace() { } } - public int find(char c) { + private int findMatching(CharMatcher matcher) { int count = 0; + while (true) { - char cur = peek(); - if (cur == Scanner.END) { + char current = peek(); + + if (current == END) { return -1; - } else if (cur == c) { + } + + if (matcher.matches(current)) { return count; } + count++; next(); } } + public int find(char c) { + return findMatching(ch -> ch == c); + } + public int find(CharMatcher matcher) { - int count = 0; - while (true) { - char c = peek(); - if (c == END) { - return -1; - } else if (matcher.matches(c)) { - return count; - } - count++; - next(); - } + return findMatching(matcher); } + // Don't expose the int index, because it would be good if we could switch input to a List of lines later // instead of one contiguous String. public Position position() { diff --git a/commonmark/src/main/java/org/commonmark/parser/block/BlockStart.java b/commonmark/src/main/java/org/commonmark/parser/block/BlockStart.java index c41f1caa3..0ede6d135 100644 --- a/commonmark/src/main/java/org/commonmark/parser/block/BlockStart.java +++ b/commonmark/src/main/java/org/commonmark/parser/block/BlockStart.java @@ -13,6 +13,7 @@ protected BlockStart() { /** * Result for when there is no block start. */ + public static BlockStart none() { return null; } diff --git a/commonmark/src/main/java/org/commonmark/renderer/html/CoreHtmlNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/html/CoreHtmlNodeRenderer.java index 5c536558e..7c996ba09 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/html/CoreHtmlNodeRenderer.java +++ b/commonmark/src/main/java/org/commonmark/renderer/html/CoreHtmlNodeRenderer.java @@ -141,9 +141,7 @@ public void visit(ThematicBreak thematicBreak) { public void visit(IndentedCodeBlock indentedCodeBlock) { renderCodeBlock(indentedCodeBlock.getLiteral(), indentedCodeBlock, Map.of()); } - - @Override - public void visit(Link link) { + private Map createLinkAttributes(Link link) { Map attrs = new LinkedHashMap<>(); String url = link.getDestination(); @@ -152,12 +150,19 @@ public void visit(Link link) { attrs.put("rel", "nofollow"); } - url = context.encodeUrl(url); - attrs.put("href", url); + attrs.put("href", context.encodeUrl(url)); + if (link.getTitle() != null) { attrs.put("title", link.getTitle()); } - html.tag("a", getAttrs(link, "a", attrs)); + + return attrs; + } + + @Override + public void visit(Link link) { + + html.tag("a", getAttrs(link, "a",createLinkAttributes(link) )); visitChildren(link); html.tag("/a"); } @@ -179,27 +184,32 @@ public void visit(OrderedList orderedList) { } renderListBlock(orderedList, "ol", getAttrs(orderedList, "ol", attrs)); } + private Map createImageAttributes(Image image) { + Map attrs = new LinkedHashMap<>(); - @Override - public void visit(Image image) { String url = image.getDestination(); - - AltTextVisitor altTextVisitor = new AltTextVisitor(); - image.accept(altTextVisitor); - String altText = altTextVisitor.getAltText(); - - Map attrs = new LinkedHashMap<>(); if (context.shouldSanitizeUrls()) { url = context.urlSanitizer().sanitizeImageUrl(url); } attrs.put("src", context.encodeUrl(url)); - attrs.put("alt", altText); + attrs.put("alt", extractAltText(image)); + if (image.getTitle() != null) { attrs.put("title", image.getTitle()); } - html.tag("img", getAttrs(image, "img", attrs), true); + return attrs; + } + private String extractAltText(Image image) { + AltTextVisitor altTextVisitor = new AltTextVisitor(); + image.accept(altTextVisitor); + return altTextVisitor.getAltText(); + } + @Override + public void visit(Image image) { + + html.tag("img", getAttrs(image, "img", createImageAttributes(image)), true); } @Override diff --git a/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java index b0264fc72..5b5815e62 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java +++ b/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java @@ -28,6 +28,8 @@ public class HtmlRenderer implements Renderer { private final List attributeProviderFactories; private final List nodeRendererFactories; + + private HtmlRenderer(Builder builder) { this.softbreak = builder.softbreak; this.escapeHtml = builder.escapeHtml; @@ -36,13 +38,20 @@ private HtmlRenderer(Builder builder) { this.sanitizeUrls = builder.sanitizeUrls; this.urlSanitizer = builder.urlSanitizer; this.attributeProviderFactories = new ArrayList<>(builder.attributeProviderFactories); + // Add as last. This means clients can override the rendering of core nodes if they want.*/ + this.nodeRendererFactories = buildNodeRenderers(builder); + } + private static List buildNodeRenderers(Builder builder) { + List result = + new ArrayList<>(builder.nodeRendererFactories.size() + 1); - this.nodeRendererFactories = new ArrayList<>(builder.nodeRendererFactories.size() + 1); - this.nodeRendererFactories.addAll(builder.nodeRendererFactories); - // Add as last. This means clients can override the rendering of core nodes if they want. - this.nodeRendererFactories.add(CoreHtmlNodeRenderer::new); + result.addAll(builder.nodeRendererFactories); + result.add(CoreHtmlNodeRenderer::new); + + return result; } + /** * Create a new builder for configuring an {@link HtmlRenderer}. * @@ -52,15 +61,20 @@ public static Builder builder() { return new Builder(); } + private RendererContext createContext(Appendable output) { + return new RendererContext(new HtmlWriter(output)); + } + @Override public void render(Node node, Appendable output) { Objects.requireNonNull(node, "node must not be null"); - RendererContext context = new RendererContext(new HtmlWriter(output)); + RendererContext context = createContext(output); context.beforeRoot(node); context.render(node); context.afterRoot(node); } + @Override public String render(Node node) { Objects.requireNonNull(node, "node must not be null"); @@ -233,12 +247,18 @@ private class RendererContext implements HtmlNodeRendererContext, AttributeProvi private RendererContext(HtmlWriter htmlWriter) { this.htmlWriter = htmlWriter; - - attributeProviders = new ArrayList<>(attributeProviderFactories.size()); + this.attributeProviders = createAttributeProviders(); + initializeNodeRenderers(); + } + private List createAttributeProviders() { + List providers = new ArrayList<>(attributeProviderFactories.size()); for (var attributeProviderFactory : attributeProviderFactories) { - attributeProviders.add(attributeProviderFactory.create(this)); + providers.add(attributeProviderFactory.create(this)); } + return providers; + } + private void initializeNodeRenderers() { for (var factory : nodeRendererFactories) { var renderer = factory.create(this); nodeRendererMap.add(renderer); diff --git a/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownRenderer.java index e4996fb08..a6f7cd74d 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownRenderer.java +++ b/commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownRenderer.java @@ -130,12 +130,18 @@ private class RendererContext implements MarkdownNodeRendererContext { private RendererContext(MarkdownWriter writer) { // Set fields that are used by interface this.writer = writer; + this.additionalTextEscapes = createAdditionalTextEscapes(); + initializeNodeRenderers(); + } + private Set createAdditionalTextEscapes() { Set escapes = new HashSet<>(); for (MarkdownNodeRendererFactory factory : nodeRendererFactories) { escapes.addAll(factory.getSpecialCharacters()); } - additionalTextEscapes = Collections.unmodifiableSet(escapes); + return Collections.unmodifiableSet(escapes); + } + private void initializeNodeRenderers(){ for (var factory : nodeRendererFactories) { // Pass in this as context here, which uses the fields set above var renderer = factory.create(this); diff --git a/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java index 62a1a054d..bb3bfadfc 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java +++ b/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java @@ -86,17 +86,25 @@ public void visit(Code code) { textContent.write('\"'); } - @Override - public void visit(FencedCodeBlock fencedCodeBlock) { - var literal = stripTrailingNewline(fencedCodeBlock.getLiteral()); + private void renderCodeBlock(String literal) { + literal = stripTrailingNewline(literal); + if (stripNewlines()) { textContent.writeStripped(literal); } else { textContent.write(literal); } + textContent.block(); } + @Override + public void visit(FencedCodeBlock fencedCodeBlock) { + renderCodeBlock(fencedCodeBlock.getLiteral()); + } + + + @Override public void visit(HardLineBreak hardLineBreak) { if (stripNewlines()) { @@ -141,15 +149,8 @@ public void visit(Image image) { @Override public void visit(IndentedCodeBlock indentedCodeBlock) { - var literal = stripTrailingNewline(indentedCodeBlock.getLiteral()); - if (stripNewlines()) { - textContent.writeStripped(literal); - } else { - textContent.write(literal); - } - textContent.block(); + renderCodeBlock(indentedCodeBlock.getLiteral()); } - @Override public void visit(Link link) { writeLink(link, link.getTitle(), link.getDestination());