From 88c8918f910c1f722b7f25ae22ec3f539558ef3b Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Wed, 17 Jun 2026 11:59:40 +0600 Subject: [PATCH 01/14] Refactor duplicated parsing workflow --- .../java/org/commonmark/parser/Parser.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/commonmark/src/main/java/org/commonmark/parser/Parser.java b/commonmark/src/main/java/org/commonmark/parser/Parser.java index 8faac789..8d3f7690 100644 --- a/commonmark/src/main/java/org/commonmark/parser/Parser.java +++ b/commonmark/src/main/java/org/commonmark/parser/Parser.java @@ -66,6 +66,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 +78,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,10 +105,10 @@ 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() { From 9ff3b57b2e8b76561ec89d6c640b1d021852f982 Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Wed, 17 Jun 2026 19:29:50 +0600 Subject: [PATCH 02/14] Refactor Parser.Builder: extract methods for extension handling, validation, and inline parser factory --- .../java/org/commonmark/parser/Parser.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/commonmark/src/main/java/org/commonmark/parser/Parser.java b/commonmark/src/main/java/org/commonmark/parser/Parser.java index 8d3f7690..2cac1c5a 100644 --- a/commonmark/src/main/java/org/commonmark/parser/Parser.java +++ b/commonmark/src/main/java/org/commonmark/parser/Parser.java @@ -152,13 +152,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. @@ -222,12 +227,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. @@ -340,11 +348,10 @@ public Builder inlineParserFactory(InlineParserFactory inlineParserFactory) { } private InlineParserFactory getInlineParserFactory() { - if (inlineParserFactory != null) { - return inlineParserFactory; - } else { - return InlineParserImpl::new; - } + return inlineParserFactory != null + ? inlineParserFactory : + InlineParserImpl::new; + } } From b1621535d2ea3ae9c54e9f05258aba5f7921aa4c Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Wed, 17 Jun 2026 23:31:31 +0600 Subject: [PATCH 03/14] Refactor HtmlRenderer: extract and staticize node renderer creation --- .../renderer/html/HtmlRenderer.java | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) 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 b0264fc7..8e0b6d12 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); + + result.addAll(builder.nodeRendererFactories); + result.add(CoreHtmlNodeRenderer::new); - 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); + 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"); From 540ab25635b7d98a3b358cb37348c99b7dbc083a Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Wed, 17 Jun 2026 23:59:30 +0600 Subject: [PATCH 04/14] Improve SRP in RendererContext by extracting initialization logic --- .../org/commonmark/renderer/html/HtmlRenderer.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 8e0b6d12..5b5815e6 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java +++ b/commonmark/src/main/java/org/commonmark/renderer/html/HtmlRenderer.java @@ -247,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); From a95834e438bdd47f683cad604a7bd79c0d95af94 Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Thu, 18 Jun 2026 18:27:13 +0600 Subject: [PATCH 05/14] refactor: extract initialization logic from RendererContext --- .../commonmark/renderer/markdown/MarkdownRenderer.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 e4996fb0..a6f7cd74 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); From a2bc02d2a3386bbbd3e5074ad7d61306ea400e46 Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Thu, 18 Jun 2026 21:13:06 +0600 Subject: [PATCH 06/14] Refactor: introduce DocumentParserConfig (Parameter Object) to remove long parameter list --- .../commonmark/internal/DocumentParser.java | 22 +++--- .../internal/DocumentParserConfig.java | 76 +++++++++++++++++++ .../java/org/commonmark/parser/Parser.java | 19 +++-- 3 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 commonmark/src/main/java/org/commonmark/internal/DocumentParserConfig.java diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java index 17e7b9c8..5fb784a1 100644 --- a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java @@ -83,18 +83,15 @@ 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; + 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)); @@ -604,3 +601,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 00000000..a3fedfda --- /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/parser/Parser.java b/commonmark/src/main/java/org/commonmark/parser/Parser.java index 2cac1c5a..7cc627b7 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; @@ -112,8 +109,18 @@ public Node parseReader(Reader input) throws IOException { } 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) { From aa4bd267168b7817e8e9ae839151ba1fae973571 Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Thu, 18 Jun 2026 22:40:29 +0600 Subject: [PATCH 07/14] Refactor magic numbers to constants in parser classes --- .../java/org/commonmark/internal/DocumentParser.java | 12 ++++++++---- .../commonmark/internal/FencedCodeBlockParser.java | 5 +++-- .../java/org/commonmark/internal/HeadingParser.java | 3 ++- .../org/commonmark/internal/HtmlBlockParser.java | 6 ++++-- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java index 5fb784a1..32afd7e4 100644 --- a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java @@ -82,6 +82,10 @@ public class DocumentParser implements ParserState { private final List openBlockParsers = new ArrayList<>(); private final List allBlockParsers = new ArrayList<>(); + 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(); @@ -216,9 +220,9 @@ private void parseLine(String ln, int inputIndex) { closeBlockParsers(openBlockParsers.size() - 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++; @@ -242,7 +246,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; } @@ -356,7 +360,7 @@ private void findNextNonSpace() { continue; case '\t': i++; - cols += (4 - (cols % 4)); + cols += (TAB_SIZE - (cols % TAB_SIZE)); continue; } blank = false; diff --git a/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java b/commonmark/src/main/java/org/commonmark/internal/FencedCodeBlockParser.java index d550f1d2..5a8593c3 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 05f07013..bffd3624 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 123d9ec1..2d64c248 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 || From 05625544e38f85fb29a91371e97947f9dc66f13d Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Fri, 19 Jun 2026 08:00:38 +0600 Subject: [PATCH 08/14] Refactor hidden field smells by renaming parameters --- .../commonmark/internal/BlockQuoteParser.java | 42 +++++++++++-------- .../commonmark/internal/BlockStartImpl.java | 8 ++-- .../org/commonmark/internal/Delimiter.java | 14 ++++--- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java b/commonmark/src/main/java/org/commonmark/internal/BlockQuoteParser.java index 572c491f..e27a59a8 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 516f944b..039c8844 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 9083ce3c..c40ba5a4 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; From 42aaf16891da57098303bb9231414a12726a4275 Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Fri, 19 Jun 2026 14:03:52 +0600 Subject: [PATCH 09/14] Refactor Scanner find methods using Extract Method --- .../commonmark/internal/DocumentParser.java | 13 ++++++-- .../org/commonmark/parser/beta/Scanner.java | 30 +++++++++---------- .../text/CoreTextContentNodeRenderer.java | 23 +++++++------- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java index 32afd7e4..ea73a16b 100644 --- a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java @@ -200,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); @@ -215,10 +222,10 @@ 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() != NO_INDEX) { setNewIndex(blockContinue.getNewIndex()); 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 32463949..8da2943a 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/renderer/text/CoreTextContentNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java index 62a1a054..bb3bfadf 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()); From f01bcc13d42b3e428b48a8565e7e883bfb01a01f Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Fri, 19 Jun 2026 18:27:17 +0600 Subject: [PATCH 10/14] Refactor Nodes.NodeIterator by removing redundant remove() override --- commonmark/src/main/java/org/commonmark/node/Nodes.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/commonmark/src/main/java/org/commonmark/node/Nodes.java b/commonmark/src/main/java/org/commonmark/node/Nodes.java index 22d5932a..2d3cd298 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"); - } + } } From 1685218ea3bbb927388a0cefb3a8667d5b82f297 Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Fri, 19 Jun 2026 19:36:07 +0600 Subject: [PATCH 11/14] Fix string concatenation in loop using StringBuilder --- .../heading/anchor/internal/HeadingIdAttributeProvider.java | 6 +++--- .../main/java/org/commonmark/parser/block/BlockStart.java | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) 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 6b8792bd..2ca9d08c 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/parser/block/BlockStart.java b/commonmark/src/main/java/org/commonmark/parser/block/BlockStart.java index c41f1caa..0ede6d13 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; } From 2cde18e28aa31f118595aeae4b3d114854e9ffdf Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Fri, 19 Jun 2026 19:57:26 +0600 Subject: [PATCH 12/14] Refactor: replace Collections.unmodifiableList with List.copyOf --- .../internal/LinkReferenceDefinitionParser.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java b/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java index 637d3b11..2a8c8564 100644 --- a/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java @@ -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; From 900561863f29881c247bae016850df96591fc9b8 Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Sun, 21 Jun 2026 17:52:25 +0600 Subject: [PATCH 13/14] Refactor Feature Envy in visit(Link) and visit(Image) --- .../renderer/html/CoreHtmlNodeRenderer.java | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) 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 5c536558..7c996ba0 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 From dcc95c6537cfe23db1dba4b342cfe1dcbb27a741 Mon Sep 17 00:00:00 2001 From: ani28-bit Date: Sun, 21 Jun 2026 18:17:53 +0600 Subject: [PATCH 14/14] refactor: replace primitive titleDelimiter char with TitleDelimiter enum --- .../LinkReferenceDefinitionParser.java | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java b/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java index 2a8c8564..3987fc6a 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; @@ -205,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(); @@ -225,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; @@ -233,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; @@ -307,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; + } + } + } }