1 module hunt.markdown.renderer.html.CoreHtmlNodeRenderer; 2 3 import hunt.markdown.node; 4 import hunt.markdown.node.AbstractVisitor; 5 import hunt.markdown.node.Heading; 6 import hunt.markdown.renderer.NodeRenderer; 7 import hunt.markdown.renderer.html.HtmlWriter; 8 import hunt.markdown.renderer.html.HtmlNodeRendererContext; 9 10 import hunt.collection.Set; 11 import hunt.collection.HashSet; 12 import hunt.collection.Map; 13 import hunt.collection.LinkedHashMap; 14 import hunt.collection.Collections; 15 import hunt.text; 16 import hunt.util.StringBuilder; 17 18 import std.conv; 19 import std.string; 20 /** 21 * The node renderer that renders all the core nodes (comes last in the order of node renderers). 22 */ 23 class CoreHtmlNodeRenderer : AbstractVisitor, NodeRenderer { 24 25 protected HtmlNodeRendererContext context; 26 private HtmlWriter html; 27 28 public this(HtmlNodeRendererContext context) { 29 this.context = context; 30 this.html = context.getWriter(); 31 } 32 33 public Set!TypeInfo_Class getNodeTypes() { 34 return new HashSet!TypeInfo_Class([ 35 typeid(Document), 36 typeid(Heading), 37 typeid(Paragraph), 38 typeid(BlockQuote), 39 typeid(BulletList), 40 typeid(FencedCodeBlock), 41 typeid(HtmlBlock), 42 typeid(ThematicBreak), 43 typeid(IndentedCodeBlock), 44 typeid(Link), 45 typeid(ListItem), 46 typeid(OrderedList), 47 typeid(Image), 48 typeid(Emphasis), 49 typeid(StrongEmphasis), 50 typeid(Text), 51 typeid(Code), 52 typeid(HtmlInline), 53 typeid(SoftLineBreak), 54 typeid(HardLineBreak) 55 ]); 56 } 57 58 public void render(Node node) { 59 node.accept(this); 60 } 61 62 override public void visit(Document document) { 63 // No rendering itself 64 visitChildren(document); 65 } 66 67 override public void visit(Heading heading) { 68 string htag = "h" ~ heading.getLevel().to!string; 69 html.line(); 70 html.tag(htag, getAttrs(heading, htag)); 71 visitChildren(heading); 72 html.tag('/' ~ htag); 73 html.line(); 74 } 75 76 override public void visit(Paragraph paragraph) { 77 bool inTightList = isInTightList(paragraph); 78 if (!inTightList) { 79 html.line(); 80 html.tag("p", getAttrs(paragraph, "p")); 81 } 82 visitChildren(paragraph); 83 if (!inTightList) { 84 html.tag("/p"); 85 html.line(); 86 } 87 } 88 89 override public void visit(BlockQuote blockQuote) { 90 html.line(); 91 html.tag("blockquote", getAttrs(blockQuote, "blockquote")); 92 html.line(); 93 visitChildren(blockQuote); 94 html.line(); 95 html.tag("/blockquote"); 96 html.line(); 97 } 98 99 override public void visit(BulletList bulletList) { 100 renderListBlock(bulletList, "ul", getAttrs(bulletList, "ul")); 101 } 102 103 override public void visit(FencedCodeBlock fencedCodeBlock) { 104 string literal = fencedCodeBlock.getLiteral(); 105 Map!(string, string) attributes = new LinkedHashMap!(string, string)(); 106 string info = fencedCodeBlock.getInfo(); 107 if (info !is null && !info.isEmpty()) { 108 int space = cast(int)(info.indexOf(" ")); 109 string language; 110 if (space == -1) { 111 language = info; 112 } else { 113 language = info.substring(0, space); 114 } 115 attributes.put("class", "language-" ~ language); 116 } 117 renderCodeBlock(literal, fencedCodeBlock, attributes); 118 } 119 120 override public void visit(HtmlBlock htmlBlock) { 121 html.line(); 122 if (context.shouldEscapeHtml()) { 123 html.tag("p", getAttrs(htmlBlock, "p")); 124 html.text(htmlBlock.getLiteral()); 125 html.tag("/p"); 126 } else { 127 html.raw(htmlBlock.getLiteral()); 128 } 129 html.line(); 130 } 131 132 override public void visit(ThematicBreak thematicBreak) { 133 html.line(); 134 html.tag("hr", getAttrs(thematicBreak, "hr"), true); 135 html.line(); 136 } 137 138 override public void visit(IndentedCodeBlock indentedCodeBlock) { 139 renderCodeBlock(indentedCodeBlock.getLiteral(), indentedCodeBlock, Collections.emptyMap!(string, string)()); 140 } 141 142 override public void visit(Link link) { 143 Map!(string, string) attrs = new LinkedHashMap!(string, string)(); 144 string url = context.encodeUrl(link.getDestination()); 145 attrs.put("href", url); 146 if (link.getTitle() !is null) { 147 attrs.put("title", link.getTitle()); 148 } 149 html.tag("a", getAttrs(link, "a", attrs)); 150 visitChildren(link); 151 html.tag("/a"); 152 } 153 154 override public void visit(ListItem listItem) { 155 html.tag("li", getAttrs(listItem, "li")); 156 visitChildren(listItem); 157 html.tag("/li"); 158 html.line(); 159 } 160 161 override public void visit(OrderedList orderedList) { 162 int start = orderedList.getStartNumber(); 163 Map!(string, string) attrs = new LinkedHashMap!(string, string)(); 164 if (start != 1) { 165 attrs.put("start", to!string(start)); 166 } 167 renderListBlock(orderedList, "ol", getAttrs(orderedList, "ol", attrs)); 168 } 169 170 override public void visit(Image image) { 171 string url = context.encodeUrl(image.getDestination()); 172 173 AltTextVisitor altTextVisitor = new AltTextVisitor(); 174 image.accept(altTextVisitor); 175 string altText = altTextVisitor.getAltText(); 176 177 Map!(string, string) attrs = new LinkedHashMap!(string, string)(); 178 attrs.put("src", url); 179 attrs.put("alt", altText); 180 if (image.getTitle() !is null) { 181 attrs.put("title", image.getTitle()); 182 } 183 184 html.tag("img", getAttrs(image, "img", attrs), true); 185 } 186 187 override public void visit(Emphasis emphasis) { 188 html.tag("em", getAttrs(emphasis, "em")); 189 visitChildren(emphasis); 190 html.tag("/em"); 191 } 192 193 override public void visit(StrongEmphasis strongEmphasis) { 194 html.tag("strong", getAttrs(strongEmphasis, "strong")); 195 visitChildren(strongEmphasis); 196 html.tag("/strong"); 197 } 198 199 override public void visit(Text text) { 200 html.text(text.getLiteral()); 201 } 202 203 override public void visit(Code code) { 204 html.tag("code", getAttrs(code, "code")); 205 html.text(code.getLiteral()); 206 html.tag("/code"); 207 } 208 209 override public void visit(HtmlInline htmlInline) { 210 if (context.shouldEscapeHtml()) { 211 html.text(htmlInline.getLiteral()); 212 } else { 213 html.raw(htmlInline.getLiteral()); 214 } 215 } 216 217 override public void visit(SoftLineBreak softLineBreak) { 218 html.raw(context.getSoftbreak()); 219 } 220 221 override public void visit(HardLineBreak hardLineBreak) { 222 html.tag("br", getAttrs(hardLineBreak, "br"), true); 223 html.line(); 224 } 225 226 override protected void visitChildren(Node parent) { 227 Node node = parent.getFirstChild(); 228 while (node !is null) { 229 Node next = node.getNext(); 230 context.render(node); 231 node = next; 232 } 233 } 234 235 private void renderCodeBlock(string literal, Node node, Map!(string, string) attributes) { 236 html.line(); 237 html.tag("pre", getAttrs(node, "pre")); 238 html.tag("code", getAttrs(node, "code", attributes)); 239 html.text(literal); 240 html.tag("/code"); 241 html.tag("/pre"); 242 html.line(); 243 } 244 245 private void renderListBlock(ListBlock listBlock, string tagName, Map!(string, string) attributes) { 246 html.line(); 247 html.tag(tagName, attributes); 248 html.line(); 249 visitChildren(listBlock); 250 html.line(); 251 html.tag('/' ~ tagName); 252 html.line(); 253 } 254 255 private bool isInTightList(Paragraph paragraph) { 256 Node parent = paragraph.getParent(); 257 if (parent !is null) { 258 Node gramps = parent.getParent(); 259 if (gramps !is null && cast(ListBlock)gramps !is null) { 260 ListBlock list = cast(ListBlock) gramps; 261 return list.isTight(); 262 } 263 } 264 return false; 265 } 266 267 private Map!(string, string) getAttrs(Node node, string tagName) { 268 return getAttrs(node, tagName, Collections.emptyMap!(string, string)()); 269 } 270 271 private Map!(string, string) getAttrs(Node node, string tagName, Map!(string, string) defaultAttributes) { 272 return context.extendAttributes(node, tagName, defaultAttributes); 273 } 274 275 private static class AltTextVisitor : AbstractVisitor { 276 277 private StringBuilder sb; 278 279 this() 280 { 281 sb = new StringBuilder(); 282 } 283 284 string getAltText() { 285 return sb.toString(); 286 } 287 288 override public void visit(Text text) { 289 sb.append(text.getLiteral()); 290 } 291 292 override public void visit(SoftLineBreak softLineBreak) { 293 sb.append('\n'); 294 } 295 296 override public void visit(HardLineBreak hardLineBreak) { 297 sb.append('\n'); 298 } 299 } 300 }