Skip to content

Commit 257e4a4

Browse files
committed
Inline footnotes rendering (first part)
1 parent be9a27f commit 257e4a4

2 files changed

Lines changed: 85 additions & 31 deletions

File tree

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

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.commonmark.ext.footnotes.FootnoteDefinition;
44
import org.commonmark.ext.footnotes.FootnoteReference;
5+
import org.commonmark.ext.footnotes.InlineFootnote;
56
import org.commonmark.node.*;
67
import org.commonmark.renderer.NodeRenderer;
78
import org.commonmark.renderer.html.HtmlNodeRendererContext;
@@ -60,7 +61,7 @@ public class FootnoteHtmlNodeRenderer implements NodeRenderer {
6061
/**
6162
* Definitions that were referenced, in order in which they should be rendered.
6263
*/
63-
private final Map<FootnoteDefinition, ReferencedDefinition> referencedDefinitions = new LinkedHashMap<>();
64+
private final Map<Node, ReferencedDefinition> referencedDefinitions = new LinkedHashMap<>();
6465

6566
/**
6667
* Information about references that should be rendered as footnotes. This doesn't contain all references, just the
@@ -75,7 +76,7 @@ public FootnoteHtmlNodeRenderer(HtmlNodeRendererContext context) {
7576

7677
@Override
7778
public Set<Class<? extends Node>> getNodeTypes() {
78-
return Set.of(FootnoteReference.class, FootnoteDefinition.class);
79+
return Set.of(FootnoteReference.class, InlineFootnote.class, FootnoteDefinition.class);
7980
}
8081

8182
@Override
@@ -92,8 +93,16 @@ public void render(Node node) {
9293
// This is called for all references, even ones inside definitions that we render at the end.
9394
var ref = (FootnoteReference) node;
9495
// Use containsKey because if value is null, we don't need to try registering again.
95-
var info = references.containsKey(ref) ? references.get(ref) : registerReference(ref);
96-
renderReference(ref, info);
96+
var info = references.containsKey(ref) ? references.get(ref) : tryRegisterReference(ref);
97+
if (info != null) {
98+
renderReference(ref, info);
99+
} else {
100+
// A reference without a corresponding definition is rendered as plain text
101+
html.text("[^" + ref.getLabel() + "]");
102+
}
103+
} else if (node instanceof InlineFootnote) {
104+
var info = registerReference(node, null);
105+
renderReference(node, info);
97106
}
98107
}
99108

@@ -114,7 +123,7 @@ public void afterRoot(Node node) {
114123
html.line();
115124

116125
// Check whether there are any footnotes inside the definitions that we're about to render. For those, we might
117-
// need to render more definitions. So do a breadth-first search to find all relevant definition.
126+
// need to render more definitions. So do a breadth-first search to find all relevant definitions.
118127
var check = new LinkedList<>(referencedDefinitions.keySet());
119128
while (!check.isEmpty()) {
120129
var def = check.removeFirst();
@@ -124,7 +133,7 @@ public void afterRoot(Node node) {
124133
if (!referencedDefinitions.containsKey(d)) {
125134
check.addLast(d);
126135
}
127-
references.put(ref, registerReference(ref));
136+
references.put(ref, registerReference(d, d.getLabel()));
128137
}
129138
}));
130139
}
@@ -140,52 +149,50 @@ public void afterRoot(Node node) {
140149
html.line();
141150
}
142151

143-
private ReferenceInfo registerReference(FootnoteReference ref) {
152+
private ReferenceInfo tryRegisterReference(FootnoteReference ref) {
144153
var def = definitionMap.get(ref.getLabel());
145154
if (def == null) {
146155
return null;
147156
}
157+
return registerReference(def, def.getLabel());
158+
}
148159

160+
private ReferenceInfo registerReference(Node node, String label) {
149161
// The first referenced definition gets number 1, second one 2, etc.
150-
var referencedDef = referencedDefinitions.computeIfAbsent(def, k -> new ReferencedDefinition(referencedDefinitions.size() + 1));
162+
var referencedDef = referencedDefinitions.computeIfAbsent(node, k -> {
163+
var num = referencedDefinitions.size() + 1;
164+
var key = definitionKey(label, num);
165+
return new ReferencedDefinition(num, key);
166+
});
151167
var definitionNumber = referencedDef.definitionNumber;
152168
// The reference number for that particular definition. E.g. if there's two references for the same definition,
153169
// the first one is 1, the second one 2, etc. This is needed to give each reference a unique ID so that each
154170
// reference can get its own backlink from the definition.
155171
var refNumber = referencedDef.references.size() + 1;
156-
var id = referenceId(def.getLabel(), refNumber);
172+
var definitionKey = referencedDef.definitionKey;
173+
var id = referenceId(definitionKey, refNumber);
157174
referencedDef.references.add(id);
158175

159-
var definitionId = definitionId(def.getLabel());
160-
161-
return new ReferenceInfo(id, definitionId, definitionNumber);
176+
return new ReferenceInfo(id, definitionId(definitionKey), definitionNumber);
162177
}
163178

164-
private void renderReference(FootnoteReference ref, ReferenceInfo referenceInfo) {
165-
if (referenceInfo == null) {
166-
// A reference without a corresponding definition is rendered as plain text
167-
html.text("[^" + ref.getLabel() + "]");
168-
return;
169-
}
170-
171-
html.tag("sup", context.extendAttributes(ref, "sup", Map.of("class", "footnote-ref")));
179+
private void renderReference(Node node, ReferenceInfo referenceInfo) {
180+
html.tag("sup", context.extendAttributes(node, "sup", Map.of("class", "footnote-ref")));
172181

173182
var href = "#" + referenceInfo.definitionId;
174183
var attrs = new LinkedHashMap<String, String>();
175184
attrs.put("href", href);
176185
attrs.put("id", referenceInfo.id);
177186
attrs.put("data-footnote-ref", null);
178-
html.tag("a", context.extendAttributes(ref, "a", attrs));
187+
html.tag("a", context.extendAttributes(node, "a", attrs));
179188
html.raw(String.valueOf(referenceInfo.definitionNumber));
180189
html.tag("/a");
181190
html.tag("/sup");
182191
}
183192

184-
private void renderDefinition(FootnoteDefinition def, ReferencedDefinition referencedDefinition) {
185-
// <ol> etc
186-
var id = definitionId(def.getLabel());
193+
private void renderDefinition(Node def, ReferencedDefinition referencedDefinition) {
187194
var attrs = new LinkedHashMap<String, String>();
188-
attrs.put("id", id);
195+
attrs.put("id", definitionId(referencedDefinition.definitionKey));
189196
html.tag("li", context.extendAttributes(def, "li", attrs));
190197
html.line();
191198

@@ -213,6 +220,13 @@ private void renderDefinition(FootnoteDefinition def, ReferencedDefinition refer
213220
renderBackrefs(def, referencedDefinition);
214221
html.tag("/p");
215222
html.line();
223+
} else if (def instanceof InlineFootnote) {
224+
html.tag("p", context.extendAttributes(def, "p", Map.of()));
225+
renderChildren(def);
226+
html.raw(" ");
227+
renderBackrefs(def, referencedDefinition);
228+
html.tag("/p");
229+
html.line();
216230
} else {
217231
renderChildren(def);
218232
html.line();
@@ -223,7 +237,7 @@ private void renderDefinition(FootnoteDefinition def, ReferencedDefinition refer
223237
html.line();
224238
}
225239

226-
private void renderBackrefs(FootnoteDefinition def, ReferencedDefinition referencedDefinition) {
240+
private void renderBackrefs(Node def, ReferencedDefinition referencedDefinition) {
227241
var refs = referencedDefinition.references;
228242
for (int i = 0; i < refs.size(); i++) {
229243
var ref = refs.get(i);
@@ -251,12 +265,22 @@ private void renderBackrefs(FootnoteDefinition def, ReferencedDefinition referen
251265
}
252266
}
253267

254-
private String referenceId(String label, int number) {
255-
return "fnref-" + label + (number == 1 ? "" : ("-" + number));
268+
private String referenceId(String definitionKey, int number) {
269+
return "fnref" + definitionKey + (number == 1 ? "" : ("-" + number));
256270
}
257271

258-
private String definitionId(String label) {
259-
return "fn-" + label;
272+
private String definitionKey(String label, int number) {
273+
// Named definitions use the pattern "fn-{name}" and inline definitions use "fn{number}" so as not to conflict.
274+
// "fn{number}" is also what pandoc uses (for all types), starting with number 1.
275+
if (label != null) {
276+
return "-" + label;
277+
} else {
278+
return "" + number;
279+
}
280+
}
281+
282+
private String definitionId(String definitionKey) {
283+
return "fn" + definitionKey;
260284
}
261285

262286
private void renderChildren(Node parent) {
@@ -306,13 +330,18 @@ private static class ReferencedDefinition {
306330
* The definition number, starting from 1, and in order in which they're referenced.
307331
*/
308332
final int definitionNumber;
333+
/**
334+
* The unique key of the definition. Together with a static prefix it forms the ID used in the HTML.
335+
*/
336+
final String definitionKey;
309337
/**
310338
* The IDs of references for this definition, for backrefs.
311339
*/
312340
final List<String> references = new ArrayList<>();
313341

314-
ReferencedDefinition(int definitionNumber) {
342+
ReferencedDefinition(int definitionNumber, String definitionKey) {
315343
this.definitionNumber = definitionNumber;
344+
this.definitionKey = definitionKey;
316345
}
317346
}
318347

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.commonmark.testutil.RenderingTestCase;
1111
import org.junit.Test;
1212

13+
import java.util.List;
1314
import java.util.Set;
1415

1516
public class FootnoteHtmlRendererTest extends RenderingTestCase {
@@ -207,6 +208,23 @@ public void testNestedFootnotesUnreferenced() {
207208
"</section>\n");
208209
}
209210

211+
@Test
212+
public void testInlineFootnotes() {
213+
assertRenderingInline("Test ^[inline *footnote*]",
214+
"<p>Test <sup class=\"footnote-ref\"><a href=\"#fn1\" id=\"fnref1\" data-footnote-ref>1</a></sup></p>\n" +
215+
"<section class=\"footnotes\" data-footnotes>\n" +
216+
"<ol>\n" +
217+
"<li id=\"fn1\">\n" +
218+
"<p>inline <em>footnote</em> <a href=\"#fnref1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n" +
219+
"</li>\n" +
220+
"</ol>\n" +
221+
"</section>\n");
222+
223+
// 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
226+
}
227+
210228
@Test
211229
public void testRenderNodesDirectly() {
212230
// Everything should work as expected when rendering from nodes directly (no parsing step).
@@ -236,4 +254,11 @@ public void testRenderNodesDirectly() {
236254
protected String render(String source) {
237255
return RENDERER.render(PARSER.parse(source));
238256
}
257+
258+
private static void assertRenderingInline(String source, String expected) {
259+
var extension = FootnotesExtension.builder().inlineFootnotes(true).build();
260+
var parser = Parser.builder().extensions(List.of(extension)).build();
261+
var renderer = HtmlRenderer.builder().extensions(List.of(extension)).build();
262+
Asserts.assertRendering(source, expected, renderer.render(parser.parse(source)));
263+
}
239264
}

0 commit comments

Comments
 (0)