blob: c1c7d4f70fb3daa480b2aedac1423b107ae7564d [file] [log] [blame]
Shawn Pearcec9549982015-02-11 13:09:01 -08001// Copyright 2015 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.gitiles.doc;
16
17import static com.google.common.base.Preconditions.checkState;
Shawn Pearce108599e2015-02-11 13:28:37 -080018import static com.google.gitiles.doc.MarkdownUtil.getInnerText;
Shawn Pearcec9549982015-02-11 13:09:01 -080019
Shawn Pearce374f1842015-02-10 15:36:54 -080020import com.google.gitiles.GitilesView;
Shawn Pearcec9549982015-02-11 13:09:01 -080021import com.google.gitiles.doc.html.HtmlBuilder;
22import com.google.template.soy.data.SanitizedContent;
23import com.google.template.soy.shared.restricted.EscapingConventions;
24
25import org.pegdown.ast.AbbreviationNode;
26import org.pegdown.ast.AutoLinkNode;
27import org.pegdown.ast.BlockQuoteNode;
28import org.pegdown.ast.BulletListNode;
29import org.pegdown.ast.CodeNode;
30import org.pegdown.ast.DefinitionListNode;
31import org.pegdown.ast.DefinitionNode;
32import org.pegdown.ast.DefinitionTermNode;
33import org.pegdown.ast.ExpImageNode;
34import org.pegdown.ast.ExpLinkNode;
35import org.pegdown.ast.HeaderNode;
36import org.pegdown.ast.HtmlBlockNode;
37import org.pegdown.ast.InlineHtmlNode;
38import org.pegdown.ast.ListItemNode;
39import org.pegdown.ast.MailLinkNode;
40import org.pegdown.ast.Node;
41import org.pegdown.ast.OrderedListNode;
42import org.pegdown.ast.ParaNode;
43import org.pegdown.ast.QuotedNode;
44import org.pegdown.ast.RefImageNode;
45import org.pegdown.ast.RefLinkNode;
46import org.pegdown.ast.ReferenceNode;
47import org.pegdown.ast.RootNode;
48import org.pegdown.ast.SimpleNode;
49import org.pegdown.ast.SpecialTextNode;
50import org.pegdown.ast.StrikeNode;
51import org.pegdown.ast.StrongEmphSuperNode;
52import org.pegdown.ast.SuperNode;
53import org.pegdown.ast.TableBodyNode;
54import org.pegdown.ast.TableCaptionNode;
55import org.pegdown.ast.TableCellNode;
56import org.pegdown.ast.TableColumnNode;
57import org.pegdown.ast.TableHeaderNode;
58import org.pegdown.ast.TableNode;
59import org.pegdown.ast.TableRowNode;
60import org.pegdown.ast.TextNode;
61import org.pegdown.ast.VerbatimNode;
62import org.pegdown.ast.WikiLinkNode;
63
64/**
65 * Formats parsed markdown AST into HTML.
66 * <p>
67 * Callers must create a new instance for each RootNode.
68 */
69public class MarkdownToHtml implements Visitor {
70 private final ReferenceMap references = new ReferenceMap();
71 private final HtmlBuilder html = new HtmlBuilder();
72 private final TocFormatter toc = new TocFormatter(html, 3);
Shawn Pearce374f1842015-02-10 15:36:54 -080073 private final GitilesView view;
Shawn Pearcec9549982015-02-11 13:09:01 -080074 private TableState table;
75
Shawn Pearce374f1842015-02-10 15:36:54 -080076 public MarkdownToHtml(GitilesView view) {
77 this.view = view;
78 }
79
Shawn Pearcec9549982015-02-11 13:09:01 -080080 /** Render the document AST to sanitized HTML. */
81 public SanitizedContent toSoyHtml(RootNode node) {
82 if (node == null) {
83 return null;
84 }
85
86 toc.setRoot(node);
87 node.accept(this);
88 return html.toSoy();
89 }
90
91 @Override
92 public void visit(RootNode node) {
93 references.add(node);
94 visitChildren(node);
95 }
96
97 @Override
98 public void visit(TocNode node) {
99 toc.format();
100 }
101
102 @Override
Shawn Pearce96f6d5f2015-02-06 22:45:58 -0800103 public void visit(DivNode node) {
104 html.open("div").attribute("class", node.getStyleName());
105 visitChildren(node);
106 html.close("div");
107 }
108
109 @Override
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -0800110 public void visit(ColsNode node) {
111 html.open("div").attribute("class", "cols");
112 boolean open = false;
113 for (Node n : node.getChildren()) {
114 if (n instanceof HeaderNode || n instanceof DivNode) {
115 if (open) {
116 html.close("div");
117 }
118 html.open("div").attribute("class", "col-3");
119 open = true;
120 }
121 n.accept(this);
122 }
123 if (open) {
124 html.close("div");
125 }
126 html.close("div");
127 }
128
129 @Override
Shawn Pearcec9549982015-02-11 13:09:01 -0800130 public void visit(HeaderNode node) {
131 String tag = "h" + node.getLevel();
132 html.open(tag);
133 if (toc.include(node)) {
134 html.attribute("id", toc.idFromHeader(node));
135 }
136 visitChildren(node);
137 html.close(tag);
138 }
139
140 @Override
141 public void visit(ParaNode node) {
142 wrapChildren("p", node);
143 }
144
145 @Override
146 public void visit(BlockQuoteNode node) {
147 wrapChildren("blockquote", node);
148 }
149
150 @Override
151 public void visit(OrderedListNode node) {
152 wrapChildren("ol", node);
153 }
154
155 @Override
156 public void visit(BulletListNode node) {
157 wrapChildren("ul", node);
158 }
159
160 @Override
161 public void visit(ListItemNode node) {
162 wrapChildren("li", node);
163 }
164
165 @Override
166 public void visit(DefinitionListNode node) {
167 wrapChildren("dl", node);
168 }
169
170 @Override
171 public void visit(DefinitionNode node) {
172 wrapChildren("dd", node);
173 }
174
175 @Override
176 public void visit(DefinitionTermNode node) {
177 wrapChildren("dt", node);
178 }
179
180 @Override
181 public void visit(VerbatimNode node) {
182 html.open("pre").attribute("class", "code");
183 String text = node.getText();
184 while (text.startsWith("\n")) {
185 html.open("br");
186 text = text.substring(1);
187 }
188 html.appendAndEscape(text);
189 html.close("pre");
190 }
191
192 @Override
193 public void visit(CodeNode node) {
194 wrapText("code", node);
195 }
196
197 @Override
198 public void visit(StrikeNode node) {
199 wrapChildren("del", node);
200 }
201
202 @Override
203 public void visit(StrongEmphSuperNode node) {
204 if (node.isClosed()) {
205 wrapChildren(node.isStrong() ? "strong" : "em", node);
206 } else {
207 // Unclosed (or unmatched) sequence is plain text.
208 html.appendAndEscape(node.getChars());
209 visitChildren(node);
210 }
211 }
212
213 @Override
214 public void visit(AutoLinkNode node) {
215 String url = node.getText();
Shawn Pearce374f1842015-02-10 15:36:54 -0800216 html.open("a").attribute("href", href(url))
Shawn Pearcec9549982015-02-11 13:09:01 -0800217 .appendAndEscape(url)
218 .close("a");
219 }
220
221 @Override
222 public void visit(MailLinkNode node) {
223 String addr = node.getText();
224 html.open("a").attribute("href", "mailto:" + addr)
225 .appendAndEscape(addr)
226 .close("a");
227 }
228
229 @Override
230 public void visit(WikiLinkNode node) {
231 String text = node.getText();
232 String path = text.replace(' ', '-') + ".md";
Shawn Pearce374f1842015-02-10 15:36:54 -0800233 html.open("a").attribute("href", href(path))
Shawn Pearcec9549982015-02-11 13:09:01 -0800234 .appendAndEscape(text)
235 .close("a");
236 }
237
238 @Override
239 public void visit(ExpLinkNode node) {
240 html.open("a")
Shawn Pearce374f1842015-02-10 15:36:54 -0800241 .attribute("href", href(node.url))
Shawn Pearcec9549982015-02-11 13:09:01 -0800242 .attribute("title", node.title);
243 visitChildren(node);
244 html.close("a");
245 }
246
247 @Override
248 public void visit(RefLinkNode node) {
249 ReferenceNode ref = references.get(node.referenceKey, getInnerText(node));
250 if (ref != null) {
251 html.open("a")
Shawn Pearce374f1842015-02-10 15:36:54 -0800252 .attribute("href", href(ref.getUrl()))
Shawn Pearcec9549982015-02-11 13:09:01 -0800253 .attribute("title", ref.getTitle());
254 visitChildren(node);
255 html.close("a");
256 } else {
257 // Treat a broken RefLink as plain text.
258 visitChildren(node);
259 }
260 }
261
Shawn Pearce374f1842015-02-10 15:36:54 -0800262 private String href(String url) {
Shawn Pearce108599e2015-02-11 13:28:37 -0800263 if (MarkdownUtil.isAbsolutePathToMarkdown(url)) {
Shawn Pearce374f1842015-02-10 15:36:54 -0800264 return GitilesView.doc().copyFrom(view).setPathPart(url).build().toUrl();
265 }
266 return url;
267 }
268
Shawn Pearcec9549982015-02-11 13:09:01 -0800269 @Override
270 public void visit(ExpImageNode node) {
271 html.open("img")
272 .attribute("src", node.url)
273 .attribute("title", node.title)
274 .attribute("alt", getInnerText(node));
275 }
276
277 @Override
278 public void visit(RefImageNode node) {
279 String alt = getInnerText(node);
280 String url, title = alt;
281 ReferenceNode ref = references.get(node.referenceKey, alt);
282 if (ref != null) {
283 url = ref.getUrl();
284 title = ref.getTitle();
285 } else {
286 // If reference is missing, insert a broken image.
287 url = EscapingConventions.FilterImageDataUri.INSTANCE.getInnocuousOutput();
288 }
289 html.open("img")
290 .attribute("src", url)
291 .attribute("title", title)
292 .attribute("alt", alt);
293 }
294
295 @Override
296 public void visit(TableNode node) {
297 table = new TableState(node);
298 wrapChildren("table", node);
299 table = null;
300 }
301
302 private void mustBeInsideTable(Node node) {
303 checkState(table != null, "%s must be in table", node);
304 }
305
306 @Override
307 public void visit(TableHeaderNode node) {
308 mustBeInsideTable(node);
309 table.inHeader = true;
310 wrapChildren("thead", node);
311 table.inHeader = false;
312 }
313
314 @Override
315 public void visit(TableBodyNode node) {
316 wrapChildren("tbody", node);
317 }
318
319 @Override
320 public void visit(TableCaptionNode node) {
321 wrapChildren("caption", node);
322 }
323
324 @Override
325 public void visit(TableRowNode node) {
326 mustBeInsideTable(node);
327 table.startRow();
328 wrapChildren("tr", node);
329 }
330
331 @Override
332 public void visit(TableCellNode node) {
333 mustBeInsideTable(node);
334 String tag = table.inHeader ? "th" : "td";
335 html.open(tag)
336 .attribute("align", table.getAlign());
337 if (node.getColSpan() > 1) {
338 html.attribute("colspan", Integer.toString(node.getColSpan()));
339 }
340 visitChildren(node);
341 html.close(tag);
342 table.done(node);
343 }
344
345 @Override
346 public void visit(TableColumnNode node) {
347 // Not for output; should not be in the Visitor API.
348 }
349
350 @Override
351 public void visit(TextNode node) {
352 html.appendAndEscape(node.getText());
353 // TODO(sop) printWithAbbreviations
354 }
355
356 @Override
357 public void visit(SpecialTextNode node) {
358 html.appendAndEscape(node.getText());
359 }
360
361 @Override
362 public void visit(QuotedNode node) {
363 switch (node.getType()) {
364 case DoubleAngle:
365 html.entity("&laquo;");
366 visitChildren(node);
367 html.entity("&raquo;");
368 break;
369 case Double:
370 html.entity("&ldquo;");
371 visitChildren(node);
372 html.entity("&rdquo;");
373 break;
374 case Single:
375 html.entity("&lsquo;");
376 visitChildren(node);
377 html.entity("&rsquo;");
378 break;
379 default:
380 checkState(false, "unsupported quote %s", node.getType());
381 }
382 }
383
384 @Override
385 public void visit(SimpleNode node) {
386 switch (node.getType()) {
387 case Apostrophe:
388 html.entity("&rsquo;");
389 break;
390 case Ellipsis:
391 html.entity("&hellip;");
392 break;
393 case Emdash:
394 html.entity("&mdash;");
395 break;
396 case Endash:
397 html.entity("&ndash;");
398 break;
399 case HRule:
400 html.open("hr");
401 break;
402 case Linebreak:
403 html.open("br");
404 break;
405 case Nbsp:
406 html.entity("&nbsp;");
407 break;
408 default:
409 checkState(false, "unsupported node %s", node.getType());
410 }
411 }
412
413 @Override
414 public void visit(SuperNode node) {
415 visitChildren(node);
416 }
417
418 @Override
419 public void visit(Node node) {
420 checkState(false, "node %s unsupported", node.getClass());
421 }
422
423 @Override
424 public void visit(HtmlBlockNode node) {
425 // Drop all HTML nodes.
426 }
427
428 @Override
429 public void visit(InlineHtmlNode node) {
430 // Drop all HTML nodes.
431 }
432
433 @Override
434 public void visit(ReferenceNode node) {
435 // Reference nodes are not printed; they only declare an item.
436 }
437
438 @Override
439 public void visit(AbbreviationNode node) {
440 // Abbreviation nodes are not printed; they only declare an item.
441 }
442
443 private void wrapText(String tag, TextNode node) {
444 html.open(tag).appendAndEscape(node.getText()).close(tag);
445 }
446
447 private void wrapChildren(String tag, SuperNode node) {
448 html.open(tag);
449 visitChildren(node);
450 html.close(tag);
451 }
452
453 private void visitChildren(Node node) {
454 for (Node child : node.getChildren()) {
455 child.accept(this);
456 }
457 }
458}