1 module hunt.markdown.renderer.html.HtmlRenderer;
2 
3 import hunt.markdown.Extension;
4 import hunt.markdown.internal.renderer.NodeRendererMap;
5 import hunt.markdown.internal.util.Escaping;
6 import hunt.markdown.node.HtmlBlock;
7 import hunt.markdown.node.HtmlInline;
8 import hunt.markdown.node.Node;
9 import hunt.markdown.renderer.NodeRenderer;
10 import hunt.markdown.renderer.Renderer;
11 import hunt.markdown.renderer.html.HtmlWriter;
12 import hunt.markdown.renderer.html.AttributeProvider;
13 import hunt.markdown.renderer.html.AttributeProviderFactory;
14 import hunt.markdown.renderer.html.HtmlNodeRendererFactory;
15 import hunt.markdown.renderer.html.AttributeProviderContext;
16 import hunt.markdown.renderer.html.HtmlNodeRendererContext;
17 import hunt.markdown.renderer.html.CoreHtmlNodeRenderer;
18 
19 import hunt.collection.ArrayList;
20 import hunt.collection.LinkedHashMap;
21 import hunt.collection.List;
22 import hunt.collection.Map;
23 
24 import hunt.util.Appendable;
25 import hunt.util.Common;
26 import hunt.util.StringBuilder;
27 
28 /**
29  * Renders a tree of nodes to HTML.
30  * <p>
31  * Start with the {@link #builder} method to configure the renderer. Example:
32  * <pre><code>
33  * HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build();
34  * renderer.render(node);
35  * </code></pre>
36  */
37 class HtmlRenderer : Renderer {
38 
39     private string softbreak;
40     private bool escapeHtml;
41     private bool percentEncodeUrls;
42     private List!(AttributeProviderFactory) attributeProviderFactories;
43     private List!(HtmlNodeRendererFactory) nodeRendererFactories;
44 
45     private this(Builder builder) {
46         this.softbreak = builder._softbreak;
47         this.escapeHtml = builder._escapeHtml;
48         this.percentEncodeUrls = builder._percentEncodeUrls;
49         this.attributeProviderFactories = new ArrayList!AttributeProviderFactory(builder._attributeProviderFactories);
50 
51         this.nodeRendererFactories = new ArrayList!HtmlNodeRendererFactory(builder._nodeRendererFactories.size() + 1);
52         this.nodeRendererFactories.addAll(builder._nodeRendererFactories);
53         // Add as last. This means clients can override the rendering of core nodes if they want.
54         this.nodeRendererFactories.add(new class HtmlNodeRendererFactory {
55             override public NodeRenderer create(HtmlNodeRendererContext context) {
56                 return new CoreHtmlNodeRenderer(context);
57             }
58         });
59     }
60 
61     /**
62      * Create a new builder for configuring an {@link HtmlRenderer}.
63      *
64      * @return a builder
65      */
66     public static Builder builder() {
67         return new Builder();
68     }
69 
70     public void render(Node node, Appendable output) {
71         RendererContext context = new RendererContext(new HtmlWriter(output));
72         context.render(node);
73     }
74 
75     override public string render(Node node) {
76         StringBuilder sb = new StringBuilder();
77         render(node, sb);
78         return sb.toString();
79     }
80 
81     /**
82      * Builder for configuring an {@link HtmlRenderer}. See methods for default configuration.
83      */
84     public static class Builder {
85 
86         private string _softbreak = "\n";
87         private bool _escapeHtml = false;
88         private bool _percentEncodeUrls = false;
89 
90         private List!(AttributeProviderFactory) _attributeProviderFactories;
91         private List!(HtmlNodeRendererFactory) _nodeRendererFactories;
92 
93         this()
94         {
95             _attributeProviderFactories = new ArrayList!AttributeProviderFactory();
96             _nodeRendererFactories = new ArrayList!HtmlNodeRendererFactory();
97         }
98 
99         /**
100          * @return the configured {@link HtmlRenderer}
101          */
102         public HtmlRenderer build() {
103             return new HtmlRenderer(this);
104         }
105 
106         /**
107          * The HTML to use for rendering a softbreak, defaults to {@code "\n"} (meaning the rendered result doesn't have
108          * a line break).
109          * <p>
110          * Set it to {@code "<br>"} (or {@code "<br />"} to make them hard breaks.
111          * <p>
112          * Set it to {@code " "} to ignore line wrapping in the source.
113          *
114          * @param softbreak HTML for softbreak
115          * @return {@code this}
116          */
117         public Builder softbreak(string softbreak) {
118             this._softbreak = softbreak;
119             return this;
120         }
121 
122         /**
123          * Whether {@link HtmlInline} and {@link HtmlBlock} should be escaped, defaults to {@code false}.
124          * <p>
125          * Note that {@link HtmlInline} is only a tag itself, not the text between an opening tag and a closing tag. So
126          * markup in the text will be parsed as normal and is not affected by this option.
127          *
128          * @param escapeHtml true for escaping, false for preserving raw HTML
129          * @return {@code this}
130          */
131         public Builder escapeHtml(bool escapeHtml) {
132             this._escapeHtml = escapeHtml;
133             return this;
134         }
135 
136         /**
137          * Whether URLs of link or images should be percent-encoded, defaults to {@code false}.
138          * <p>
139          * If enabled, the following is done:
140          * <ul>
141          * <li>Existing percent-encoded parts are preserved (e.g. "%20" is kept as "%20")</li>
142          * <li>Reserved characters such as "/" are preserved, except for "[" and "]" (see encodeURI in JS)</li>
143          * <li>Unreserved characters such as "a" are preserved</li>
144          * <li>Other characters such umlauts are percent-encoded</li>
145          * </ul>
146          *
147          * @param percentEncodeUrls true to percent-encode, false for leaving as-is
148          * @return {@code this}
149          */
150         public Builder percentEncodeUrls(bool percentEncodeUrls) {
151             this._percentEncodeUrls = percentEncodeUrls;
152             return this;
153         }
154 
155         /**
156          * Add a factory for an attribute provider for adding/changing HTML attributes to the rendered tags.
157          *
158          * @param attributeProviderFactory the attribute provider factory to add
159          * @return {@code this}
160          */
161         public Builder attributeProviderFactory(AttributeProviderFactory attributeProviderFactory) {
162             this._attributeProviderFactories.add(attributeProviderFactory);
163             return this;
164         }
165 
166         /**
167          * Add a factory for instantiating a node renderer (done when rendering). This allows to override the rendering
168          * of node types or define rendering for custom node types.
169          * <p>
170          * If multiple node renderers for the same node type are created, the one from the factory that was added first
171          * "wins". (This is how the rendering for core node types can be overridden; the default rendering comes last.)
172          *
173          * @param nodeRendererFactory the factory for creating a node renderer
174          * @return {@code this}
175          */
176         public Builder nodeRendererFactory(HtmlNodeRendererFactory nodeRendererFactory) {
177             this._nodeRendererFactories.add(nodeRendererFactory);
178             return this;
179         }
180 
181         /**
182          * @param extensions extensions to use on this HTML renderer
183          * @return {@code this}
184          */
185         public Builder extensions(Iterable!Extension extensions) {
186             foreach (Extension extension ; extensions) {
187                 if (cast(HtmlRendererExtension)extension !is null) {
188                     HtmlRendererExtension htmlRendererExtension = cast(HtmlRendererExtension) extension;
189                     htmlRendererExtension.extend(this);
190                 }
191             }
192             return this;
193         }
194     }
195 
196     /**
197      * Extension for {@link HtmlRenderer}.
198      */
199     public interface HtmlRendererExtension : Extension {
200         void extend(Builder rendererBuilder);
201     }
202 
203     private class RendererContext : HtmlNodeRendererContext, AttributeProviderContext {
204 
205         private HtmlWriter _htmlWriter;
206         private List!(AttributeProvider) _attributeProviders;
207         private NodeRendererMap _nodeRendererMap;
208 
209         private this(HtmlWriter htmlWriter) {
210             _nodeRendererMap = new NodeRendererMap();
211             this._htmlWriter = htmlWriter;
212 
213             _attributeProviders = new ArrayList!AttributeProvider(attributeProviderFactories.size());
214             foreach (AttributeProviderFactory attributeProviderFactory ; attributeProviderFactories) {
215                 _attributeProviders.add(attributeProviderFactory.create(this));
216             }
217 
218             // The first node renderer for a node type "wins".
219             for (int i = nodeRendererFactories.size() - 1; i >= 0; i--) {
220                 HtmlNodeRendererFactory nodeRendererFactory = nodeRendererFactories.get(i);
221                 NodeRenderer nodeRenderer = nodeRendererFactory.create(this);
222                 _nodeRendererMap.add(nodeRenderer);
223             }
224         }
225 
226         override public bool shouldEscapeHtml() {
227             return escapeHtml;
228         }
229 
230         public string encodeUrl(string url) {
231             if (percentEncodeUrls) {
232                 return Escaping.percentEncodeUrl(url);
233             } else {
234                 return url;
235             }
236         }
237 
238         public Map!(string, string) extendAttributes(Node node, string tagName, Map!(string, string) attributes) {
239             Map!(string, string) attrs = new LinkedHashMap!(string, string)(attributes);
240             setCustomAttributes(node, tagName, attrs);
241             return attrs;
242         }
243 
244         override public HtmlWriter getWriter() {
245             return _htmlWriter;
246         }
247 
248         public string getSoftbreak() {
249             return softbreak;
250         }
251 
252         public void render(Node node) {
253             _nodeRendererMap.render(node);
254         }
255 
256         private void setCustomAttributes(Node node, string tagName, Map!(string, string) attrs) {
257             foreach (AttributeProvider attributeProvider ; _attributeProviders) {
258                 attributeProvider.setAttributes(node, tagName, attrs);
259             }
260         }
261     }
262 }