Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9ec3935
Beginnings of a MarkdownRenderer
robinst Jan 9, 2024
b25b7ea
Code spans
robinst Jan 10, 2024
aa7ac8b
Add test that renders the spec examples through Markdown
robinst Jan 10, 2024
bd8fa6a
Count how many examples pass/fail
robinst Jan 10, 2024
25abdad
Emphasis and strong emphasis
robinst Jan 10, 2024
3e99b48
Line breaks
robinst Jan 10, 2024
43006b2
Links and images
robinst Jan 10, 2024
88c767a
HTML blocks and inlines
robinst Jan 10, 2024
e4ced9b
Block quotes
robinst Jan 10, 2024
f640e94
Indented code blocks
robinst Jan 10, 2024
31e3664
Fenced code blocks
robinst Jan 10, 2024
bbde769
Lists (not tight lists yet)
robinst Jan 11, 2024
b82d7ce
Percent encode URLs in spec test (like in other spec tests)
robinst Jan 11, 2024
d0e89f4
Fix code spans with spaces
robinst Jan 11, 2024
a4eecb5
Escape special characters in normal text
robinst Jan 11, 2024
9f7d949
Fix empty fenced code blocks
robinst Jan 11, 2024
29145aa
Tight lists
robinst Jan 11, 2024
8171519
Preserve indent and content indent for lists
robinst Jan 17, 2024
41eeede
Fix empty list items
robinst Jan 17, 2024
12ce6bc
Fix tight list with nested loose list
robinst Jan 17, 2024
6417d01
Print failing cases
robinst Jan 17, 2024
26ac7e1
Don't discard trailing empty lines in code blocks
robinst Jan 22, 2024
724619b
Use Setext heading if necessary
robinst Jan 22, 2024
02670c9
Test tabs properly
robinst Jan 22, 2024
4918d64
Write HTML block in lines to make use of prefix
robinst Jan 22, 2024
db5f055
Escape special characters at beginning of line
robinst Jan 23, 2024
64d163d
Escape # in headings too
robinst Jan 23, 2024
f5b04ef
Disregard prefixes for line start logic
robinst Jan 23, 2024
59d5c03
Escape whitespace too (weird)
robinst Jan 24, 2024
37e507a
Reorder methods in spec order
robinst Jan 24, 2024
edfc3d0
Some more comments
robinst Jan 29, 2024
08a60f1
Reorder methods in writer, add comments
robinst Jan 29, 2024
e6c82d5
Tables extension markdown renderer
robinst Feb 6, 2024
d726f72
Change isTight/setTight to use push/pop pattern like other settings
robinst Feb 6, 2024
aaf18a2
Use tight blocks to render tables
robinst Feb 6, 2024
01a52bc
Rename file
robinst Feb 6, 2024
a490fae
Strikethrough markdown renderer
robinst Feb 6, 2024
981d236
Add getSpecialCharacters for escaping of extension characters
robinst Feb 6, 2024
64eafc4
Use getSpecialCharacters for table extension too
robinst Feb 6, 2024
2966df1
Ins extensions markdown renderer
robinst Feb 7, 2024
0c5ad6e
Use parsed delimiter for emphasis if available
robinst Feb 8, 2024
5a0960f
Add integration test for multiple extensions
robinst Feb 8, 2024
114956a
Fix thematic break and list items
robinst Feb 8, 2024
979d2f6
Documentation
robinst Feb 15, 2024
94eb5ff
Add test for hard line break in heading
robinst Feb 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ Java library for parsing and rendering [Markdown] text according to the
Introduction
------------

Provides classes for parsing input to an abstract syntax tree of nodes
(AST), visiting and manipulating nodes, and rendering to HTML. It
started out as a port of [commonmark.js], but has since evolved into a
full library with a nice API and the following features:
Provides classes for parsing input to an abstract syntax tree (AST),
visiting and manipulating nodes, and rendering to HTML or back to Markdown.
It started out as a port of [commonmark.js], but has since evolved into an
extensible library with the following features:

* Small (core has no dependencies, extensions in separate artifacts)
* Fast (10-20 times faster than pegdown, see benchmarks in repo)
* Fast (10-20 times faster than [pegdown] which used to be a popular Markdown
library, see benchmarks in repo)
* Flexible (manipulate the AST after parsing, customize HTML rendering)
* Extensible (tables, strikethrough, autolinking and more, see below)

Expand Down Expand Up @@ -63,9 +64,9 @@ import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;

Parser parser = Parser.builder().build();
Node document = parser.parse("This is *Sparta*");
Node document = parser.parse("This is *Markdown*");
HtmlRenderer renderer = HtmlRenderer.builder().build();
renderer.render(document); // "<p>This is <em>Sparta</em></p>\n"
renderer.render(document); // "<p>This is <em>Markdown</em></p>\n"
```

This uses the parser and renderer with default options. Both builders have
Expand All @@ -81,8 +82,23 @@ to which tags are allowed, etc. That is the responsibility of the caller, and
if you expose the resulting HTML, you probably want to run a sanitizer on it
after this.

For rendering to plain text, there's also a `TextContentRenderer` with
a very similar API.
#### Render to Markdown

```java
import org.commonmark.node.*;
import org.commonmark.renderer.markdown.MarkdownRenderer;

MarkdownRenderer renderer = MarkdownRenderer.builder().build();
Node document = new Document();
Heading heading = new Heading();
heading.setLevel(2);
heading.appendChild(new Text("My title"));
document.appendChild(heading);

renderer.render(document); // "## My title\n"
```

For rendering to plain text with minimal markup, there's also `TextContentRenderer`.

#### Use a visitor to process parsed nodes

Expand Down Expand Up @@ -390,6 +406,7 @@ BSD (2-clause) licensed, see LICENSE.txt file.
[CommonMark]: https://commonmark.org/
[Markdown]: https://daringfireball.net/projects/markdown/
[commonmark.js]: https://github.com/commonmark/commonmark.js
[pegdown]: https://github.com/sirthias/pegdown
[CommonMark Dingus]: https://spec.commonmark.org/dingus/
[Maven Central]: https://search.maven.org/#search|ga|1|g%3A%22org.commonmark%22
[Semantic Versioning]: https://semver.org/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@
import org.commonmark.node.Delimited;

/**
* A strikethrough node containing text and other inline nodes nodes as children.
* A strikethrough node containing text and other inline nodes as children.
*/
public class Strikethrough extends CustomNode implements Delimited {

private static final String DELIMITER = "~~";
private String delimiter;

public Strikethrough(String delimiter) {
this.delimiter = delimiter;
}

@Override
public String getOpeningDelimiter() {
return DELIMITER;
return delimiter;
}

@Override
public String getClosingDelimiter() {
return DELIMITER;
return delimiter;
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package org.commonmark.ext.gfm.strikethrough;

import org.commonmark.Extension;
import org.commonmark.renderer.text.TextContentRenderer;
import org.commonmark.renderer.text.TextContentNodeRendererContext;
import org.commonmark.renderer.text.TextContentNodeRendererFactory;
import org.commonmark.ext.gfm.strikethrough.internal.StrikethroughDelimiterProcessor;
import org.commonmark.ext.gfm.strikethrough.internal.StrikethroughHtmlNodeRenderer;
import org.commonmark.ext.gfm.strikethrough.internal.StrikethroughMarkdownNodeRenderer;
import org.commonmark.ext.gfm.strikethrough.internal.StrikethroughTextContentNodeRenderer;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlNodeRendererFactory;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlNodeRendererFactory;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory;
import org.commonmark.renderer.markdown.MarkdownRenderer;
import org.commonmark.renderer.text.TextContentNodeRendererContext;
import org.commonmark.renderer.text.TextContentNodeRendererFactory;
import org.commonmark.renderer.text.TextContentRenderer;

import java.util.Collections;
import java.util.Set;

/**
* Extension for GFM strikethrough using {@code ~} or {@code ~~} (GitHub Flavored Markdown).
Expand Down Expand Up @@ -42,7 +49,7 @@
* </p>
*/
public class StrikethroughExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
TextContentRenderer.TextContentRendererExtension {
TextContentRenderer.TextContentRendererExtension, MarkdownRenderer.MarkdownRendererExtension {

private final boolean requireTwoTildes;

Expand Down Expand Up @@ -89,13 +96,28 @@ public NodeRenderer create(TextContentNodeRendererContext context) {
});
}

@Override
public void extend(MarkdownRenderer.Builder rendererBuilder) {
rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
@Override
public NodeRenderer create(MarkdownNodeRendererContext context) {
return new StrikethroughMarkdownNodeRenderer(context);
}

@Override
public Set<Character> getSpecialCharacters() {
return Collections.singleton('~');
}
});
}

public static class Builder {

private boolean requireTwoTildes = false;

/**
* @param requireTwoTildes Whether two tilde characters ({@code ~~}) are required for strikethrough or whether
* one is also enough. Default is {@code false}; both a single tilde and two tildes can be used for strikethrough.
* one is also enough. Default is {@code false}; both a single tilde and two tildes can be used for strikethrough.
* @return {@code this}
*/
public Builder requireTwoTildes(boolean requireTwoTildes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public int process(DelimiterRun openingRun, DelimiterRun closingRun) {
Text opener = openingRun.getOpener();

// Wrap nodes between delimiters in strikethrough.
Node strikethrough = new Strikethrough();
String delimiter = openingRun.length() == 1 ? opener.getLiteral() : opener.getLiteral() + opener.getLiteral();
Node strikethrough = new Strikethrough(delimiter);

SourceSpans sourceSpans = new SourceSpans();
sourceSpans.addAllFrom(openingRun.getOpeners(openingRun.length()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.commonmark.ext.gfm.strikethrough.internal;

import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.node.Node;
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
import org.commonmark.renderer.markdown.MarkdownWriter;

public class StrikethroughMarkdownNodeRenderer extends StrikethroughNodeRenderer {

private final MarkdownNodeRendererContext context;
private final MarkdownWriter writer;

public StrikethroughMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
this.context = context;
this.writer = context.getWriter();
}

@Override
public void render(Node node) {
Strikethrough strikethrough = (Strikethrough) node;
writer.raw(strikethrough.getOpeningDelimiter());
renderChildren(node);
writer.raw(strikethrough.getClosingDelimiter());
}

private void renderChildren(Node parent) {
Node node = parent.getFirstChild();
while (node != null) {
Node next = node.getNext();
context.render(node);
node = next;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.commonmark.ext.gfm.strikethrough;

import org.commonmark.Extension;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.markdown.MarkdownRenderer;
import org.junit.Test;

import java.util.Collections;
import java.util.Set;

import static org.junit.Assert.assertEquals;

public class StrikethroughMarkdownRendererTest {

private static final Set<Extension> EXTENSIONS = Collections.singleton(StrikethroughExtension.create());
private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build();

@Test
public void testStrikethrough() {
assertRoundTrip("~foo~ ~bar~\n");
assertRoundTrip("~~foo~~ ~~bar~~\n");
assertRoundTrip("~~f\\~oo~~ ~~bar~~\n");

assertRoundTrip("\\~foo\\~\n");
}

protected String render(String source) {
return RENDERER.render(PARSER.parse(source));
}

private void assertRoundTrip(String input) {
String rendered = render(input);
assertEquals(input, rendered);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public void setHeader(boolean header) {
}

/**
* @return the cell alignment
* @return the cell alignment or {@code null} if no specific alignment
*/
public Alignment getAlignment() {
return alignment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@
import org.commonmark.Extension;
import org.commonmark.ext.gfm.tables.internal.TableBlockParser;
import org.commonmark.ext.gfm.tables.internal.TableHtmlNodeRenderer;
import org.commonmark.ext.gfm.tables.internal.TableMarkdownNodeRenderer;
import org.commonmark.ext.gfm.tables.internal.TableTextContentNodeRenderer;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlNodeRendererFactory;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory;
import org.commonmark.renderer.markdown.MarkdownRenderer;
import org.commonmark.renderer.text.TextContentNodeRendererContext;
import org.commonmark.renderer.text.TextContentNodeRendererFactory;
import org.commonmark.renderer.text.TextContentRenderer;

import java.util.Collections;
import java.util.Set;

/**
* Extension for GFM tables using "|" pipes (GitHub Flavored Markdown).
* <p>
Expand All @@ -27,7 +34,7 @@
* @see <a href="https://github.github.com/gfm/#tables-extension-">Tables (extension) in GitHub Flavored Markdown Spec</a>
*/
public class TablesExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
TextContentRenderer.TextContentRendererExtension {
TextContentRenderer.TextContentRendererExtension, MarkdownRenderer.MarkdownRendererExtension {

private TablesExtension() {
}
Expand Down Expand Up @@ -60,4 +67,19 @@ public NodeRenderer create(TextContentNodeRendererContext context) {
}
});
}

@Override
public void extend(MarkdownRenderer.Builder rendererBuilder) {
rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
@Override
public NodeRenderer create(MarkdownNodeRendererContext context) {
return new TableMarkdownNodeRenderer(context);
}

@Override
public Set<Character> getSpecialCharacters() {
return Collections.singleton('|');
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.commonmark.ext.gfm.tables.internal;

import org.commonmark.ext.gfm.tables.*;
import org.commonmark.internal.util.AsciiMatcher;
import org.commonmark.internal.util.CharMatcher;
import org.commonmark.node.Node;
import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
import org.commonmark.renderer.markdown.MarkdownWriter;

import java.util.ArrayList;
import java.util.List;

/**
* The Table node renderer that is needed for rendering GFM tables (GitHub Flavored Markdown) to text content.
*/
public class TableMarkdownNodeRenderer extends TableNodeRenderer implements NodeRenderer {
private final MarkdownWriter writer;
private final MarkdownNodeRendererContext context;

private final CharMatcher pipe = AsciiMatcher.builder().c('|').build();

private final List<TableCell.Alignment> columns = new ArrayList<>();

public TableMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
this.writer = context.getWriter();
this.context = context;
}

@Override
protected void renderBlock(TableBlock node) {
columns.clear();
writer.pushTight(true);
renderChildren(node);
writer.popTight();
writer.block();
}

@Override
protected void renderHead(TableHead node) {
renderChildren(node);
for (TableCell.Alignment columnAlignment : columns) {
writer.raw('|');
if (columnAlignment == TableCell.Alignment.LEFT) {
writer.raw(":---");
} else if (columnAlignment == TableCell.Alignment.RIGHT) {
writer.raw("---:");
} else if (columnAlignment == TableCell.Alignment.CENTER) {
writer.raw(":---:");
} else {
writer.raw("---");
}
}
writer.raw("|");
writer.block();
}

@Override
protected void renderBody(TableBody node) {
renderChildren(node);
}

@Override
protected void renderRow(TableRow node) {
renderChildren(node);
// Trailing | at the end of the line
writer.raw("|");
writer.block();
}

@Override
protected void renderCell(TableCell node) {
if (node.getParent() != null && node.getParent().getParent() instanceof TableHead) {
columns.add(node.getAlignment());
}
writer.raw("|");
writer.pushRawEscape(pipe);
renderChildren(node);
writer.popRawEscape();
}

private void renderChildren(Node parent) {
Node node = parent.getFirstChild();
while (node != null) {
Node next = node.getNext();
context.render(node);
node = next;
}
}
}
Loading