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 }