Skip to content

Commit e60be15

Browse files
authored
Merge pull request #402 from commonmark/footnotes-within-links
Disallow link when a footnote is nested inside it
2 parents 4ad81e4 + 926013c commit e60be15

File tree

3 files changed

+53
-10
lines changed

3 files changed

+53
-10
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
77
with the exception that 0.x versions can break between minor versions.
88

99
## Unreleased
10+
### Changed
11+
- A `LinkProcessor` using `replaceWith` now also stops outer links from being
12+
parsed as links, same as with `wrapTextIn`. This prevents nested links, see
13+
footnotes change below.
1014
### Fixed
11-
1215
- Fix rendering of image alt text to include contents of code spans (`` `code` ``). (#398)
16+
- footnotes: Fix footnotes nested within links. Before, both the link and the
17+
footnote reference would be parsed and lead to nested `<a>` elements, which
18+
is disallowed. Now, only the footnote is parsed and the outer link becomes
19+
plain text; this matches the behavior of links. (#400)
1320

1421
## [0.25.1] - 2025-08-01
1522
### Fixed

commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnotesTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,29 @@ public void testReferenceLinkWithoutDefinition() {
254254
assertText("[foo]", paragraph.getLastChild());
255255
}
256256

257+
@Test
258+
public void testFootnoteInLink() {
259+
// Expected to behave the same way as a link within a link, see https://spec.commonmark.org/0.31.2/#example-518
260+
// i.e. the first (inner) link is parsed, which means the outer one becomes plain text, as nesting links is not
261+
// allowed.
262+
var doc = PARSER.parse("[link with footnote ref [^1]](https://example.com)\n\n[^1]: footnote\n");
263+
var ref = find(doc, FootnoteReference.class);
264+
assertThat(ref.getLabel()).isEqualTo("1");
265+
var paragraph = doc.getFirstChild();
266+
assertText("[link with footnote ref ", paragraph.getFirstChild());
267+
assertText("](https://example.com)", paragraph.getLastChild());
268+
}
269+
270+
@Test
271+
public void testFootnoteWithMarkerInLink() {
272+
var doc = PARSER.parse("[link with footnote ref ![^1]](https://example.com)\n\n[^1]: footnote\n");
273+
var ref = find(doc, FootnoteReference.class);
274+
assertThat(ref.getLabel()).isEqualTo("1");
275+
var paragraph = doc.getFirstChild();
276+
assertText("[link with footnote ref !", paragraph.getFirstChild());
277+
assertText("](https://example.com)", paragraph.getLastChild());
278+
}
279+
257280
@Test
258281
public void testInlineFootnote() {
259282
var extension = FootnotesExtension.builder().inlineFootnotes(true).build();

commonmark/src/main/java/org/commonmark/internal/InlineParserImpl.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -440,16 +440,9 @@ private Node wrapBracket(Bracket opener, Node wrapperNode, boolean includeMarker
440440
opener.bracketNode.unlink();
441441
removeLastBracket();
442442

443-
// Links within links are not allowed. We found this link, so there can be no other link around it.
443+
// Links within links are not allowed. We found this link, so there can be no other links around it.
444444
if (opener.markerNode == null) {
445-
Bracket bracket = lastBracket;
446-
while (bracket != null) {
447-
if (bracket.markerNode == null) {
448-
// Disallow link opener. It will still get matched, but will not result in a link.
449-
bracket.allowed = false;
450-
}
451-
bracket = bracket.previous;
452-
}
445+
disallowPreviousLinks();
453446
}
454447

455448
return wrapperNode;
@@ -475,6 +468,15 @@ private Node replaceBracket(Bracket opener, Node node, boolean includeMarker) {
475468
n.unlink();
476469
n = next;
477470
}
471+
472+
// Links within links are not allowed. We found this link, so there can be no other links around it.
473+
// Note that this makes any syntax like `[foo]` behave the same as built-in links, which is probably a good
474+
// default (it works for footnotes). It might be useful for a `LinkProcessor` to be able to specify the
475+
// behavior; something we could add to `LinkResult` in the future if requested.
476+
if (opener.markerNode == null || !includeMarker) {
477+
disallowPreviousLinks();
478+
}
479+
478480
return node;
479481
}
480482

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

494+
private void disallowPreviousLinks() {
495+
Bracket bracket = lastBracket;
496+
while (bracket != null) {
497+
if (bracket.markerNode == null) {
498+
// Disallow link opener. It will still get matched, but will not result in a link.
499+
bracket.allowed = false;
500+
}
501+
bracket = bracket.previous;
502+
}
503+
}
504+
492505
/**
493506
* Try to parse the destination and an optional title for an inline link/image.
494507
*/

0 commit comments

Comments
 (0)