> nodeRendererFactories = new ArrayList<>();
+
+ /**
+ * @return the configured {@link TextContentRenderer}
+ */
+ public TextContentRenderer build() {
+ return new TextContentRenderer(this);
+ }
+
+ /**
+ * Set the value of flag for stripping new lines.
+ *
+ * @param stripNewlines true for stripping new lines and render text as "single line",
+ * false for keeping all line breaks
+ * @return {@code this}
+ */
+ public Builder stripNewlines(boolean stripNewlines) {
+ this.stripNewlines = stripNewlines;
+ return this;
+ }
+
+ /**
+ * Add a factory for instantiating a node renderer (done when rendering). This allows to override the rendering
+ * of node types or define rendering for custom node types.
+ *
+ * If multiple node renderers for the same node type are created, the one from the factory that was added first
+ * "wins". (This is how the rendering for core node types can be overridden; the default rendering comes last.)
+ *
+ * @param nodeRendererFactory the factory for creating a node renderer
+ * @return {@code this}
+ */
+ public Builder nodeRendererFactory(TextContentNodeRendererFactory nodeRendererFactory) {
+ this.nodeRendererFactories.add(nodeRendererFactory);
+ return this;
+ }
+
+ /**
+ * @param extensions extensions to use on this text content renderer
+ * @return {@code this}
+ */
+ public Builder extensions(Iterable extends Extension> extensions) {
+ for (Extension extension : extensions) {
+ if (extension instanceof TextContentRenderer.TextContentRendererExtension) {
+ TextContentRenderer.TextContentRendererExtension htmlRendererExtension =
+ (TextContentRenderer.TextContentRendererExtension) extension;
+ htmlRendererExtension.extend(this);
+ }
+ }
+ return this;
+ }
+ }
+
+ /**
+ * Extension for {@link TextContentRenderer}.
+ */
+ public interface TextContentRendererExtension extends Extension {
+ void extend(TextContentRenderer.Builder rendererBuilder);
+ }
+
+ private class RendererContext extends TextContentNodeRendererContext {
+ private final TextContentWriter textContentWriter;
+
+ private RendererContext(TextContentWriter textContentWriter) {
+ this.textContentWriter = textContentWriter;
+
+ List renderers = new ArrayList<>(nodeRendererFactories.size());
+ for (NodeRendererFactory nodeRendererFactory : nodeRendererFactories) {
+ renderers.add(nodeRendererFactory.create(this));
+ }
+ addNodeRenderers(renderers);
+ }
+
+ @Override
+ public boolean stripNewlines() {
+ return stripNewlines;
+ }
+
+ @Override
+ public TextContentWriter getWriter() {
+ return textContentWriter;
+ }
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/content/TextContentWriter.java b/commonmark/src/main/java/org/commonmark/content/TextContentWriter.java
new file mode 100644
index 000000000..852d759db
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/content/TextContentWriter.java
@@ -0,0 +1,69 @@
+package org.commonmark.content;
+
+import org.commonmark.renderer.Writer;
+
+import java.io.IOException;
+
+public class TextContentWriter implements Writer {
+
+ private final Appendable buffer;
+
+ private char lastChar;
+
+ public TextContentWriter(Appendable out) {
+ buffer = out;
+ }
+
+ public void whitespace() {
+ if (lastChar != 0 && lastChar != ' ') {
+ append(' ');
+ }
+ }
+
+ public void colon() {
+ if (lastChar != 0 && lastChar != ':') {
+ append(':');
+ }
+ }
+
+ public void line() {
+ if (lastChar != 0 && lastChar != '\n') {
+ append('\n');
+ }
+ }
+
+ public void writeStripped(String s) {
+ append(s.replaceAll("[\\r\\n\\s]+", " ").trim());
+ }
+
+ public void write(String s) {
+ append(s);
+ }
+
+ public void write(char c) {
+ append(c);
+ }
+
+ private void append(String s) {
+ try {
+ buffer.append(s);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ int length = s.length();
+ if (length != 0) {
+ lastChar = s.charAt(length - 1);
+ }
+ }
+
+ private void append(char c) {
+ try {
+ buffer.append(c);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ lastChar = c;
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/content/package-info.java b/commonmark/src/main/java/org/commonmark/content/package-info.java
new file mode 100644
index 000000000..414b8f5b7
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/content/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Text content rendering (see {@link org.commonmark.content.TextContentRenderer})
+ */
+package org.commonmark.content;
diff --git a/commonmark/src/main/java/org/commonmark/content/renderer/TextContentNodeRenderer.java b/commonmark/src/main/java/org/commonmark/content/renderer/TextContentNodeRenderer.java
new file mode 100644
index 000000000..3323cfbfe
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/content/renderer/TextContentNodeRenderer.java
@@ -0,0 +1,258 @@
+package org.commonmark.content.renderer;
+
+import org.commonmark.content.TextContentWriter;
+import org.commonmark.node.*;
+import org.commonmark.renderer.NodeRenderer;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * The node renderer that renders all the core nodes (comes last in the order of node renderers).
+ */
+public class TextContentNodeRenderer extends AbstractVisitor implements NodeRenderer {
+
+ protected final TextContentNodeRendererContext context;
+ private final TextContentWriter textContent;
+
+ private Integer orderedListCounter;
+ private Character orderedListDelimiter;
+
+ private Character bulletListMarker;
+
+ public TextContentNodeRenderer(TextContentNodeRendererContext context) {
+ this.context = context;
+ this.textContent = context.getWriter();
+ }
+
+ @Override
+ public Set> getNodeTypes() {
+ return new HashSet<>(Arrays.asList(
+ Document.class,
+ Heading.class,
+ Paragraph.class,
+ BlockQuote.class,
+ BulletList.class,
+ FencedCodeBlock.class,
+ HtmlBlock.class,
+ ThematicBreak.class,
+ IndentedCodeBlock.class,
+ Link.class,
+ ListItem.class,
+ OrderedList.class,
+ Image.class,
+ Emphasis.class,
+ StrongEmphasis.class,
+ Text.class,
+ Code.class,
+ HtmlInline.class,
+ SoftLineBreak.class,
+ HardLineBreak.class
+ ));
+ }
+
+ @Override
+ public void render(Node node) {
+ node.accept(this);
+ }
+
+ @Override
+ public void visit(Document document) {
+ // No rendering itself
+ visitChildren(document);
+ }
+
+ @Override
+ public void visit(BlockQuote blockQuote) {
+ textContent.write('«');
+ visitChildren(blockQuote);
+ textContent.write('»');
+
+ writeEndOfLine(blockQuote, null);
+ }
+
+ @Override
+ public void visit(BulletList bulletList) {
+ bulletListMarker = bulletList.getBulletMarker();
+ visitChildren(bulletList);
+ writeEndOfLine(bulletList, null);
+ bulletListMarker = null;
+ }
+
+ @Override
+ public void visit(Code code) {
+ textContent.write('\"');
+ textContent.write(code.getLiteral());
+ textContent.write('\"');
+ }
+
+ @Override
+ public void visit(FencedCodeBlock fencedCodeBlock) {
+ if (context.stripNewlines()) {
+ textContent.writeStripped(fencedCodeBlock.getLiteral());
+ writeEndOfLine(fencedCodeBlock, null);
+ } else {
+ textContent.write(fencedCodeBlock.getLiteral());
+ }
+ }
+
+ @Override
+ public void visit(HardLineBreak hardLineBreak) {
+ writeEndOfLine(hardLineBreak, null);
+ }
+
+ @Override
+ public void visit(Heading heading) {
+ visitChildren(heading);
+ writeEndOfLine(heading, ':');
+ }
+
+ @Override
+ public void visit(ThematicBreak thematicBreak) {
+ if (!context.stripNewlines()) {
+ textContent.write("***");
+ }
+ writeEndOfLine(thematicBreak, null);
+ }
+
+ @Override
+ public void visit(HtmlInline htmlInline) {
+ writeText(htmlInline.getLiteral());
+ }
+
+ @Override
+ public void visit(HtmlBlock htmlBlock) {
+ writeText(htmlBlock.getLiteral());
+ }
+
+ @Override
+ public void visit(Image image) {
+ writeLink(image, image.getTitle(), image.getDestination());
+ }
+
+ @Override
+ public void visit(IndentedCodeBlock indentedCodeBlock) {
+ if (context.stripNewlines()) {
+ textContent.writeStripped(indentedCodeBlock.getLiteral());
+ writeEndOfLine(indentedCodeBlock, null);
+ } else {
+ textContent.write(indentedCodeBlock.getLiteral());
+ }
+ }
+
+ @Override
+ public void visit(Link link) {
+ writeLink(link, link.getTitle(), link.getDestination());
+ }
+
+ @Override
+ public void visit(ListItem listItem) {
+ if (orderedListCounter != null) {
+ textContent.write(String.valueOf(orderedListCounter) + orderedListDelimiter + " ");
+ visitChildren(listItem);
+ writeEndOfLine(listItem, null);
+ orderedListCounter++;
+ } else if (bulletListMarker != null) {
+ if (!context.stripNewlines()) {
+ textContent.write(bulletListMarker + " ");
+ }
+ visitChildren(listItem);
+ writeEndOfLine(listItem, null);
+ }
+ }
+
+ @Override
+ public void visit(OrderedList orderedList) {
+ orderedListCounter = orderedList.getStartNumber();
+ orderedListDelimiter = orderedList.getDelimiter();
+ visitChildren(orderedList);
+ writeEndOfLine(orderedList, null);
+ orderedListCounter = null;
+ orderedListDelimiter = null;
+ }
+
+ @Override
+ public void visit(Paragraph paragraph) {
+ visitChildren(paragraph);
+ // Add "end of line" only if its "root paragraph.
+ if (paragraph.getParent() == null || paragraph.getParent() instanceof Document) {
+ writeEndOfLine(paragraph, null);
+ }
+ }
+
+ @Override
+ public void visit(SoftLineBreak softLineBreak) {
+ writeEndOfLine(softLineBreak, null);
+ }
+
+ @Override
+ public void visit(Text text) {
+ writeText(text.getLiteral());
+ }
+
+ @Override
+ protected void visitChildren(Node parent) {
+ Node node = parent.getFirstChild();
+ while (node != null) {
+ Node next = node.getNext();
+ context.render(node);
+ node = next;
+ }
+ }
+
+ private void writeText(String text) {
+ if (context.stripNewlines()) {
+ textContent.writeStripped(text);
+ } else {
+ textContent.write(text);
+ }
+ }
+
+ private void writeLink(Node node, String title, String destination) {
+ boolean hasChild = node.getFirstChild() != null;
+ boolean hasTitle = title != null;
+ boolean hasDestination = destination != null && !destination.equals("");
+
+ if (hasChild) {
+ textContent.write('"');
+ visitChildren(node);
+ textContent.write('"');
+ if (hasTitle || hasDestination) {
+ textContent.whitespace();
+ textContent.write('(');
+ }
+ }
+
+ if (hasTitle) {
+ textContent.write(title);
+ if (hasDestination) {
+ textContent.colon();
+ textContent.whitespace();
+ }
+ }
+
+ if (hasDestination) {
+ textContent.write(destination);
+ }
+
+ if (hasChild && (hasTitle || hasDestination)) {
+ textContent.write(')');
+ }
+ }
+
+ private void writeEndOfLine(Node node, Character c) {
+ if (context.stripNewlines()) {
+ if (c != null) {
+ textContent.write(c);
+ }
+ if (node.getNext() != null) {
+ textContent.whitespace();
+ }
+ } else {
+ if (node.getNext() != null) {
+ textContent.line();
+ }
+ }
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/content/renderer/TextContentNodeRendererContext.java b/commonmark/src/main/java/org/commonmark/content/renderer/TextContentNodeRendererContext.java
new file mode 100644
index 000000000..fc2dffb70
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/content/renderer/TextContentNodeRendererContext.java
@@ -0,0 +1,13 @@
+package org.commonmark.content.renderer;
+
+import org.commonmark.content.TextContentWriter;
+import org.commonmark.renderer.BaseNodeRendererContext;
+
+public abstract class TextContentNodeRendererContext extends BaseNodeRendererContext {
+
+ /**
+ * @return true for stripping new lines and render text as "single line",
+ * false for keeping all line breaks.
+ */
+ public abstract boolean stripNewlines();
+}
diff --git a/commonmark/src/main/java/org/commonmark/content/renderer/TextContentNodeRendererFactory.java b/commonmark/src/main/java/org/commonmark/content/renderer/TextContentNodeRendererFactory.java
new file mode 100644
index 000000000..5a633b565
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/content/renderer/TextContentNodeRendererFactory.java
@@ -0,0 +1,6 @@
+package org.commonmark.content.renderer;
+
+import org.commonmark.renderer.NodeRendererFactory;
+
+public interface TextContentNodeRendererFactory extends NodeRendererFactory {
+}
diff --git a/commonmark/src/main/java/org/commonmark/html/HtmlRenderer.java b/commonmark/src/main/java/org/commonmark/html/HtmlRenderer.java
index d36742680..d65933f0a 100644
--- a/commonmark/src/main/java/org/commonmark/html/HtmlRenderer.java
+++ b/commonmark/src/main/java/org/commonmark/html/HtmlRenderer.java
@@ -1,16 +1,22 @@
package org.commonmark.html;
import org.commonmark.Extension;
-import org.commonmark.html.renderer.CoreNodeRenderer;
-import org.commonmark.html.renderer.NodeRenderer;
-import org.commonmark.html.renderer.NodeRendererContext;
-import org.commonmark.html.renderer.NodeRendererFactory;
+import org.commonmark.html.renderer.HtmlNodeRenderer;
+import org.commonmark.html.renderer.HtmlNodeRendererContext;
+import org.commonmark.html.renderer.HtmlNodeRendererFactory;
import org.commonmark.internal.util.Escaping;
import org.commonmark.node.HtmlBlock;
import org.commonmark.node.HtmlInline;
import org.commonmark.node.Node;
+import org.commonmark.renderer.BaseRenderer;
+import org.commonmark.renderer.NodeRenderer;
+import org.commonmark.renderer.NodeRendererContext;
+import org.commonmark.renderer.NodeRendererFactory;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
/**
* Renders a tree of nodes to HTML.
@@ -21,13 +27,13 @@
* renderer.render(node);
*
*/
-public class HtmlRenderer {
+public class HtmlRenderer extends BaseRenderer {
private final String softbreak;
private final boolean escapeHtml;
private final boolean percentEncodeUrls;
private final List attributeProviders;
- private final List nodeRendererFactories;
+ private final List> nodeRendererFactories;
private HtmlRenderer(Builder builder) {
this.softbreak = builder.softbreak;
@@ -38,10 +44,10 @@ private HtmlRenderer(Builder builder) {
this.nodeRendererFactories = new ArrayList<>(builder.nodeRendererFactories.size() + 1);
this.nodeRendererFactories.addAll(builder.nodeRendererFactories);
// Add as last. This means clients can override the rendering of core nodes if they want.
- this.nodeRendererFactories.add(new NodeRendererFactory() {
+ this.nodeRendererFactories.add(new HtmlNodeRendererFactory() {
@Override
- public NodeRenderer create(NodeRendererContext context) {
- return new CoreNodeRenderer(context);
+ public NodeRenderer create(HtmlNodeRendererContext context) {
+ return new HtmlNodeRenderer(context);
}
});
}
@@ -55,21 +61,9 @@ public static Builder builder() {
return new Builder();
}
- public void render(Node node, Appendable output) {
- MainNodeRenderer renderer = new MainNodeRenderer(new HtmlWriter(output));
- renderer.render(node);
- }
-
- /**
- * Render the tree of nodes to HTML.
- *
- * @param node the root node
- * @return the rendered HTML
- */
- public String render(Node node) {
- StringBuilder sb = new StringBuilder();
- render(node, sb);
- return sb.toString();
+ @Override
+ public NodeRendererContext createContext(Appendable out) {
+ return new RendererContext(new HtmlWriter(out));
}
/**
@@ -81,7 +75,7 @@ public static class Builder {
private boolean escapeHtml = false;
private boolean percentEncodeUrls = false;
private List attributeProviders = new ArrayList<>();
- private List nodeRendererFactories = new ArrayList<>();
+ private List> nodeRendererFactories = new ArrayList<>();
/**
* @return the configured {@link HtmlRenderer}
@@ -160,7 +154,7 @@ public Builder attributeProvider(AttributeProvider attributeProvider) {
* @param nodeRendererFactory the factory for creating a node renderer
* @return {@code this}
*/
- public Builder nodeRendererFactory(NodeRendererFactory nodeRendererFactory) {
+ public Builder nodeRendererFactory(HtmlNodeRendererFactory nodeRendererFactory) {
this.nodeRendererFactories.add(nodeRendererFactory);
return this;
}
@@ -187,24 +181,18 @@ public interface HtmlRendererExtension extends Extension {
void extend(Builder rendererBuilder);
}
- private class MainNodeRenderer implements NodeRendererContext {
+ private class RendererContext extends HtmlNodeRendererContext {
private final HtmlWriter htmlWriter;
- private final Map, NodeRenderer> renderers;
- private MainNodeRenderer(HtmlWriter htmlWriter) {
+ private RendererContext(HtmlWriter htmlWriter) {
this.htmlWriter = htmlWriter;
- this.renderers = new HashMap<>(32);
-
- // The first node renderer for a node type "wins".
- for (int i = nodeRendererFactories.size() - 1; i >= 0; i--) {
- NodeRendererFactory nodeRendererFactory = nodeRendererFactories.get(i);
- NodeRenderer nodeRenderer = nodeRendererFactory.create(this);
- for (Class extends Node> nodeType : nodeRenderer.getNodeTypes()) {
- // Overwrite existing renderer
- renderers.put(nodeType, nodeRenderer);
- }
+
+ List renderers = new ArrayList<>(nodeRendererFactories.size());
+ for (NodeRendererFactory nodeRendererFactory : nodeRendererFactories) {
+ renderers.add(nodeRendererFactory.create(this));
}
+ addNodeRenderers(renderers);
}
@Override
@@ -229,7 +217,7 @@ public Map extendAttributes(Node node, Map attri
}
@Override
- public HtmlWriter getHtmlWriter() {
+ public HtmlWriter getWriter() {
return htmlWriter;
}
@@ -238,14 +226,6 @@ public String getSoftbreak() {
return softbreak;
}
- @Override
- public void render(Node node) {
- NodeRenderer nodeRenderer = renderers.get(node.getClass());
- if (nodeRenderer != null) {
- nodeRenderer.render(node);
- }
- }
-
private void setCustomAttributes(Node node, Map attrs) {
for (AttributeProvider attributeProvider : attributeProviders) {
attributeProvider.setAttributes(node, attrs);
diff --git a/commonmark/src/main/java/org/commonmark/html/HtmlWriter.java b/commonmark/src/main/java/org/commonmark/html/HtmlWriter.java
index e69881601..0677f740a 100644
--- a/commonmark/src/main/java/org/commonmark/html/HtmlWriter.java
+++ b/commonmark/src/main/java/org/commonmark/html/HtmlWriter.java
@@ -1,12 +1,13 @@
package org.commonmark.html;
import org.commonmark.internal.util.Escaping;
+import org.commonmark.renderer.Writer;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
-public class HtmlWriter {
+public class HtmlWriter implements Writer {
private static final Map NO_ATTRIBUTES = Collections.emptyMap();
diff --git a/commonmark/src/main/java/org/commonmark/html/renderer/CoreNodeRenderer.java b/commonmark/src/main/java/org/commonmark/html/renderer/HtmlNodeRenderer.java
similarity index 96%
rename from commonmark/src/main/java/org/commonmark/html/renderer/CoreNodeRenderer.java
rename to commonmark/src/main/java/org/commonmark/html/renderer/HtmlNodeRenderer.java
index 51ad18e89..cdb91befe 100644
--- a/commonmark/src/main/java/org/commonmark/html/renderer/CoreNodeRenderer.java
+++ b/commonmark/src/main/java/org/commonmark/html/renderer/HtmlNodeRenderer.java
@@ -2,20 +2,21 @@
import org.commonmark.html.HtmlWriter;
import org.commonmark.node.*;
+import org.commonmark.renderer.NodeRenderer;
import java.util.*;
/**
* The node renderer that renders all the core nodes (comes last in the order of node renderers).
*/
-public class CoreNodeRenderer extends AbstractVisitor implements NodeRenderer {
+public class HtmlNodeRenderer extends AbstractVisitor implements NodeRenderer {
- protected final NodeRendererContext context;
+ protected final HtmlNodeRendererContext context;
private final HtmlWriter html;
- public CoreNodeRenderer(NodeRendererContext context) {
+ public HtmlNodeRenderer(HtmlNodeRendererContext context) {
this.context = context;
- this.html = context.getHtmlWriter();
+ this.html = context.getWriter();
}
@Override
diff --git a/commonmark/src/main/java/org/commonmark/html/renderer/HtmlNodeRendererContext.java b/commonmark/src/main/java/org/commonmark/html/renderer/HtmlNodeRendererContext.java
new file mode 100644
index 000000000..9b633c36d
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/html/renderer/HtmlNodeRendererContext.java
@@ -0,0 +1,35 @@
+package org.commonmark.html.renderer;
+
+import org.commonmark.html.HtmlWriter;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.BaseNodeRendererContext;
+
+import java.util.Map;
+
+public abstract class HtmlNodeRendererContext extends BaseNodeRendererContext {
+
+ /**
+ * @param url to be encoded
+ * @return an encoded URL (depending on the configuration)
+ */
+ public abstract String encodeUrl(String url);
+
+ /**
+ * Extend the attributes by extensions.
+ *
+ * @param node the node for which the attributes are applied
+ * @param attributes the attributes that were calculated by the renderer
+ * @return the extended attributes with added/updated/removed entries
+ */
+ public abstract Map extendAttributes(Node node, Map attributes);
+
+ /**
+ * @return HTML that should be rendered for a soft line break
+ */
+ public abstract String getSoftbreak();
+
+ /**
+ * @return whether HTML blocks and tags should be escaped or not
+ */
+ public abstract boolean shouldEscapeHtml();
+}
diff --git a/commonmark/src/main/java/org/commonmark/html/renderer/HtmlNodeRendererFactory.java b/commonmark/src/main/java/org/commonmark/html/renderer/HtmlNodeRendererFactory.java
new file mode 100644
index 000000000..8467b3722
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/html/renderer/HtmlNodeRendererFactory.java
@@ -0,0 +1,6 @@
+package org.commonmark.html.renderer;
+
+import org.commonmark.renderer.NodeRendererFactory;
+
+public interface HtmlNodeRendererFactory extends NodeRendererFactory {
+}
diff --git a/commonmark/src/main/java/org/commonmark/html/renderer/NodeRendererContext.java b/commonmark/src/main/java/org/commonmark/html/renderer/NodeRendererContext.java
deleted file mode 100644
index 9b24c1240..000000000
--- a/commonmark/src/main/java/org/commonmark/html/renderer/NodeRendererContext.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package org.commonmark.html.renderer;
-
-import org.commonmark.html.HtmlWriter;
-import org.commonmark.node.Node;
-
-import java.util.Map;
-
-/**
- * The context for node rendering, including configuration and functionality for the node renderer to use.
- */
-public interface NodeRendererContext {
-
- /**
- * @param url to be encoded
- * @return an encoded URL (depending on the configuration)
- */
- String encodeUrl(String url);
-
- /**
- * Extend the attributes by extensions.
- *
- * @param node the node for which the attributes are applied
- * @param attributes the attributes that were calculated by the renderer
- * @return the extended attributes with added/updated/removed entries
- */
- Map extendAttributes(Node node, Map attributes);
-
- /**
- * @return the HTML writer to use
- */
- HtmlWriter getHtmlWriter();
-
- /**
- * @return HTML that should be rendered for a soft line break
- */
- String getSoftbreak();
-
- /**
- * Render the specified node and its children using the configured renderers. This should be used to render child
- * nodes; be careful not to pass the node that is being rendered, that would result in an endless loop.
- *
- * @param node the node to render
- */
- void render(Node node);
-
- /**
- * @return whether HTML blocks and tags should be escaped or not
- */
- boolean shouldEscapeHtml();
-}
diff --git a/commonmark/src/main/java/org/commonmark/renderer/BaseNodeRendererContext.java b/commonmark/src/main/java/org/commonmark/renderer/BaseNodeRendererContext.java
new file mode 100644
index 000000000..69f98be00
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/renderer/BaseNodeRendererContext.java
@@ -0,0 +1,31 @@
+package org.commonmark.renderer;
+
+import org.commonmark.node.Node;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public abstract class BaseNodeRendererContext implements NodeRendererContext {
+
+ private final Map, NodeRenderer> renderers = new HashMap<>(32);
+
+ @Override
+ public void render(Node node) {
+ NodeRenderer nodeRenderer = renderers.get(node.getClass());
+ if (nodeRenderer != null) {
+ nodeRenderer.render(node);
+ }
+ }
+
+ protected void addNodeRenderers(List renderers) {
+ // The first node renderer for a node type "wins".
+ for (int i = renderers.size() - 1; i >= 0; i--) {
+ NodeRenderer nodeRenderer = renderers.get(i);
+ for (Class extends Node> nodeType : nodeRenderer.getNodeTypes()) {
+ // Overwrite existing renderer
+ this.renderers.put(nodeType, nodeRenderer);
+ }
+ }
+ }
+}
diff --git a/commonmark/src/main/java/org/commonmark/renderer/BaseRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/BaseRenderer.java
new file mode 100644
index 000000000..d78a741b3
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/renderer/BaseRenderer.java
@@ -0,0 +1,28 @@
+package org.commonmark.renderer;
+
+import org.commonmark.node.Node;
+
+public abstract class BaseRenderer implements Renderer {
+
+ @Override
+ public void render(Node node, Appendable output) {
+ NodeRendererContext context = createContext(output);
+ context.render(node);
+ }
+
+
+ @Override
+ public String render(Node node) {
+ StringBuilder sb = new StringBuilder();
+ render(node, sb);
+ return sb.toString();
+ }
+
+ /**
+ * Create context for renderer.
+ *
+ * @param out the output for rendering
+ * @return context for renderer
+ */
+ protected abstract NodeRendererContext createContext(Appendable out);
+}
diff --git a/commonmark/src/main/java/org/commonmark/html/renderer/NodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/NodeRenderer.java
similarity index 92%
rename from commonmark/src/main/java/org/commonmark/html/renderer/NodeRenderer.java
rename to commonmark/src/main/java/org/commonmark/renderer/NodeRenderer.java
index 472bdc6ce..e2d5ebc96 100644
--- a/commonmark/src/main/java/org/commonmark/html/renderer/NodeRenderer.java
+++ b/commonmark/src/main/java/org/commonmark/renderer/NodeRenderer.java
@@ -1,4 +1,4 @@
-package org.commonmark.html.renderer;
+package org.commonmark.renderer;
import org.commonmark.node.Node;
diff --git a/commonmark/src/main/java/org/commonmark/renderer/NodeRendererContext.java b/commonmark/src/main/java/org/commonmark/renderer/NodeRendererContext.java
new file mode 100644
index 000000000..22e349284
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/renderer/NodeRendererContext.java
@@ -0,0 +1,22 @@
+package org.commonmark.renderer;
+
+import org.commonmark.node.Node;
+
+/**
+ * The context for node rendering, including configuration and functionality for the node renderer to use.
+ */
+public interface NodeRendererContext {
+
+ /**
+ * Render the specified node and its children using the configured renderers. This should be used to render child
+ * nodes; be careful not to pass the node that is being rendered, that would result in an endless loop.
+ *
+ * @param node the node to render
+ */
+ void render(Node node);
+
+ /**
+ * @return the writer to use
+ */
+ W getWriter();
+}
diff --git a/commonmark/src/main/java/org/commonmark/html/renderer/NodeRendererFactory.java b/commonmark/src/main/java/org/commonmark/renderer/NodeRendererFactory.java
similarity index 68%
rename from commonmark/src/main/java/org/commonmark/html/renderer/NodeRendererFactory.java
rename to commonmark/src/main/java/org/commonmark/renderer/NodeRendererFactory.java
index 2080b0030..f4f5e7a44 100644
--- a/commonmark/src/main/java/org/commonmark/html/renderer/NodeRendererFactory.java
+++ b/commonmark/src/main/java/org/commonmark/renderer/NodeRendererFactory.java
@@ -1,9 +1,9 @@
-package org.commonmark.html.renderer;
+package org.commonmark.renderer;
/**
* Factory for instantiating new node renderers when rendering is done.
*/
-public interface NodeRendererFactory {
+public interface NodeRendererFactory {
/**
* Create a new node renderer for the specified rendering context.
@@ -11,6 +11,5 @@ public interface NodeRendererFactory {
* @param context the context for rendering (normally passed on to the node renderer)
* @return a node renderer
*/
- NodeRenderer create(NodeRendererContext context);
-
+ NodeRenderer create(C context);
}
diff --git a/commonmark/src/main/java/org/commonmark/renderer/Renderer.java b/commonmark/src/main/java/org/commonmark/renderer/Renderer.java
new file mode 100644
index 000000000..e954439a6
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/renderer/Renderer.java
@@ -0,0 +1,22 @@
+package org.commonmark.renderer;
+
+import org.commonmark.node.Node;
+
+public interface Renderer {
+
+ /**
+ * Render the tree of nodes to output.
+ *
+ * @param node the root node
+ * @param output output for rendering
+ */
+ void render(Node node, Appendable output);
+
+ /**
+ * Render the tree of nodes to string.
+ *
+ * @param node the root node
+ * @return the rendered string
+ */
+ String render(Node node);
+}
diff --git a/commonmark/src/main/java/org/commonmark/renderer/Writer.java b/commonmark/src/main/java/org/commonmark/renderer/Writer.java
new file mode 100644
index 000000000..8eba55305
--- /dev/null
+++ b/commonmark/src/main/java/org/commonmark/renderer/Writer.java
@@ -0,0 +1,4 @@
+package org.commonmark.renderer;
+
+public interface Writer {
+}
diff --git a/commonmark/src/test/java/org/commonmark/test/DelimiterProcessorTest.java b/commonmark/src/test/java/org/commonmark/test/DelimiterProcessorTest.java
index bd58427c8..631192fb4 100644
--- a/commonmark/src/test/java/org/commonmark/test/DelimiterProcessorTest.java
+++ b/commonmark/src/test/java/org/commonmark/test/DelimiterProcessorTest.java
@@ -1,15 +1,16 @@
package org.commonmark.test;
import org.commonmark.html.HtmlRenderer;
-import org.commonmark.html.renderer.NodeRenderer;
-import org.commonmark.html.renderer.NodeRendererContext;
-import org.commonmark.html.renderer.NodeRendererFactory;
+import org.commonmark.html.renderer.HtmlNodeRendererContext;
+import org.commonmark.html.renderer.HtmlNodeRendererFactory;
import org.commonmark.node.CustomNode;
import org.commonmark.node.Node;
import org.commonmark.node.Text;
-import org.commonmark.parser.delimiter.DelimiterProcessor;
import org.commonmark.parser.Parser;
+import org.commonmark.parser.delimiter.DelimiterProcessor;
import org.commonmark.parser.delimiter.DelimiterRun;
+import org.commonmark.renderer.NodeRenderer;
+import org.commonmark.renderer.NodeRendererContext;
import org.junit.Test;
import java.util.Collections;
@@ -126,10 +127,10 @@ public void process(Text opener, Text closer, int delimiterUse) {
private static class UpperCaseNode extends CustomNode {
}
- private static class UpperCaseNodeRendererFactory implements NodeRendererFactory {
+ private static class UpperCaseNodeRendererFactory implements HtmlNodeRendererFactory {
@Override
- public NodeRenderer create(NodeRendererContext context) {
+ public NodeRenderer create(HtmlNodeRendererContext context) {
return new UpperCaseNodeRenderer(context);
}
}
diff --git a/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java b/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java
index 6a77f8a0a..6d2bbff14 100644
--- a/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java
+++ b/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java
@@ -2,14 +2,14 @@
import org.commonmark.html.AttributeProvider;
import org.commonmark.html.HtmlRenderer;
-import org.commonmark.html.renderer.NodeRenderer;
-import org.commonmark.html.renderer.NodeRendererContext;
-import org.commonmark.html.renderer.NodeRendererFactory;
+import org.commonmark.html.renderer.HtmlNodeRendererContext;
+import org.commonmark.html.renderer.HtmlNodeRendererFactory;
import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.Image;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
+import org.commonmark.renderer.NodeRenderer;
import org.junit.Test;
import java.util.Collections;
@@ -125,9 +125,9 @@ public void setAttributes(Node node, Map attributes) {
@Test
public void overrideNodeRender() {
- NodeRendererFactory nodeRendererFactory = new NodeRendererFactory() {
+ HtmlNodeRendererFactory nodeRendererFactory = new HtmlNodeRendererFactory() {
@Override
- public NodeRenderer create(final NodeRendererContext context) {
+ public NodeRenderer create(final HtmlNodeRendererContext context) {
return new NodeRenderer() {
@Override
public Set> getNodeTypes() {
@@ -136,7 +136,7 @@ public Set> getNodeTypes() {
@Override
public void render(Node node) {
- context.getHtmlWriter().text("test");
+ context.getWriter().text("test");
}
};
}
diff --git a/commonmark/src/test/java/org/commonmark/test/TextContentRendererTest.java b/commonmark/src/test/java/org/commonmark/test/TextContentRendererTest.java
new file mode 100644
index 000000000..7a6b6dca8
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/test/TextContentRendererTest.java
@@ -0,0 +1,169 @@
+package org.commonmark.test;
+
+import org.commonmark.content.TextContentRenderer;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class TextContentRendererTest {
+
+ @Test
+ public void textContentEmphasis() {
+ String rendered;
+
+ rendered = defaultRenderer().render(parse("foo\n***foo***\nbar\n\n***bar***"));
+ assertEquals("foo\nfoo\nbar\nbar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo\n***foo\nbar***\n\n***bar***"));
+ assertEquals("foo foo bar bar", rendered);
+ }
+
+ @Test
+ public void textContentQuotes() {
+ String rendered;
+
+ rendered = defaultRenderer().render(parse("foo\n>foo\nbar\n\nbar"));
+ assertEquals("foo\n«foo\nbar»\nbar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo\n>foo\nbar\n\nbar"));
+ assertEquals("foo «foo bar» bar", rendered);
+ }
+
+ @Test
+ public void textContentLinks() {
+ String rendered;
+
+ rendered = defaultRenderer().render(parse("foo [text](http://link \"title\") bar"));
+ assertEquals("foo \"text\" (title: http://link) bar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo [text](http://link) bar"));
+ assertEquals("foo \"text\" (http://link) bar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo [text]() bar"));
+ assertEquals("foo \"text\" bar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo http://link bar"));
+ assertEquals("foo http://link bar", rendered);
+ }
+
+ @Test
+ public void textContentImages() {
+ String rendered;
+
+ rendered = defaultRenderer().render(parse("foo  bar"));
+ assertEquals("foo \"text\" (title: http://link) bar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo  bar"));
+ assertEquals("foo \"text\" (http://link) bar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo ![text]() bar"));
+ assertEquals("foo \"text\" bar", rendered);
+ }
+
+ @Test
+ public void textContentLists() {
+ String rendered;
+
+ rendered = defaultRenderer().render(parse("foo\n* foo\n* bar\n\nbar"));
+ assertEquals("foo\n* foo\n* bar\nbar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo\n- foo\n- bar\n\nbar"));
+ assertEquals("foo\n- foo\n- bar\nbar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo\n* foo\n* bar\n\nbar"));
+ assertEquals("foo foo bar bar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo\n1. foo\n2. bar\n\nbar"));
+ assertEquals("foo\n1. foo\n2. bar\nbar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo\n0) foo\n1) bar\n\nbar"));
+ assertEquals("foo\n0) foo\n1) bar\nbar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo\n1. foo\n2. bar\n\nbar"));
+ assertEquals("foo 1. foo 2. bar bar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo\n0) foo\n1) bar\n\nbar"));
+ assertEquals("foo 0) foo 1) bar bar", rendered);
+ }
+
+ @Test
+ public void textContentCode() {
+ String rendered;
+
+ rendered = defaultRenderer().render(parse("foo `code` bar"));
+ assertEquals("foo \"code\" bar", rendered);
+ }
+
+ @Test
+ public void textContentCodeBlock() {
+ String rendered;
+
+ rendered = defaultRenderer().render(parse("foo\n```\nfoo\nbar\n```\nbar"));
+ assertEquals("foo\nfoo\nbar\nbar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo\n```\nfoo\nbar\n```\nbar"));
+ assertEquals("foo foo bar bar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo\n\n foo\n bar\nbar"));
+ assertEquals("foo\nfoo\n bar\nbar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo\n\n foo\n bar\nbar"));
+ assertEquals("foo foo bar bar", rendered);
+ }
+
+ @Test
+ public void textContentBrakes() {
+ String rendered;
+
+ rendered = defaultRenderer().render(parse("foo\nbar"));
+ assertEquals("foo\nbar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo\nbar"));
+ assertEquals("foo bar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo \nbar"));
+ assertEquals("foo\nbar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo \nbar"));
+ assertEquals("foo bar", rendered);
+
+ rendered = defaultRenderer().render(parse("foo\n___\nbar"));
+ assertEquals("foo\n***\nbar", rendered);
+
+ rendered = strippedRenderer().render(parse("foo\n___\nbar"));
+ assertEquals("foo bar", rendered);
+ }
+
+ @Test
+ public void textContentHtml() {
+ String rendered;
+
+ String html = "\n" +
+ " \n" +
+ " | \n" +
+ " foobar\n" +
+ " | \n" +
+ "
\n" +
+ "
";
+ rendered = defaultRenderer().render(parse(html));
+ assertEquals(html, rendered);
+
+ html = "foo foobar bar";
+ rendered = defaultRenderer().render(parse(html));
+ assertEquals(html, rendered);
+ }
+
+ private TextContentRenderer defaultRenderer() {
+ return TextContentRenderer.builder().build();
+ }
+
+ private TextContentRenderer strippedRenderer() {
+ return TextContentRenderer.builder().stripNewlines(true).build();
+ }
+
+ private Node parse(String source) {
+ return Parser.builder().build().parse(source);
+ }
+}
diff --git a/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java b/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java
new file mode 100644
index 000000000..a0bf41b18
--- /dev/null
+++ b/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java
@@ -0,0 +1,55 @@
+package org.commonmark.test;
+
+import org.commonmark.content.TextContentWriter;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class TextContentWriterTest {
+
+ @Test
+ public void whitespace() throws Exception {
+ StringBuilder stringBuilder = new StringBuilder();
+ TextContentWriter writer = new TextContentWriter(stringBuilder);
+ writer.write("foo");
+ writer.whitespace();
+ writer.write("bar");
+ assertEquals("foo bar", stringBuilder.toString());
+ }
+
+ @Test
+ public void colon() throws Exception {
+ StringBuilder stringBuilder = new StringBuilder();
+ TextContentWriter writer = new TextContentWriter(stringBuilder);
+ writer.write("foo");
+ writer.colon();
+ writer.write("bar");
+ assertEquals("foo:bar", stringBuilder.toString());
+ }
+
+ @Test
+ public void line() throws Exception {
+ StringBuilder stringBuilder = new StringBuilder();
+ TextContentWriter writer = new TextContentWriter(stringBuilder);
+ writer.write("foo");
+ writer.line();
+ writer.write("bar");
+ assertEquals("foo\nbar", stringBuilder.toString());
+ }
+
+ @Test
+ public void writeStripped() throws Exception {
+ StringBuilder stringBuilder = new StringBuilder();
+ TextContentWriter writer = new TextContentWriter(stringBuilder);
+ writer.writeStripped("foo\n bar\n");
+ assertEquals("foo bar", stringBuilder.toString());
+ }
+
+ @Test
+ public void write() throws Exception {
+ StringBuilder stringBuilder = new StringBuilder();
+ TextContentWriter writer = new TextContentWriter(stringBuilder);
+ writer.writeStripped("foo bar");
+ assertEquals("foo bar", stringBuilder.toString());
+ }
+}