Skip to content

Commit 73d76d0

Browse files
authored
Merge pull request #456 from tballison/markdown-writer-bulk-spans
MarkdownWriter: append unescaped spans in bulk
2 parents b89e72f + 5d529c6 commit 73d76d0

3 files changed

Lines changed: 106 additions & 10 deletions

File tree

‎commonmark/src/main/java/org/commonmark/renderer/markdown/MarkdownWriter.java‎

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,7 @@ private void write(String s, CharMatcher escape) {
158158
// Normal fast path
159159
buffer.append(s);
160160
} else {
161-
for (int i = 0; i < s.length(); i++) {
162-
append(s.charAt(i), escape);
163-
}
161+
append(s, escape);
164162
}
165163
} catch (IOException e) {
166164
throw new RuntimeException(e);
@@ -219,16 +217,40 @@ private void flushBlockSeparator() {
219217
}
220218
}
221219

220+
private void append(String s, CharMatcher escape) throws IOException {
221+
// Append unescaped spans in bulk; per-char appends are slow for most Appendables
222+
// (e.g. Writer, where each char is a synchronized call).
223+
int end = s.length();
224+
int start = 0;
225+
for (int i = 0; i < end; i++) {
226+
char c = s.charAt(i);
227+
if (needsEscaping(c, escape)) {
228+
if (start < i) {
229+
buffer.append(s, start, i);
230+
}
231+
appendEscaped(c);
232+
start = i + 1;
233+
}
234+
}
235+
if (start < end) {
236+
buffer.append(s, start, end);
237+
}
238+
}
239+
222240
private void append(char c, CharMatcher escape) throws IOException {
223241
if (needsEscaping(c, escape)) {
224-
if (c == '\n') {
225-
// Can't escape this with \, use numeric character reference
226-
buffer.append("&#10;");
227-
} else {
228-
buffer.append('\\');
229-
buffer.append(c);
230-
}
242+
appendEscaped(c);
243+
} else {
244+
buffer.append(c);
245+
}
246+
}
247+
248+
private void appendEscaped(char c) throws IOException {
249+
if (c == '\n') {
250+
// Can't escape this with \, use numeric character reference
251+
buffer.append("&#10;");
231252
} else {
253+
buffer.append('\\');
232254
buffer.append(c);
233255
}
234256
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.commonmark.renderer.markdown;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import org.commonmark.text.CharMatcher;
6+
import org.junit.jupiter.api.Test;
7+
8+
public class MarkdownWriterTest {
9+
10+
private static final CharMatcher STAR = c -> c == '*';
11+
private static final CharMatcher NEWLINE = c -> c == '\n';
12+
private static final CharMatcher PIPE = c -> c == '|';
13+
14+
@Test
15+
public void testTextEscaping() {
16+
assertThat(text("abc", STAR)).isEqualTo("abc");
17+
assertThat(text("*abc", STAR)).isEqualTo("\\*abc");
18+
assertThat(text("abc*", STAR)).isEqualTo("abc\\*");
19+
assertThat(text("a*b*c", STAR)).isEqualTo("a\\*b\\*c");
20+
assertThat(text("a**b", STAR)).isEqualTo("a\\*\\*b");
21+
assertThat(text("***", STAR)).isEqualTo("\\*\\*\\*");
22+
assertThat(text("*", STAR)).isEqualTo("\\*");
23+
}
24+
25+
@Test
26+
public void testTextEscapingNewline() {
27+
assertThat(text("a\nb\nc", NEWLINE)).isEqualTo("a&#10;b&#10;c");
28+
assertThat(text("\n", NEWLINE)).isEqualTo("&#10;");
29+
}
30+
31+
@Test
32+
public void testRawEscaping() {
33+
StringBuilder sb = new StringBuilder();
34+
MarkdownWriter writer = new MarkdownWriter(sb);
35+
writer.pushRawEscape(PIPE);
36+
writer.raw("a|b|c");
37+
writer.popRawEscape();
38+
writer.raw("d|e");
39+
assertThat(sb.toString()).isEqualTo("a\\|b\\|cd|e");
40+
}
41+
42+
@Test
43+
public void testLastCharAndLineStart() {
44+
StringBuilder sb = new StringBuilder();
45+
MarkdownWriter writer = new MarkdownWriter(sb);
46+
writer.text("a*", STAR);
47+
assertThat(writer.getLastChar()).isEqualTo('*');
48+
assertThat(writer.isAtLineStart()).isFalse();
49+
writer.line();
50+
assertThat(writer.isAtLineStart()).isTrue();
51+
}
52+
53+
private static String text(String s, CharMatcher escape) {
54+
StringBuilder sb = new StringBuilder();
55+
MarkdownWriter writer = new MarkdownWriter(sb);
56+
writer.text(s, escape);
57+
return sb.toString();
58+
}
59+
}

‎commonmark/src/test/java/org/commonmark/test/SpecBenchmark.java‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package org.commonmark.test;
22

3+
import java.io.StringWriter;
34
import java.util.List;
45
import org.commonmark.node.Node;
56
import org.commonmark.parser.Parser;
67
import org.commonmark.renderer.html.HtmlRenderer;
8+
import org.commonmark.renderer.markdown.MarkdownRenderer;
79
import org.commonmark.testutil.TestResources;
810
import org.commonmark.testutil.example.ExampleReader;
911
import org.openjdk.jmh.annotations.*;
@@ -23,6 +25,7 @@ public class SpecBenchmark {
2325
ExampleReader.readExampleSources(TestResources.getSpec());
2426
private static final Parser PARSER = Parser.builder().build();
2527
private static final HtmlRenderer RENDERER = HtmlRenderer.builder().build();
28+
private static final MarkdownRenderer MARKDOWN_RENDERER = MarkdownRenderer.builder().build();
2629

2730
private static final Node SPEC_NODE = PARSER.parse(SPEC);
2831

@@ -60,6 +63,18 @@ public long renderWholeSpec() {
6063
return RENDERER.render(SPEC_NODE).length();
6164
}
6265

66+
@Benchmark
67+
public long renderMarkdownWholeSpec() {
68+
return MARKDOWN_RENDERER.render(SPEC_NODE).length();
69+
}
70+
71+
@Benchmark
72+
public long renderMarkdownWholeSpecToWriter() {
73+
StringWriter writer = new StringWriter();
74+
MARKDOWN_RENDERER.render(SPEC_NODE, writer);
75+
return writer.getBuffer().length();
76+
}
77+
6378
private static long parseAndRender(List<String> examples) {
6479
long length = 0;
6580
for (String example : examples) {

0 commit comments

Comments
 (0)