Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html),
with the exception that 0.x versions can break between minor versions.

## [Unreleased]
### Fixed
- Fix parsing of unclosed titles for link reference definitions, fix source
spans in resulting paragraph (#460)


## [0.30.1] - 2026-10-11
### Changed
- Improve MarkdownRenderer performance by appending unescaped spans in bulk (#456)
Expand Down Expand Up @@ -601,6 +607,7 @@ API breaking changes (caused by changes in spec):
Initial release of commonmark-java, a port of commonmark.js with extensions
for autolinking URLs, GitHub flavored strikethrough and tables.

[Unreleased]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.30.1...HEAD
[0.30.1]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.30.0...commonmark-parent-0.30.1
[0.30.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.29.0...commonmark-parent-0.30.0
[0.29.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.28.0...commonmark-parent-0.29.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class LinkReferenceDefinitionParser {
private char titleDelimiter;
private StringBuilder title;
private boolean referenceValid = false;
private int referenceSourceSpanCount;

public void parse(SourceLine line) {
paragraphLines.add(line);
Expand Down Expand Up @@ -87,6 +88,11 @@ public void parse(SourceLine line) {

public void addSourceSpan(SourceSpan sourceSpan) {
sourceSpans.add(sourceSpan);
// Clearing paragraphLines commits the current line to a valid definition. Its span
// arrives after parsing, so record the boundary here.
if (paragraphLines.isEmpty()) {
referenceSourceSpanCount = sourceSpans.size();
}
}

/**
Expand Down Expand Up @@ -278,10 +284,14 @@ private void finishReference() {
}

String d = Escaping.unescapeString(destination);
String t = title != null ? Escaping.unescapeString(title.toString()) : null;
String t =
state != State.TITLE && title != null
? Escaping.unescapeString(title.toString())
: null;
LinkReferenceDefinition definition = new LinkReferenceDefinition(label.toString(), d, t);
definition.setSourceSpans(sourceSpans);
sourceSpans.clear();
definition.setSourceSpans(sourceSpans.subList(0, referenceSourceSpanCount));
sourceSpans.subList(0, referenceSourceSpanCount).clear();
referenceSourceSpanCount = 0;
definitions.add(definition);

label = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import org.commonmark.internal.LinkReferenceDefinitionParser.State;
import org.commonmark.node.LinkReferenceDefinition;
import org.commonmark.node.SourceSpan;
import org.commonmark.parser.SourceLine;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -159,14 +160,64 @@ void testTitleMultiline3() {

@Test
void testTitleMultiline4() {
var definitionSpan = SourceSpan.of(0, 0, 0, 11);
var titleSpan = SourceSpan.of(1, 0, 12, 6);
var invalidSpan = SourceSpan.of(2, 0, 19, 4);
parse("[foo]: /url");
parser.addSourceSpan(definitionSpan);
assertThat(parser.getState()).isEqualTo(State.START_TITLE);
parse("(title");
parser.addSourceSpan(titleSpan);
assertThat(parser.getState()).isEqualTo(State.TITLE);
parse("foo(");
parser.addSourceSpan(invalidSpan);
assertThat(parser.getState()).isEqualTo(State.PARAGRAPH);

assertDef(parser.getDefinitions().get(0), "foo", "/url", null);
assertThat(parser.getDefinitions().get(0).getSourceSpans()).containsExactly(definitionSpan);
assertParagraphLines("(title\nfoo(", parser);
assertThat(parser.getParagraphSourceSpans()).containsExactly(titleSpan, invalidSpan);
}

@Test
void testUnclosedMultilineTitleKeepsParagraphSourceSpans() {
var definitionSpan = SourceSpan.of(0, 0, 0, 11);
var paragraphSpan = SourceSpan.of(1, 0, 12, 14);

parse("[foo]: /url");
parser.addSourceSpan(definitionSpan);
parse("(partial title");
parser.addSourceSpan(paragraphSpan);

var definitions = parser.getDefinitions();

assertThat(definitions).hasSize(1);
assertDef(definitions.get(0), "foo", "/url", null);
assertThat(definitions.get(0).getSourceSpans()).containsExactly(definitionSpan);
assertParagraphLines("(partial title", parser);
assertThat(parser.getParagraphSourceSpans()).containsExactly(paragraphSpan);
}

@Test
void testMultilineTitleWithTrailingGarbageKeepsParagraphSourceSpans() {
var definitionSpan = SourceSpan.of(0, 0, 0, 11);
var titleSpan = SourceSpan.of(1, 0, 12, 6);
var invalidSpan = SourceSpan.of(2, 0, 19, 18);

parse("[foo]: /url");
parser.addSourceSpan(definitionSpan);
parse("\"title");
parser.addSourceSpan(titleSpan);
parse("continued\" garbage");
parser.addSourceSpan(invalidSpan);

var definitions = parser.getDefinitions();

assertThat(definitions).hasSize(1);
assertDef(definitions.get(0), "foo", "/url", null);
assertThat(definitions.get(0).getSourceSpans()).containsExactly(definitionSpan);
assertParagraphLines("\"title\ncontinued\" garbage", parser);
assertThat(parser.getParagraphSourceSpans()).containsExactly(titleSpan, invalidSpan);
}

@Test
Expand Down
Loading