Skip to content

Commit 0af05f5

Browse files
footnote: fix idx reset (#1496)
1 parent 469614d commit 0af05f5

8 files changed

Lines changed: 105 additions & 22 deletions

File tree

znai-core/src/main/java/org/testingisdocumenting/znai/extensions/footnote/ParsedFootnote.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import org.commonmark.ext.footnotes.FootnoteDefinition;
2020
import org.testingisdocumenting.znai.core.ComponentsRegistry;
21+
import org.testingisdocumenting.znai.parser.MarkdownParsingContext;
2122
import org.testingisdocumenting.znai.parser.ParserHandlersList;
2223
import org.testingisdocumenting.znai.parser.commonmark.MarkdownVisitor;
2324
import org.testingisdocumenting.znai.parser.docelement.DocElement;
@@ -29,22 +30,22 @@
2930
import java.util.List;
3031
import java.util.stream.Collectors;
3132

32-
public record ParsedFootnote(FootnoteId id, DocElement docElement, List<PageSearchEntry> searchEntries) {
33-
public static ParsedFootnote parse(ComponentsRegistry componentsRegistry, Path markdownPath, FootnoteDefinition footnote) {
33+
public record ParsedFootnote(FootnoteId id, int idx, DocElement docElement, List<PageSearchEntry> searchEntries) {
34+
public static ParsedFootnote parse(ComponentsRegistry componentsRegistry, Path markdownPath, FootnoteDefinition footnote, int idx) {
3435
var searchHandler = new SearchCrawlerParserHandler();
3536
DocElementCreationParserHandler docElementsHandler = new DocElementCreationParserHandler(componentsRegistry, markdownPath);
3637
var parserHandler = new ParserHandlersList(
3738
docElementsHandler,
3839
searchHandler
3940
);
4041

41-
var visitor = new MarkdownVisitor(componentsRegistry, markdownPath, parserHandler);
42+
var visitor = new MarkdownVisitor(componentsRegistry, markdownPath, new MarkdownParsingContext(), parserHandler);
4243
visit(visitor, footnote);
4344

4445
List<PageSearchEntry> searchEntries = searchHandler.getSearchEntries();
4546
DocElement docElement = docElementsHandler.getDocElement();
4647

47-
return new ParsedFootnote(new FootnoteId(footnote.getLabel()), docElement, searchEntries);
48+
return new ParsedFootnote(new FootnoteId(footnote.getLabel()), idx, docElement, searchEntries);
4849
}
4950

5051
private static void visit(MarkdownVisitor visitor, FootnoteDefinition footnote) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2026 znai maintainers
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.testingisdocumenting.znai.parser;
18+
19+
public class MarkdownParsingContext {
20+
private int footnoteIdx;
21+
22+
public int nextFootnoteIdx() {
23+
return ++footnoteIdx;
24+
}
25+
}

znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/MarkdownParser.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.testingisdocumenting.znai.core.Log;
3030
import org.testingisdocumenting.znai.extensions.PluginParamWarning;
3131
import org.testingisdocumenting.znai.extensions.PluginParamsFactory;
32+
import org.testingisdocumenting.znai.parser.MarkdownParsingContext;
3233
import org.testingisdocumenting.znai.parser.MarkupParser;
3334
import org.testingisdocumenting.znai.parser.MarkupParserResult;
3435
import org.testingisdocumenting.znai.parser.ParserHandler;
@@ -45,6 +46,8 @@
4546
import org.commonmark.parser.Parser;
4647

4748
public class MarkdownParser implements MarkupParser {
49+
private static final ThreadLocal<MarkdownParsingContext> currentParsingContext = new ThreadLocal<>();
50+
4851
private final Parser fullParser;
4952
private final Parser metaOnlyParser;
5053
private final ComponentsRegistry componentsRegistry;
@@ -58,6 +61,24 @@ public MarkdownParser(ComponentsRegistry componentsRegistry) {
5861
}
5962

6063
public MarkupParserResult parse(Path path, String markdown) {
64+
MarkdownParsingContext existing = currentParsingContext.get();
65+
boolean isTopLevel = existing == null;
66+
MarkdownParsingContext parsingContext = isTopLevel ? new MarkdownParsingContext() : existing;
67+
68+
if (isTopLevel) {
69+
currentParsingContext.set(parsingContext);
70+
}
71+
72+
try {
73+
return doParse(path, parsingContext, markdown);
74+
} finally {
75+
if (isTopLevel) {
76+
currentParsingContext.remove();
77+
}
78+
}
79+
}
80+
81+
private MarkupParserResult doParse(Path path, MarkdownParsingContext parsingContext, String markdown) {
6182
SearchCrawlerParserHandler searchCrawler = new SearchCrawlerParserHandler();
6283
DocElementCreationParserHandler elementCreationHandler =
6384
new DocElementCreationParserHandler(componentsRegistry, path);
@@ -66,7 +87,7 @@ public MarkupParserResult parse(Path path, String markdown) {
6687
ParserHandlersList parserHandler = new ParserHandlersList(elementCreationHandler, searchCrawler, markdownGenerator);
6788

6889
Node node = fullParser.parse(markdown);
69-
MarkdownVisitor visitor = parsePartial(node, path, parserHandler);
90+
MarkdownVisitor visitor = parsePartial(node, path, parsingContext, parserHandler);
7091

7192
if (!visitor.getUnresolvedFootnoteRefs().isEmpty()) {
7293
throw new IllegalArgumentException("undefined footnote reference(s): " +
@@ -99,11 +120,11 @@ public PageMeta parsePageMetaOnly(String markdown) {
99120

100121
public void parse(Path path, ParserHandler handler, String markdown) {
101122
Node node = fullParser.parse(markdown);
102-
parsePartial(node, path, handler);
123+
parsePartial(node, path, new MarkdownParsingContext(), handler);
103124
}
104125

105-
private MarkdownVisitor parsePartial(Node node, Path path, ParserHandler handler) {
106-
MarkdownVisitor visitor = new MarkdownVisitor(componentsRegistry, path, handler);
126+
private MarkdownVisitor parsePartial(Node node, Path path, MarkdownParsingContext parsingContext, ParserHandler handler) {
127+
MarkdownVisitor visitor = new MarkdownVisitor(componentsRegistry, path, parsingContext, handler);
107128
node.accept(visitor);
108129

109130
return visitor;

znai-core/src/main/java/org/testingisdocumenting/znai/parser/commonmark/MarkdownVisitor.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.testingisdocumenting.znai.extensions.fence.FencePlugin;
2525
import org.testingisdocumenting.znai.extensions.footnote.FootnoteId;
2626
import org.testingisdocumenting.znai.extensions.footnote.ParsedFootnote;
27+
import org.testingisdocumenting.znai.parser.MarkdownParsingContext;
2728
import org.testingisdocumenting.znai.extensions.include.IncludePlugin;
2829
import org.testingisdocumenting.znai.extensions.inlinedcode.InlinedCodePlugin;
2930
import org.testingisdocumenting.znai.extensions.latex.LatexDollarBlock;
@@ -52,15 +53,17 @@ public class MarkdownVisitor extends AbstractVisitor {
5253

5354
private final ComponentsRegistry componentsRegistry;
5455
private final Path path;
56+
private final MarkdownParsingContext parsingContext;
5557
private final ParserHandler parserHandler;
5658
private boolean sectionStarted;
5759

5860
private final Set<PluginParamWarning> parameterWarnings;
5961
private final Set<String> unresolvedFootnoteRefs;
6062

61-
public MarkdownVisitor(ComponentsRegistry componentsRegistry, Path path, ParserHandler parserHandler) {
63+
public MarkdownVisitor(ComponentsRegistry componentsRegistry, Path path, MarkdownParsingContext parsingContext, ParserHandler parserHandler) {
6264
this.componentsRegistry = componentsRegistry;
6365
this.path = path;
66+
this.parsingContext = parsingContext;
6467
this.parserHandler = parserHandler;
6568
this.parameterWarnings = new LinkedHashSet<>();
6669
this.unresolvedFootnoteRefs = new LinkedHashSet<>();
@@ -181,7 +184,7 @@ public void visit(CustomBlock customBlock) {
181184
GfmTableToTableConverter gfmTableToTableConverter = new GfmTableToTableConverter(componentsRegistry, path, (TableBlock) customBlock);
182185
parserHandler.onTable(gfmTableToTableConverter.convert());
183186
} else if (customBlock instanceof FootnoteDefinition footnote) {
184-
ParsedFootnote parsed = ParsedFootnote.parse(componentsRegistry, path, footnote);
187+
ParsedFootnote parsed = ParsedFootnote.parse(componentsRegistry, path, footnote, parsingContext.nextFootnoteIdx());
185188
parserHandler.onFootnoteDefinition(parsed);
186189
} else {
187190
throw new UnsupportedOperationException("unsupported custom block: " + customBlock);

znai-core/src/main/java/org/testingisdocumenting/znai/parser/docelement/DocElementCreationParserHandler.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ public class DocElementCreationParserHandler implements ParserHandler {
6161
private final Deque<DocElement> elementsStack;
6262

6363
private final Map<FootnoteId, ParsedFootnote> parsedFootnotes;
64-
private final Map<FootnoteId, Integer> footnoteIdxById;
65-
private int footnoteAutoIdx;
6664
private String currentSectionTitle;
6765

6866
private boolean isSectionStarted;
@@ -76,8 +74,6 @@ public DocElementCreationParserHandler(ComponentsRegistry componentsRegistry, Pa
7674
this.globalAnchorIds = new ArrayList<>();
7775

7876
this.parsedFootnotes = new HashMap<>();
79-
this.footnoteAutoIdx = 0;
80-
this.footnoteIdxById = new HashMap<>();
8177

8278
this.docElement = new DocElement(DocElementType.PAGE);
8379
this.elementsStack = new ArrayDeque<>();
@@ -224,15 +220,12 @@ public void onTable(MarkupTableData tableData) {
224220
@Override
225221
public void onFootnoteDefinition(ParsedFootnote footnote) {
226222
parsedFootnotes.put(footnote.id(), footnote);
227-
228-
int indexToUse = ++footnoteAutoIdx;
229-
footnoteIdxById.put(footnote.id(), indexToUse);
230223
}
231224

232225
@Override
233226
public void onFootnoteReference(FootnoteId footnoteId) {
234227
append(new DocElement( "FootnoteReference",
235-
"label", (Supplier<?>) (() -> Integer.toString(footnoteIdxById.getOrDefault(footnoteId, 1))),
228+
"label", (Supplier<?>) (() -> footnoteLabel(footnoteId)),
236229
"content", (Supplier<?>) (() -> footnoteContent(footnoteId))));
237230
}
238231

@@ -577,13 +570,18 @@ private void addAnchorIdsToProps(Map<String, Object> props, AnchorIds ids) {
577570
props.put("additionalIds", ids.additional());
578571
}
579572

573+
private String footnoteLabel(FootnoteId footnoteId) {
574+
ParsedFootnote footnote = parsedFootnotes.get(footnoteId);
575+
return footnote != null ? Integer.toString(footnote.idx()) : "undefined";
576+
}
577+
580578
private List<Map<String, Object>> footnoteContent(FootnoteId footnoteId) {
581-
ParsedFootnote parsedFootnote = parsedFootnotes.get(footnoteId);
582-
if (parsedFootnote == null) {
579+
ParsedFootnote footnote = parsedFootnotes.get(footnoteId);
580+
if (footnote == null) {
583581
throw new IllegalArgumentException("can't find footnote with id <" + footnoteId + ">");
584582
}
585583

586-
return parsedFootnote.docElement().contentToListOfMaps();
584+
return footnote.docElement().contentToListOfMaps();
587585
}
588586

589587
private void addHeadingContentWhenNotSimple(Map<String, Object> props, Heading heading) {
@@ -633,4 +631,5 @@ public void visit(StrongEmphasis strongEmphasis) {
633631

634632
props.put("headingContent", content);
635633
}
634+
636635
}

znai-core/src/main/java/org/testingisdocumenting/znai/parser/table/GfmTableToTableConverter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.testingisdocumenting.znai.parser.table;
1919

2020
import org.testingisdocumenting.znai.core.ComponentsRegistry;
21+
import org.testingisdocumenting.znai.parser.MarkdownParsingContext;
2122
import org.testingisdocumenting.znai.parser.docelement.DocElementCreationParserHandler;
2223
import org.testingisdocumenting.znai.parser.commonmark.MarkdownVisitor;
2324
import org.commonmark.ext.gfm.tables.*;
@@ -102,7 +103,7 @@ private void handleBodyCell(TableCell bodyCell) {
102103
@SuppressWarnings("unchecked")
103104
private List<Map<String, Object>> contentFromCell(TableCell bodyCell) {
104105
DocElementCreationParserHandler handler = new DocElementCreationParserHandler(componentsRegistry, markdownPath);
105-
MarkdownVisitor markdownVisitor = new MarkdownVisitor(componentsRegistry, markdownPath, handler);
106+
MarkdownVisitor markdownVisitor = new MarkdownVisitor(componentsRegistry, markdownPath, new MarkdownParsingContext(), handler);
106107

107108
bodyCell.accept(markdownVisitor);
108109

znai-core/src/test/groovy/org/testingisdocumenting/znai/parser/MarkdownParserTest.groovy

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,38 @@ after footnote
637637
]
638638
}
639639

640+
@Test
641+
void "footnotes inside fence plugin should continue numbering"() {
642+
def previousDefaultParser = componentsRegistry.defaultParser()
643+
componentsRegistry.setDefaultParser(parser)
644+
645+
try {
646+
parse("""
647+
text before [^first]
648+
649+
[^first]: first footnote
650+
651+
~~~attention-note
652+
inner text [^second]
653+
654+
[^second]: second footnote
655+
~~~
656+
""")
657+
658+
def content = PropsUtils.exerciseSuppliers(content)
659+
660+
def firstRef = content[0].content.find { it.type == "FootnoteReference" }
661+
firstRef.label.should == "1"
662+
663+
def attentionBlock = content.find { it.type == "AttentionBlock" }
664+
def innerParagraph = attentionBlock.content[0]
665+
def secondRef = innerParagraph.content.find { it.type == "FootnoteReference" }
666+
secondRef.label.should == "2"
667+
} finally {
668+
componentsRegistry.setDefaultParser(previousDefaultParser)
669+
}
670+
}
671+
640672
@Test
641673
void "undefined footnote reference"() {
642674
code {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Fix: footnote idx is no longer reset when footnotes are defined inside plugins like `readmore`

0 commit comments

Comments
 (0)