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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ 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
### Changed
- A `LinkProcessor` using `replaceWith` now also stops outer links from being
parsed as links, same as with `wrapTextIn`. This prevents nested links, see
footnotes change below.
### Fixed

- Fix rendering of image alt text to include contents of code spans (`` `code` ``). (#398)
- footnotes: Fix footnotes nested within links. Before, both the link and the
footnote reference would be parsed and lead to nested `<a>` elements, which
is disallowed. Now, only the footnote is parsed and the outer link becomes
plain text; this matches the behavior of links. (#400)

## [0.25.1] - 2025-08-01
### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,29 @@ public void testReferenceLinkWithoutDefinition() {
assertText("[foo]", paragraph.getLastChild());
}

@Test
public void testFootnoteInLink() {
// Expected to behave the same way as a link within a link, see https://spec.commonmark.org/0.31.2/#example-518
// i.e. the first (inner) link is parsed, which means the outer one becomes plain text, as nesting links is not
// allowed.
var doc = PARSER.parse("[link with footnote ref [^1]](https://example.com)\n\n[^1]: footnote\n");
var ref = find(doc, FootnoteReference.class);
assertThat(ref.getLabel()).isEqualTo("1");
var paragraph = doc.getFirstChild();
assertText("[link with footnote ref ", paragraph.getFirstChild());
assertText("](https://example.com)", paragraph.getLastChild());
}

@Test
public void testFootnoteWithMarkerInLink() {
var doc = PARSER.parse("[link with footnote ref ![^1]](https://example.com)\n\n[^1]: footnote\n");
var ref = find(doc, FootnoteReference.class);
assertThat(ref.getLabel()).isEqualTo("1");
var paragraph = doc.getFirstChild();
assertText("[link with footnote ref !", paragraph.getFirstChild());
assertText("](https://example.com)", paragraph.getLastChild());
}

@Test
public void testInlineFootnote() {
var extension = FootnotesExtension.builder().inlineFootnotes(true).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,16 +440,9 @@ private Node wrapBracket(Bracket opener, Node wrapperNode, boolean includeMarker
opener.bracketNode.unlink();
removeLastBracket();

// Links within links are not allowed. We found this link, so there can be no other link around it.
// Links within links are not allowed. We found this link, so there can be no other links around it.
if (opener.markerNode == null) {
Bracket bracket = lastBracket;
while (bracket != null) {
if (bracket.markerNode == null) {
// Disallow link opener. It will still get matched, but will not result in a link.
bracket.allowed = false;
}
bracket = bracket.previous;
}
disallowPreviousLinks();
}

return wrapperNode;
Expand All @@ -475,6 +468,15 @@ private Node replaceBracket(Bracket opener, Node node, boolean includeMarker) {
n.unlink();
n = next;
}

// Links within links are not allowed. We found this link, so there can be no other links around it.
// Note that this makes any syntax like `[foo]` behave the same as built-in links, which is probably a good
// default (it works for footnotes). It might be useful for a `LinkProcessor` to be able to specify the
// behavior; something we could add to `LinkResult` in the future if requested.
if (opener.markerNode == null || !includeMarker) {
disallowPreviousLinks();
}

return node;
}

Expand All @@ -489,6 +491,17 @@ private void removeLastBracket() {
lastBracket = lastBracket.previous;
}

private void disallowPreviousLinks() {
Bracket bracket = lastBracket;
while (bracket != null) {
if (bracket.markerNode == null) {
// Disallow link opener. It will still get matched, but will not result in a link.
bracket.allowed = false;
}
bracket = bracket.previous;
}
}

/**
* Try to parse the destination and an optional title for an inline link/image.
*/
Expand Down