Skip to content

Commit c6b4275

Browse files
committed
Inline footnotes rendering finished
1 parent 257e4a4 commit c6b4275

3 files changed

Lines changed: 134 additions & 16 deletions

File tree

commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteHtmlNodeRenderer.java

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public class FootnoteHtmlNodeRenderer implements NodeRenderer {
6767
* Information about references that should be rendered as footnotes. This doesn't contain all references, just the
6868
* ones from inside definitions.
6969
*/
70-
private final Map<FootnoteReference, ReferenceInfo> references = new HashMap<>();
70+
private final Map<Node, ReferenceInfo> references = new HashMap<>();
7171

7272
public FootnoteHtmlNodeRenderer(HtmlNodeRendererContext context) {
7373
this.html = context.getWriter();
@@ -91,6 +91,7 @@ public void beforeRoot(Node node) {
9191
public void render(Node node) {
9292
if (node instanceof FootnoteReference) {
9393
// This is called for all references, even ones inside definitions that we render at the end.
94+
// Inside definitions, we have registered the reference already.
9495
var ref = (FootnoteReference) node;
9596
// Use containsKey because if value is null, we don't need to try registering again.
9697
var info = references.containsKey(ref) ? references.get(ref) : tryRegisterReference(ref);
@@ -101,13 +102,16 @@ public void render(Node node) {
101102
html.text("[^" + ref.getLabel() + "]");
102103
}
103104
} else if (node instanceof InlineFootnote) {
104-
var info = registerReference(node, null);
105+
var info = references.get(node);
106+
if (info == null) {
107+
info = registerReference(node, null);
108+
}
105109
renderReference(node, info);
106110
}
107111
}
108112

109113
@Override
110-
public void afterRoot(Node node) {
114+
public void afterRoot(Node rootNode) {
111115
// Now render the referenced definitions if there are any.
112116
if (referencedDefinitions.isEmpty()) {
113117
return;
@@ -127,13 +131,19 @@ public void afterRoot(Node node) {
127131
var check = new LinkedList<>(referencedDefinitions.keySet());
128132
while (!check.isEmpty()) {
129133
var def = check.removeFirst();
130-
def.accept(new ReferenceVisitor(ref -> {
131-
var d = definitionMap.get(ref.getLabel());
132-
if (d != null) {
133-
if (!referencedDefinitions.containsKey(d)) {
134-
check.addLast(d);
134+
def.accept(new ShallowReferenceVisitor(def, node -> {
135+
if (node instanceof FootnoteReference) {
136+
var ref = (FootnoteReference) node;
137+
var d = definitionMap.get(ref.getLabel());
138+
if (d != null) {
139+
if (!referencedDefinitions.containsKey(d)) {
140+
check.addLast(d);
141+
}
142+
references.put(ref, registerReference(d, d.getLabel()));
135143
}
136-
references.put(ref, registerReference(d, d.getLabel()));
144+
} else if (node instanceof InlineFootnote) {
145+
check.addLast(node);
146+
references.put(node, registerReference(node, null));
137147
}
138148
}));
139149
}
@@ -307,18 +317,31 @@ public void visit(CustomBlock customBlock) {
307317
}
308318
}
309319

310-
private static class ReferenceVisitor extends AbstractVisitor {
311-
private final Consumer<FootnoteReference> consumer;
320+
/**
321+
* Visit footnote references/inline footnotes inside the parent (but not the parent itself). We want a shallow visit
322+
* because the caller wants to control when to descend.
323+
*/
324+
private static class ShallowReferenceVisitor extends AbstractVisitor {
325+
private final Node parent;
326+
private final Consumer<Node> consumer;
312327

313-
private ReferenceVisitor(Consumer<FootnoteReference> consumer) {
328+
private ShallowReferenceVisitor(Node parent, Consumer<Node> consumer) {
329+
this.parent = parent;
314330
this.consumer = consumer;
315331
}
316332

317333
@Override
318334
public void visit(CustomNode customNode) {
319335
if (customNode instanceof FootnoteReference) {
320-
var ref = (FootnoteReference) customNode;
321-
consumer.accept(ref);
336+
consumer.accept(customNode);
337+
} else if (customNode instanceof InlineFootnote) {
338+
if (customNode == parent) {
339+
// Descend into the parent (inline footnotes can contain inline footnotes)
340+
super.visit(customNode);
341+
} else {
342+
// Don't descend here because we want to be shallow.
343+
consumer.accept(customNode);
344+
}
322345
} else {
323346
super.visit(customNode);
324347
}

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

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,87 @@ public void testInlineFootnotes() {
221221
"</section>\n");
222222

223223
// TODO: Tricky ones like inline footnote referencing a normal one, and a definition containing an inline one.
224-
// TODO: Nested inline footnotes?
225-
// TODO: Inline and normal ones mixed, keys need to be unique
226224
}
227225

226+
@Test
227+
public void testInlineFootnotesNested() {
228+
assertRenderingInline("Test ^[inline ^[nested]]",
229+
"<p>Test <sup class=\"footnote-ref\"><a href=\"#fn1\" id=\"fnref1\" data-footnote-ref>1</a></sup></p>\n" +
230+
"<section class=\"footnotes\" data-footnotes>\n" +
231+
"<ol>\n" +
232+
"<li id=\"fn1\">\n" +
233+
"<p>inline <sup class=\"footnote-ref\"><a href=\"#fn2\" id=\"fnref2\" data-footnote-ref>2</a></sup> <a href=\"#fnref1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n" +
234+
"</li>\n" +
235+
"<li id=\"fn2\">\n" +
236+
"<p>nested <a href=\"#fnref2\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"2\" aria-label=\"Back to reference 2\">↩</a></p>\n" +
237+
"</li>\n" +
238+
"</ol>\n" +
239+
"</section>\n");
240+
}
241+
242+
@Test
243+
public void testInlineFootnoteWithReference() {
244+
// This is a bit tricky because the IDs need to be unique.
245+
assertRenderingInline("Test ^[inline [^1]]\n" +
246+
"\n" +
247+
"[^1]: normal",
248+
"<p>Test <sup class=\"footnote-ref\"><a href=\"#fn1\" id=\"fnref1\" data-footnote-ref>1</a></sup></p>\n" +
249+
"<section class=\"footnotes\" data-footnotes>\n" +
250+
"<ol>\n" +
251+
"<li id=\"fn1\">\n" +
252+
"<p>inline <sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\" data-footnote-ref>2</a></sup> <a href=\"#fnref1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n" +
253+
"</li>\n" +
254+
"<li id=\"fn-1\">\n" +
255+
"<p>normal <a href=\"#fnref-1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"2\" aria-label=\"Back to reference 2\">↩</a></p>\n" +
256+
"</li>\n" +
257+
"</ol>\n" +
258+
"</section>\n");
259+
}
260+
261+
@Test
262+
public void testInlineFootnoteInsideDefinition() {
263+
assertRenderingInline("Test [^1]\n" +
264+
"\n" +
265+
"[^1]: Definition ^[inline]\n",
266+
"<p>Test <sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\" data-footnote-ref>1</a></sup></p>\n" +
267+
"<section class=\"footnotes\" data-footnotes>\n" +
268+
"<ol>\n" +
269+
"<li id=\"fn-1\">\n" +
270+
"<p>Definition <sup class=\"footnote-ref\"><a href=\"#fn2\" id=\"fnref2\" data-footnote-ref>2</a></sup> <a href=\"#fnref-1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n" +
271+
"</li>\n" +
272+
"<li id=\"fn2\">\n" +
273+
"<p>inline <a href=\"#fnref2\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"2\" aria-label=\"Back to reference 2\">↩</a></p>\n" +
274+
"</li>\n" +
275+
"</ol>\n" +
276+
"</section>\n");
277+
}
278+
279+
@Test
280+
public void testInlineFootnoteInsideDefinition2() {
281+
// Tricky because of the nested inline footnote which we want to visit after foo (breadth-first).
282+
assertRenderingInline("Test [^1]\n" +
283+
"\n" +
284+
"[^1]: Definition ^[inline ^[nested]] ^[foo]\n",
285+
"<p>Test <sup class=\"footnote-ref\"><a href=\"#fn-1\" id=\"fnref-1\" data-footnote-ref>1</a></sup></p>\n" +
286+
"<section class=\"footnotes\" data-footnotes>\n" +
287+
"<ol>\n" +
288+
"<li id=\"fn-1\">\n" +
289+
"<p>Definition <sup class=\"footnote-ref\"><a href=\"#fn2\" id=\"fnref2\" data-footnote-ref>2</a></sup> <sup class=\"footnote-ref\"><a href=\"#fn3\" id=\"fnref3\" data-footnote-ref>3</a></sup> <a href=\"#fnref-1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n" +
290+
"</li>\n" +
291+
"<li id=\"fn2\">\n" +
292+
"<p>inline <sup class=\"footnote-ref\"><a href=\"#fn4\" id=\"fnref4\" data-footnote-ref>4</a></sup> <a href=\"#fnref2\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"2\" aria-label=\"Back to reference 2\">↩</a></p>\n" +
293+
"</li>\n" +
294+
"<li id=\"fn3\">\n" +
295+
"<p>foo <a href=\"#fnref3\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"3\" aria-label=\"Back to reference 3\">↩</a></p>\n" +
296+
"</li>\n" +
297+
"<li id=\"fn4\">\n" +
298+
"<p>nested <a href=\"#fnref4\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"4\" aria-label=\"Back to reference 4\">↩</a></p>\n" +
299+
"</li>\n" +
300+
"</ol>\n" +
301+
"</section>\n");
302+
}
303+
304+
228305
@Test
229306
public void testRenderNodesDirectly() {
230307
// Everything should work as expected when rendering from nodes directly (no parsing step).
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
3+
<html lang="en">
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Footnotes testing</title>
7+
<style>
8+
:target {
9+
background-color: #ffffaa;
10+
}
11+
</style>
12+
</head>
13+
<body>
14+
15+
Paste HTML from footnote rendering in here to manually check that linking works as expected.
16+
17+
</body>
18+
</html>

0 commit comments

Comments
 (0)