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 }