1 module hunt.markdown.renderer.text.CoreTextContentNodeRenderer;
2 
3 import hunt.markdown.node;
4 import hunt.markdown.node.Heading;
5 import hunt.markdown.node.AbstractVisitor;
6 import hunt.markdown.renderer.NodeRenderer;
7 import hunt.markdown.renderer.text.TextContentNodeRendererContext;
8 import hunt.markdown.renderer.text.TextContentWriter;
9 import hunt.markdown.internal.renderer.text.BulletListHolder;
10 import hunt.markdown.internal.renderer.text.ListHolder;
11 import hunt.markdown.internal.renderer.text.OrderedListHolder;
12 
13 import hunt.collection.HashSet;
14 import hunt.collection.Set;
15 import hunt.Char;
16 import hunt.util.StringBuilder;
17 
18 import std.conv;
19 
20 alias Character = Char;
21 
22 /**
23  * The node renderer that renders all the core nodes (comes last in the order of node renderers).
24  */
25 class CoreTextContentNodeRenderer : AbstractVisitor, NodeRenderer {
26 
27     protected TextContentNodeRendererContext context;
28     private TextContentWriter textContent;
29 
30     private ListHolder listHolder;
31 
32     public this(TextContentNodeRendererContext context) {
33         this.context = context;
34         this.textContent = context.getWriter();
35     }
36 
37     override public Set!TypeInfo_Class getNodeTypes() {
38         return new HashSet!TypeInfo_Class([
39                 typeid(Document),
40                 typeid(Heading),
41                 typeid(Paragraph),
42                 typeid(BlockQuote),
43                 typeid(BulletList),
44                 typeid(FencedCodeBlock),
45                 typeid(HtmlBlock),
46                 typeid(ThematicBreak),
47                 typeid(IndentedCodeBlock),
48                 typeid(Link),
49                 typeid(ListItem),
50                 typeid(OrderedList),
51                 typeid(Image),
52                 typeid(Emphasis),
53                 typeid(StrongEmphasis),
54                 typeid(Text),
55                 typeid(Code),
56                 typeid(HtmlInline),
57                 typeid(SoftLineBreak),
58                 typeid(HardLineBreak)
59         ]);
60     }
61 
62     public void render(Node node) {
63         node.accept(this);
64     }
65 
66     override public void visit(Document document) {
67         // No rendering itself
68         visitChildren(document);
69     }
70 
71     override public void visit(BlockQuote blockQuote) {
72         textContent.write("«");
73         visitChildren(blockQuote);
74         textContent.write("»");
75 
76         writeEndOfLineIfNeeded(blockQuote, null);
77     }
78 
79     override public void visit(BulletList bulletList) {
80         if (listHolder !is null) {
81             writeEndOfLine();
82         }
83         listHolder = new BulletListHolder(listHolder, bulletList);
84         visitChildren(bulletList);
85         writeEndOfLineIfNeeded(bulletList, null);
86         if (listHolder.getParent() !is null) {
87             listHolder = listHolder.getParent();
88         } else {
89             listHolder = null;
90         }
91     }
92 
93     override public void visit(Code code) {
94         textContent.write('\"');
95         textContent.write(code.getLiteral());
96         textContent.write('\"');
97     }
98 
99     override public void visit(FencedCodeBlock fencedCodeBlock) {
100         if (context.stripNewlines()) {
101             textContent.writeStripped(fencedCodeBlock.getLiteral());
102             writeEndOfLineIfNeeded(fencedCodeBlock, null);
103         } else {
104             textContent.write(fencedCodeBlock.getLiteral());
105         }
106     }
107 
108     override public void visit(HardLineBreak hardLineBreak) {
109         writeEndOfLineIfNeeded(hardLineBreak, null);
110     }
111 
112     override public void visit(Heading heading) {
113         visitChildren(heading);
114         writeEndOfLineIfNeeded(heading, new Char(':'));
115     }
116 
117     override public void visit(ThematicBreak thematicBreak) {
118         if (!context.stripNewlines()) {
119             textContent.write("***");
120         }
121         writeEndOfLineIfNeeded(thematicBreak, null);
122     }
123 
124     override public void visit(HtmlInline htmlInline) {
125         writeText(htmlInline.getLiteral());
126     }
127 
128     override public void visit(HtmlBlock htmlBlock) {
129         writeText(htmlBlock.getLiteral());
130     }
131 
132     override public void visit(Image image) {
133         writeLink(image, image.getTitle(), image.getDestination());
134     }
135 
136     override public void visit(IndentedCodeBlock indentedCodeBlock) {
137         if (context.stripNewlines()) {
138             textContent.writeStripped(indentedCodeBlock.getLiteral());
139             writeEndOfLineIfNeeded(indentedCodeBlock, null);
140         } else {
141             textContent.write(indentedCodeBlock.getLiteral());
142         }
143     }
144 
145     override public void visit(Link link) {
146         writeLink(link, link.getTitle(), link.getDestination());
147     }
148 
149     override public void visit(ListItem listItem) {
150         if (listHolder !is null && cast(OrderedListHolder)listHolder !is null) {
151             OrderedListHolder orderedListHolder = cast(OrderedListHolder) listHolder;
152             string indent = context.stripNewlines() ? "" : orderedListHolder.getIndent();
153             textContent.write(indent ~ orderedListHolder.getCounter().to!string ~ orderedListHolder.getDelimiter() ~ " ");
154             visitChildren(listItem);
155             writeEndOfLineIfNeeded(listItem, null);
156             orderedListHolder.increaseCounter();
157         } else if (listHolder !is null && cast(BulletListHolder)listHolder !is null) {
158             BulletListHolder bulletListHolder = cast(BulletListHolder) listHolder;
159             if (!context.stripNewlines()) {
160                 textContent.write(bulletListHolder.getIndent() ~ bulletListHolder.getMarker() ~ " ");
161             }
162             visitChildren(listItem);
163             writeEndOfLineIfNeeded(listItem, null);
164         }
165     }
166 
167     override public void visit(OrderedList orderedList) {
168         if (listHolder !is null) {
169             writeEndOfLine();
170         }
171         listHolder = new OrderedListHolder(listHolder, orderedList);
172         visitChildren(orderedList);
173         writeEndOfLineIfNeeded(orderedList, null);
174         if (listHolder.getParent() !is null) {
175             listHolder = listHolder.getParent();
176         } else {
177             listHolder = null;
178         }
179     }
180 
181     override public void visit(Paragraph paragraph) {
182         visitChildren(paragraph);
183         // Add "end of line" only if its "root paragraph.
184         if (paragraph.getParent() is null || cast(Document)paragraph.getParent() !is null) {
185             writeEndOfLineIfNeeded(paragraph, null);
186         }
187     }
188 
189     override public void visit(SoftLineBreak softLineBreak) {
190         writeEndOfLineIfNeeded(softLineBreak, null);
191     }
192 
193     override public void visit(Text text) {
194         writeText(text.getLiteral());
195     }
196 
197     override protected void visitChildren(Node parent) {
198         Node node = parent.getFirstChild();
199         while (node !is null) {
200             Node next = node.getNext();
201             context.render(node);
202             node = next;
203         }
204     }
205 
206     private void writeText(string text) {
207         if (context.stripNewlines()) {
208             textContent.writeStripped(text);
209         } else {
210             textContent.write(text);
211         }
212     }
213 
214     private void writeLink(Node node, string title, string destination) {
215         bool hasChild = node.getFirstChild() !is null;
216         bool hasTitle = title !is null && !(title == destination);
217         bool hasDestination = destination !is null && !(destination == (""));
218 
219         if (hasChild) {
220             textContent.write('"');
221             visitChildren(node);
222             textContent.write('"');
223             if (hasTitle || hasDestination) {
224                 textContent.whitespace();
225                 textContent.write('(');
226             }
227         }
228 
229         if (hasTitle) {
230             textContent.write(title);
231             if (hasDestination) {
232                 textContent.colon();
233                 textContent.whitespace();
234             }
235         }
236 
237         if (hasDestination) {
238             textContent.write(destination);
239         }
240 
241         if (hasChild && (hasTitle || hasDestination)) {
242             textContent.write(')');
243         }
244     }
245 
246     private void writeEndOfLineIfNeeded(Node node, Character c) {
247         if (context.stripNewlines()) {
248             if (c !is null) {
249                 textContent.write(c.charValue);
250             }
251             if (node.getNext() !is null) {
252                 textContent.whitespace();
253             }
254         } else {
255             if (node.getNext() !is null) {
256                 textContent.line();
257             }
258         }
259     }
260 
261     private void writeEndOfLine() {
262         if (context.stripNewlines()) {
263             textContent.whitespace();
264         } else {
265             textContent.line();
266         }
267     }
268 }