Skip to content

Commit 260bd2e

Browse files
authored
Merge pull request #312 from commonmark/fix-markdown-renderer-fenced-code-blocks-from-ast
Fix markdown renderer for manually created fenced code blocks
2 parents ccdf1a3 + 8b7f928 commit 260bd2e

4 files changed

Lines changed: 148 additions & 39 deletions

File tree

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@
1212
public class FencedCodeBlockParser extends AbstractBlockParser {
1313

1414
private final FencedCodeBlock block = new FencedCodeBlock();
15+
private final char fenceChar;
16+
private final int openingFenceLength;
1517

1618
private String firstLine;
1719
private StringBuilder otherLines = new StringBuilder();
1820

1921
public FencedCodeBlockParser(char fenceChar, int fenceLength, int fenceIndent) {
20-
block.setFenceChar(fenceChar);
21-
block.setFenceLength(fenceLength);
22+
this.fenceChar = fenceChar;
23+
this.openingFenceLength = fenceLength;
24+
block.setFenceCharacter(String.valueOf(fenceChar));
25+
block.setOpeningFenceLength(fenceLength);
2226
block.setFenceIndent(fenceIndent);
2327
}
2428

@@ -32,7 +36,7 @@ public BlockContinue tryContinue(ParserState state) {
3236
int nextNonSpace = state.getNextNonSpaceIndex();
3337
int newIndex = state.getIndex();
3438
CharSequence line = state.getLine().getContent();
35-
if (state.getIndent() < Parsing.CODE_BLOCK_INDENT && nextNonSpace < line.length() && line.charAt(nextNonSpace) == block.getFenceChar() && isClosing(line, nextNonSpace)) {
39+
if (state.getIndent() < Parsing.CODE_BLOCK_INDENT && nextNonSpace < line.length() && tryClosing(line, nextNonSpace)) {
3640
// closing fence - we're at end of line, so we can finalize now
3741
return BlockContinue.finished();
3842
} else {
@@ -76,7 +80,7 @@ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockPar
7680
int nextNonSpace = state.getNextNonSpaceIndex();
7781
FencedCodeBlockParser blockParser = checkOpener(state.getLine().getContent(), nextNonSpace, indent);
7882
if (blockParser != null) {
79-
return BlockStart.of(blockParser).atIndex(nextNonSpace + blockParser.block.getFenceLength());
83+
return BlockStart.of(blockParser).atIndex(nextNonSpace + blockParser.block.getOpeningFenceLength());
8084
} else {
8185
return BlockStart.none();
8286
}
@@ -119,15 +123,17 @@ private static FencedCodeBlockParser checkOpener(CharSequence line, int index, i
119123
// spec: The content of the code block consists of all subsequent lines, until a closing code fence of the same type
120124
// as the code block began with (backticks or tildes), and with at least as many backticks or tildes as the opening
121125
// code fence.
122-
private boolean isClosing(CharSequence line, int index) {
123-
char fenceChar = block.getFenceChar();
124-
int fenceLength = block.getFenceLength();
126+
private boolean tryClosing(CharSequence line, int index) {
125127
int fences = Characters.skip(fenceChar, line, index, line.length()) - index;
126-
if (fences < fenceLength) {
128+
if (fences < openingFenceLength) {
127129
return false;
128130
}
129131
// spec: The closing code fence [...] may be followed only by spaces, which are ignored.
130132
int after = Characters.skipSpaceTab(line, index + fences, line.length());
131-
return after == line.length();
133+
if (after == line.length()) {
134+
block.setClosingFenceLength(fences);
135+
return true;
136+
}
137+
return false;
132138
}
133139
}

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

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
public class FencedCodeBlock extends Block {
44

5-
private char fenceChar;
6-
private int fenceLength;
5+
private String fenceCharacter;
6+
private Integer openingFenceLength;
7+
private Integer closingFenceLength;
78
private int fenceIndent;
89

910
private String info;
@@ -14,20 +15,47 @@ public void accept(Visitor visitor) {
1415
visitor.visit(this);
1516
}
1617

17-
public char getFenceChar() {
18-
return fenceChar;
18+
/**
19+
* @return the fence character that was used, e.g. {@code `} or {@code ~}, if available, or null otherwise
20+
*/
21+
public String getFenceCharacter() {
22+
return fenceCharacter;
1923
}
2024

21-
public void setFenceChar(char fenceChar) {
22-
this.fenceChar = fenceChar;
25+
public void setFenceCharacter(String fenceCharacter) {
26+
this.fenceCharacter = fenceCharacter;
2327
}
2428

25-
public int getFenceLength() {
26-
return fenceLength;
29+
/**
30+
* @return the length of the opening fence (how many of {{@link #getFenceCharacter()}} were used to start the code
31+
* block) if available, or null otherwise
32+
*/
33+
public Integer getOpeningFenceLength() {
34+
return openingFenceLength;
2735
}
2836

29-
public void setFenceLength(int fenceLength) {
30-
this.fenceLength = fenceLength;
37+
public void setOpeningFenceLength(Integer openingFenceLength) {
38+
if (openingFenceLength != null && openingFenceLength < 3) {
39+
throw new IllegalArgumentException("openingFenceLength needs to be >= 3");
40+
}
41+
checkFenceLengths(openingFenceLength, closingFenceLength);
42+
this.openingFenceLength = openingFenceLength;
43+
}
44+
45+
/**
46+
* @return the length of the closing fence (how many of {@link #getFenceCharacter()} were used to end the code
47+
* block) if available, or null otherwise
48+
*/
49+
public Integer getClosingFenceLength() {
50+
return closingFenceLength;
51+
}
52+
53+
public void setClosingFenceLength(Integer closingFenceLength) {
54+
if (closingFenceLength != null && closingFenceLength < 3) {
55+
throw new IllegalArgumentException("closingFenceLength needs to be >= 3");
56+
}
57+
checkFenceLengths(openingFenceLength, closingFenceLength);
58+
this.closingFenceLength = closingFenceLength;
3159
}
3260

3361
public int getFenceIndent() {
@@ -56,4 +84,44 @@ public String getLiteral() {
5684
public void setLiteral(String literal) {
5785
this.literal = literal;
5886
}
87+
88+
/**
89+
* @deprecated use {@link #getFenceCharacter()} instead
90+
*/
91+
@Deprecated
92+
public char getFenceChar() {
93+
return fenceCharacter != null && !fenceCharacter.isEmpty() ? fenceCharacter.charAt(0) : '\0';
94+
}
95+
96+
/**
97+
* @deprecated use {@link #setFenceCharacter} instead
98+
*/
99+
@Deprecated
100+
public void setFenceChar(char fenceChar) {
101+
this.fenceCharacter = fenceChar != '\0' ? String.valueOf(fenceChar) : null;
102+
}
103+
104+
/**
105+
* @deprecated use {@link #getOpeningFenceLength} instead
106+
*/
107+
@Deprecated
108+
public int getFenceLength() {
109+
return openingFenceLength != null ? openingFenceLength : 0;
110+
}
111+
112+
/**
113+
* @deprecated use {@link #setOpeningFenceLength} instead
114+
*/
115+
@Deprecated
116+
public void setFenceLength(int fenceLength) {
117+
this.openingFenceLength = fenceLength != 0 ? fenceLength : null;
118+
}
119+
120+
private static void checkFenceLengths(Integer openingFenceLength, Integer closingFenceLength) {
121+
if (openingFenceLength != null && closingFenceLength != null) {
122+
if (closingFenceLength < openingFenceLength) {
123+
throw new IllegalArgumentException("fence lengths required to be: closingFenceLength >= openingFenceLength");
124+
}
125+
}
126+
}
59127
}

commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -147,20 +147,35 @@ public void visit(IndentedCodeBlock indentedCodeBlock) {
147147
}
148148

149149
@Override
150-
public void visit(FencedCodeBlock fencedCodeBlock) {
151-
String literal = fencedCodeBlock.getLiteral();
152-
String fence = repeat(String.valueOf(fencedCodeBlock.getFenceChar()), fencedCodeBlock.getFenceLength());
153-
int indent = fencedCodeBlock.getFenceIndent();
150+
public void visit(FencedCodeBlock codeBlock) {
151+
String literal = codeBlock.getLiteral();
152+
String fenceChar = codeBlock.getFenceCharacter() != null ? codeBlock.getFenceCharacter() : "`";
153+
int openingFenceLength;
154+
if (codeBlock.getOpeningFenceLength() != null) {
155+
// If we have a known fence length, use it
156+
openingFenceLength = codeBlock.getOpeningFenceLength();
157+
} else {
158+
// Otherwise, calculate the closing fence length pessimistically, e.g. if the code block itself contains a
159+
// line with ```, we need to use a fence of length 4. If ``` occurs with non-whitespace characters on a
160+
// line, we technically don't need a longer fence, but it's not incorrect to do so.
161+
int fenceCharsInLiteral = findMaxRunLength(fenceChar, literal);
162+
openingFenceLength = Math.max(fenceCharsInLiteral + 1, 3);
163+
}
164+
int closingFenceLength = codeBlock.getClosingFenceLength() != null ? codeBlock.getClosingFenceLength() : openingFenceLength;
165+
166+
String openingFence = repeat(fenceChar, openingFenceLength);
167+
String closingFence = repeat(fenceChar, closingFenceLength);
168+
int indent = codeBlock.getFenceIndent();
154169

155170
if (indent > 0) {
156171
String indentPrefix = repeat(" ", indent);
157172
writer.writePrefix(indentPrefix);
158173
writer.pushPrefix(indentPrefix);
159174
}
160175

161-
writer.raw(fence);
162-
if (fencedCodeBlock.getInfo() != null) {
163-
writer.raw(fencedCodeBlock.getInfo());
176+
writer.raw(openingFence);
177+
if (codeBlock.getInfo() != null) {
178+
writer.raw(codeBlock.getInfo());
164179
}
165180
writer.line();
166181
if (!literal.isEmpty()) {
@@ -170,7 +185,7 @@ public void visit(FencedCodeBlock fencedCodeBlock) {
170185
writer.line();
171186
}
172187
}
173-
writer.raw(fence);
188+
writer.raw(closingFence);
174189
if (indent > 0) {
175190
writer.popPrefix();
176191
}
@@ -259,7 +274,7 @@ public void visit(ListItem listItem) {
259274
public void visit(Code code) {
260275
String literal = code.getLiteral();
261276
// If the literal includes backticks, we can surround them by using one more backtick.
262-
int backticks = findMaxRunLength('`', literal);
277+
int backticks = findMaxRunLength("`", literal);
263278
for (int i = 0; i < backticks + 1; i++) {
264279
writer.raw('`');
265280
}
@@ -411,19 +426,22 @@ protected void visitChildren(Node parent) {
411426
}
412427
}
413428

414-
private static int findMaxRunLength(char c, CharSequence s) {
415-
int backticks = 0;
416-
int start = 0;
417-
while (start < s.length()) {
418-
int index = Characters.find(c, s, start);
419-
if (index != -1) {
420-
start = Characters.skip(c, s, index + 1, s.length());
421-
backticks = Math.max(backticks, start - index);
422-
} else {
423-
break;
429+
private static int findMaxRunLength(String needle, String s) {
430+
int maxRunLength = 0;
431+
int pos = 0;
432+
while (pos < s.length()) {
433+
pos = s.indexOf(needle, pos);
434+
if (pos == -1) {
435+
return maxRunLength;
424436
}
437+
int runLength = 0;
438+
do {
439+
pos += needle.length();
440+
runLength++;
441+
} while (s.startsWith(needle, pos));
442+
maxRunLength = Math.max(runLength, maxRunLength);
425443
}
426-
return backticks;
444+
return maxRunLength;
427445
}
428446

429447
private static boolean contains(String s, CharMatcher charMatcher) {

commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,23 @@ public void testFencedCodeBlocks() {
5252
assertRoundTrip("```info\ntest\n```\n");
5353
assertRoundTrip(" ```\n test\n ```\n");
5454
assertRoundTrip("```\n```\n");
55+
56+
// Preserve the length
57+
assertRoundTrip("````\ntest\n````\n");
58+
assertRoundTrip("~~~\ntest\n~~~~~~\n");
59+
}
60+
61+
@Test
62+
public void testFencedCodeBlocksFromAst() {
63+
var doc = new Document();
64+
var codeBlock = new FencedCodeBlock();
65+
codeBlock.setLiteral("hi code");
66+
doc.appendChild(codeBlock);
67+
68+
assertRendering("", "```\nhi code\n```\n", render(doc));
69+
70+
codeBlock.setLiteral("hi`\n```\n``test");
71+
assertRendering("", "````\nhi`\n```\n``test\n````\n", render(doc));
5572
}
5673

5774
@Test

0 commit comments

Comments
 (0)