Skip to content

Commit 163a484

Browse files
add node depth limit
1 parent e62bca1 commit 163a484

File tree

22 files changed

+821
-53
lines changed

22 files changed

+821
-53
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public Set<Class<? extends Node>> getNodeTypes() {
8282
@Override
8383
public void beforeRoot(Node rootNode) {
8484
// Collect all definitions first, so we can look them up when encountering a reference later.
85-
var visitor = new DefinitionVisitor();
85+
var visitor = new DefinitionVisitor(context.getNodeTraversal(rootNode));
8686
rootNode.accept(visitor);
8787
definitionMap = visitor.definitions;
8888
}
@@ -145,7 +145,7 @@ public void afterRoot(Node rootNode) {
145145
check.addLast(node);
146146
references.put(node, registerReference(node, null));
147147
}
148-
}));
148+
}, context.getNodeTraversal(def)));
149149
}
150150

151151
for (var entry : referencedDefinitions.entrySet()) {
@@ -306,6 +306,10 @@ private static class DefinitionVisitor extends AbstractVisitor {
306306

307307
private final DefinitionMap<FootnoteDefinition> definitions = new DefinitionMap<>(FootnoteDefinition.class);
308308

309+
private DefinitionVisitor(NodeTraversal traversal) {
310+
super(traversal);
311+
}
312+
309313
@Override
310314
public void visit(CustomBlock customBlock) {
311315
if (customBlock instanceof FootnoteDefinition) {
@@ -325,7 +329,8 @@ private static class ShallowReferenceVisitor extends AbstractVisitor {
325329
private final Node parent;
326330
private final Consumer<Node> consumer;
327331

328-
private ShallowReferenceVisitor(Node parent, Consumer<Node> consumer) {
332+
private ShallowReferenceVisitor(Node parent, Consumer<Node> consumer, NodeTraversal traversal) {
333+
super(traversal);
329334
this.parent = parent;
330335
this.consumer = consumer;
331336
}

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

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

33
import org.commonmark.Extension;
44
import org.commonmark.node.Document;
5+
import org.commonmark.node.NodeTraversal;
56
import org.commonmark.node.Paragraph;
67
import org.commonmark.node.Text;
78
import org.commonmark.parser.Parser;
@@ -221,6 +222,25 @@ public void testInlineFootnotes() {
221222
"</section>\n");
222223
}
223224

225+
@Test
226+
public void testNestedFootnotesTraversalLimitUsesRemainingDefinitionBudget() {
227+
String source = "[^foo1]\n" +
228+
"\n" +
229+
"[^foo1]: one *[^foo2]*\n" +
230+
"[^foo2]: two\n";
231+
232+
String expected = "<p><sup class=\"footnote-ref\"><a href=\"#fn-foo1\" id=\"fnref-foo1\" data-footnote-ref>1</a></sup></p>\n" +
233+
"<section class=\"footnotes\" data-footnotes>\n" +
234+
"<ol>\n" +
235+
"<li id=\"fn-foo1\">\n" +
236+
"<p>one <em></em> <a href=\"#fnref-foo1\" class=\"footnote-backref\" data-footnote-backref data-footnote-backref-idx=\"1\" aria-label=\"Back to reference 1\">↩</a></p>\n" +
237+
"</li>\n" +
238+
"</ol>\n" +
239+
"</section>\n";
240+
241+
Asserts.assertRendering(source, expected, renderWithTraversal(source, NodeTraversal.succeedLossy(3)));
242+
}
243+
224244
@Test
225245
public void testInlineFootnotesNested() {
226246
assertRenderingInline("Test ^[inline ^[nested]]",
@@ -330,6 +350,14 @@ protected String render(String source) {
330350
return RENDERER.render(PARSER.parse(source));
331351
}
332352

353+
private static String renderWithTraversal(String source, NodeTraversal traversal) {
354+
HtmlRenderer renderer = HtmlRenderer.builder()
355+
.extensions(EXTENSIONS)
356+
.nodeTraversal(traversal)
357+
.build();
358+
return renderer.render(PARSER.parse(source));
359+
}
360+
333361
private static void assertRenderingInline(String source, String expected) {
334362
var extension = FootnotesExtension.builder().inlineFootnotes(true).build();
335363
var parser = Parser.builder().extensions(List.of(extension)).build();

commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/HeadingAnchorExtension.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void extend(HtmlRenderer.Builder rendererBuilder) {
5858
rendererBuilder.attributeProviderFactory(new AttributeProviderFactory() {
5959
@Override
6060
public AttributeProvider create(AttributeProviderContext context) {
61-
return HeadingIdAttributeProvider.create(defaultId, idPrefix, idSuffix);
61+
return HeadingIdAttributeProvider.create(defaultId, idPrefix, idSuffix, context);
6262
}
6363
});
6464
}

commonmark-ext-heading-anchor/src/main/java/org/commonmark/ext/heading/anchor/internal/HeadingIdAttributeProvider.java

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,46 @@
22

33
import org.commonmark.ext.heading.anchor.IdGenerator;
44
import org.commonmark.renderer.html.AttributeProvider;
5+
import org.commonmark.renderer.html.AttributeProviderContext;
56
import org.commonmark.node.*;
67

7-
import java.util.ArrayList;
8-
import java.util.List;
98
import java.util.Map;
109

1110
public class HeadingIdAttributeProvider implements AttributeProvider {
1211

1312
private final IdGenerator idGenerator;
13+
private final AttributeProviderContext context;
1414

15-
private HeadingIdAttributeProvider(String defaultId, String prefix, String suffix) {
15+
private HeadingIdAttributeProvider(String defaultId, String prefix, String suffix, AttributeProviderContext context) {
1616
idGenerator = IdGenerator.builder()
1717
.defaultId(defaultId)
1818
.prefix(prefix)
1919
.suffix(suffix)
2020
.build();
21+
this.context = context;
2122
}
2223

23-
public static HeadingIdAttributeProvider create(String defaultId, String prefix, String suffix) {
24-
return new HeadingIdAttributeProvider(defaultId, prefix, suffix);
24+
public static HeadingIdAttributeProvider create(String defaultId, String prefix, String suffix, AttributeProviderContext context) {
25+
return new HeadingIdAttributeProvider(defaultId, prefix, suffix, context);
2526
}
2627

2728
@Override
2829
public void setAttributes(Node node, String tagName, final Map<String, String> attributes) {
29-
3030
if (node instanceof Heading) {
31-
32-
final List<String> wordList = new ArrayList<>();
33-
34-
node.accept(new AbstractVisitor() {
31+
StringBuilder text = new StringBuilder();
32+
node.accept(new AbstractVisitor(context.getNodeTraversal(node)) {
3533
@Override
36-
public void visit(Text text) {
37-
wordList.add(text.getLiteral());
34+
public void visit(Text textNode) {
35+
text.append(textNode.getLiteral());
3836
}
3937

4038
@Override
4139
public void visit(Code code) {
42-
wordList.add(code.getLiteral());
40+
text.append(code.getLiteral());
4341
}
4442
});
4543

46-
String finalString = "";
47-
for (String word : wordList) {
48-
finalString += word;
49-
}
50-
finalString = finalString.trim().toLowerCase();
51-
52-
attributes.put("id", idGenerator.generateId(finalString));
44+
attributes.put("id", idGenerator.generateId(text.toString().trim().toLowerCase()));
5345
}
5446
}
5547
}

commonmark-ext-heading-anchor/src/test/java/org/commonmark/ext/heading/anchor/HeadingAnchorConfigurationTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.commonmark.ext.heading.anchor;
22

33
import org.commonmark.Extension;
4+
import org.commonmark.node.NodeTraversal;
45
import org.commonmark.parser.Parser;
56
import org.commonmark.renderer.html.HtmlRenderer;
67
import org.junit.jupiter.api.Test;
@@ -50,6 +51,16 @@ public void testSuffixAddedToHeader() {
5051
assertThat(doRender(renderer, "# text")).isEqualTo("<h1 id=\"text-post\">text</h1>\n");
5152
}
5253

54+
@Test
55+
public void testTraversalLimitAppliesToHeadingIdGeneration() {
56+
HtmlRenderer renderer = HtmlRenderer.builder()
57+
.extensions(List.of(HeadingAnchorExtension.create()))
58+
.nodeTraversal(NodeTraversal.succeedLossy(2))
59+
.build();
60+
61+
assertThat(doRender(renderer, "# foo *bar*")).isEqualTo("<h1 id=\"foo\">foo <em></em></h1>\n");
62+
}
63+
5364
private String doRender(HtmlRenderer renderer, String text) {
5465
return renderer.render(PARSER.parse(text));
5566
}

commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/ImageAttributesExtension.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void extend(HtmlRenderer.Builder rendererBuilder) {
3838
rendererBuilder.attributeProviderFactory(new AttributeProviderFactory() {
3939
@Override
4040
public AttributeProvider create(AttributeProviderContext context) {
41-
return ImageAttributesAttributeProvider.create();
41+
return ImageAttributesAttributeProvider.create(context);
4242
}
4343
});
4444
}

commonmark-ext-image-attributes/src/main/java/org/commonmark/ext/image/attributes/internal/ImageAttributesAttributeProvider.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,30 @@
66
import org.commonmark.node.Image;
77
import org.commonmark.node.Node;
88
import org.commonmark.renderer.html.AttributeProvider;
9+
import org.commonmark.renderer.html.AttributeProviderContext;
910

1011
import java.util.*;
1112

1213
public class ImageAttributesAttributeProvider implements AttributeProvider {
1314

14-
private ImageAttributesAttributeProvider() {
15+
private final AttributeProviderContext context;
16+
17+
private ImageAttributesAttributeProvider(AttributeProviderContext context) {
18+
this.context = context;
1519
}
1620

17-
public static ImageAttributesAttributeProvider create() {
18-
return new ImageAttributesAttributeProvider();
21+
public static ImageAttributesAttributeProvider create(AttributeProviderContext context) {
22+
return new ImageAttributesAttributeProvider(context);
1923
}
2024

2125
@Override
2226
public void setAttributes(Node node, String tagName, final Map<String, String> attributes) {
2327
if (node instanceof Image) {
24-
node.accept(new AbstractVisitor() {
28+
node.accept(new AbstractVisitor(context.getNodeTraversal(node)) {
2529
@Override
26-
public void visit(CustomNode node) {
27-
if (node instanceof ImageAttributes) {
28-
ImageAttributes imageAttributes = (ImageAttributes) node;
30+
public void visit(CustomNode customNode) {
31+
if (customNode instanceof ImageAttributes) {
32+
ImageAttributes imageAttributes = (ImageAttributes) customNode;
2933
for (Map.Entry<String, String> entry : imageAttributes.getAttributes().entrySet()) {
3034
attributes.put(entry.getKey(), entry.getValue());
3135
}

commonmark-ext-image-attributes/src/test/java/org/commonmark/ext/image/attributes/ImageAttributesTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.commonmark.ext.image.attributes;
22

33
import org.commonmark.Extension;
4+
import org.commonmark.node.Image;
45
import org.commonmark.node.Node;
6+
import org.commonmark.node.NodeTraversal;
57
import org.commonmark.node.Paragraph;
68
import org.commonmark.node.SourceSpan;
79
import org.commonmark.parser.IncludeSourceSpans;
@@ -134,6 +136,23 @@ public void sourceSpans() {
134136
assertThat(text.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 19)));
135137
}
136138

139+
@Test
140+
public void traversalLimitSkipsImageAttributesWithoutUnlinkingNodes() {
141+
Node document = PARSER.parse("![text](/url.png){height=5}");
142+
Paragraph paragraph = (Paragraph) document.getFirstChild();
143+
Image image = (Image) paragraph.getFirstChild();
144+
145+
assertThat(image.getLastChild()).isInstanceOf(ImageAttributes.class);
146+
147+
HtmlRenderer renderer = HtmlRenderer.builder()
148+
.extensions(EXTENSIONS)
149+
.nodeTraversal(NodeTraversal.succeedLossy(2))
150+
.build();
151+
152+
assertThat(renderer.render(document)).isEqualTo("<p><img src=\"/url.png\" alt=\"\" /></p>\n");
153+
assertThat(image.getLastChild()).isInstanceOf(ImageAttributes.class);
154+
}
155+
137156
@Override
138157
protected String render(String source) {
139158
return RENDERER.render(PARSER.parse(source));

commonmark/src/main/java/org/commonmark/node/AbstractVisitor.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
package org.commonmark.node;
22

3+
import java.util.Objects;
4+
35
/**
46
* Abstract visitor that visits all children by default.
57
* <p>
68
* Can be used to only process certain nodes. If you override a method and want visiting to descend into children,
7-
* call {@link #visitChildren}.
9+
* call {@link #visitChildren}. Traversal limits configured through the constructor are only enforced for recursion
10+
* that goes through {@link #visitChildren(Node)}.
811
*/
912
public abstract class AbstractVisitor implements Visitor {
1013

14+
private final NodeTraversal traversal;
15+
private int currentDepth;
16+
17+
protected AbstractVisitor() {
18+
this(NodeTraversal.unlimited());
19+
}
20+
21+
protected AbstractVisitor(NodeTraversal traversal) {
22+
this.traversal = Objects.requireNonNull(traversal, "traversal must not be null");
23+
}
24+
1125
@Override
1226
public void visit(BlockQuote blockQuote) {
1327
visitChildren(blockQuote);
@@ -134,8 +148,33 @@ protected void visitChildren(Node parent) {
134148
// A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
135149
// node after visiting it. So get the next node before visiting.
136150
Node next = node.getNext();
137-
node.accept(this);
151+
int childDepth = currentDepth + 1;
152+
if (canVisit(childDepth)) {
153+
int previousDepth = currentDepth;
154+
currentDepth = childDepth;
155+
try {
156+
node.accept(this);
157+
} finally {
158+
currentDepth = previousDepth;
159+
}
160+
}
138161
node = next;
139162
}
140163
}
164+
165+
private boolean canVisit(int depth) {
166+
Integer maxDepth = traversal.getMaxDepth();
167+
if (maxDepth == null || depth <= maxDepth) {
168+
return true;
169+
}
170+
171+
NodeTraversal.LimitExceededBehavior behavior = traversal.getLimitExceededBehavior();
172+
switch (behavior) {
173+
case FAIL_FAST:
174+
throw new NodeTraversalLimitExceededException(maxDepth, depth);
175+
case SUCCEED_LOSSY:
176+
return false;
177+
}
178+
throw new IllegalStateException("Unknown limit exceeded behavior: " + behavior);
179+
}
141180
}
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
package org.commonmark.renderer.html;
22

3+
import org.commonmark.node.Node;
4+
import org.commonmark.node.NodeTraversal;
5+
36
/**
47
* The context for attribute providers.
5-
* <p>Note: There are currently no methods here, this is for future extensibility.</p>
68
* <p><em>This interface is not intended to be implemented by clients.</em></p>
79
*/
810
public interface AttributeProviderContext {
11+
12+
/**
13+
* @return the configured traversal rebased to the current callback node
14+
*/
15+
default NodeTraversal getNodeTraversal() {
16+
return NodeTraversal.unlimited();
17+
}
18+
19+
/**
20+
* @param node a node within the current render root
21+
* @return the configured traversal rebased to the supplied node
22+
*/
23+
default NodeTraversal getNodeTraversal(Node node) {
24+
return NodeTraversal.unlimited();
25+
}
926
}

0 commit comments

Comments
 (0)