blob: b88f4fd3734a47cf836d4fb871814d5650e4c40f [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
Shawn Pearce108599e2015-02-11 13:28:37 -080017import static com.google.gitiles.doc.MarkdownUtil.getInnerText;
Shawn Pearcec9549982015-02-11 13:09:01 -080018
Shawn Pearcec8fac642016-05-16 13:15:43 -060019import com.google.common.annotations.VisibleForTesting;
Shawn Pearcec10cc992015-02-09 00:22:33 -080020import com.google.common.base.Strings;
Shawn Pearce374f1842015-02-10 15:36:54 -080021import com.google.gitiles.GitilesView;
Shawn Pearcec10cc992015-02-09 00:22:33 -080022import com.google.gitiles.ThreadSafePrettifyParser;
Shawn Pearcec9549982015-02-11 13:09:01 -080023import com.google.gitiles.doc.html.HtmlBuilder;
Shawn Pearce5c34e092017-06-29 21:18:30 -070024import com.google.gitiles.doc.html.SoyHtmlBuilder;
Shawn Pearcec9549982015-02-11 13:09:01 -080025import com.google.template.soy.data.SanitizedContent;
Shawn Pearce47fd6562016-05-28 14:15:15 -070026import com.google.template.soy.shared.restricted.EscapingConventions.FilterImageDataUri;
Shawn Pearceb7e872d2015-07-10 15:21:47 -070027import com.google.template.soy.shared.restricted.EscapingConventions.FilterNormalizeUri;
Dave Borowitz3b744b12016-08-19 16:11:10 -040028import java.util.List;
29import javax.annotation.Nullable;
Shawn Pearce12c8fab2016-05-15 16:55:21 -070030import org.commonmark.ext.gfm.strikethrough.Strikethrough;
31import org.commonmark.ext.gfm.tables.TableBlock;
32import org.commonmark.ext.gfm.tables.TableBody;
33import org.commonmark.ext.gfm.tables.TableCell;
34import org.commonmark.ext.gfm.tables.TableHead;
35import org.commonmark.ext.gfm.tables.TableRow;
36import org.commonmark.node.Block;
37import org.commonmark.node.BlockQuote;
38import org.commonmark.node.BulletList;
39import org.commonmark.node.Code;
40import org.commonmark.node.CustomBlock;
41import org.commonmark.node.CustomNode;
42import org.commonmark.node.Document;
43import org.commonmark.node.Emphasis;
44import org.commonmark.node.FencedCodeBlock;
45import org.commonmark.node.HardLineBreak;
46import org.commonmark.node.Heading;
47import org.commonmark.node.HtmlBlock;
48import org.commonmark.node.HtmlInline;
49import org.commonmark.node.Image;
50import org.commonmark.node.IndentedCodeBlock;
51import org.commonmark.node.Link;
52import org.commonmark.node.ListBlock;
53import org.commonmark.node.ListItem;
54import org.commonmark.node.Node;
55import org.commonmark.node.OrderedList;
56import org.commonmark.node.Paragraph;
57import org.commonmark.node.SoftLineBreak;
58import org.commonmark.node.StrongEmphasis;
59import org.commonmark.node.Text;
60import org.commonmark.node.ThematicBreak;
61import org.commonmark.node.Visitor;
Shawn Pearce47fd6562016-05-28 14:15:15 -070062import org.eclipse.jgit.lib.ObjectReader;
63import org.eclipse.jgit.revwalk.RevTree;
Shawn Pearcec10cc992015-02-09 00:22:33 -080064import prettify.parser.Prettify;
65import syntaxhighlight.ParseResult;
66
Shawn Pearcec9549982015-02-11 13:09:01 -080067/**
Shawn Pearce12c8fab2016-05-15 16:55:21 -070068 * Formats parsed Markdown AST into HTML.
Dave Borowitz40255d52016-08-19 16:16:22 -040069 *
70 * <p>Callers must create a new instance for each document.
Shawn Pearcec9549982015-02-11 13:09:01 -080071 */
72public class MarkdownToHtml implements Visitor {
Shawn Pearce47fd6562016-05-28 14:15:15 -070073 public static Builder builder() {
74 return new Builder();
75 }
76
77 public static class Builder {
Shawn Pearcec68ad0b2016-05-28 16:52:47 -070078 private String requestUri;
Shawn Pearce47fd6562016-05-28 14:15:15 -070079 private GitilesView view;
80 private MarkdownConfig config;
81 private String filePath;
82 private ObjectReader reader;
83 private RevTree root;
84
85 Builder() {}
86
Shawn Pearcec68ad0b2016-05-28 16:52:47 -070087 public Builder setRequestUri(@Nullable String uri) {
88 requestUri = uri;
89 return this;
90 }
91
Shawn Pearce47fd6562016-05-28 14:15:15 -070092 public Builder setGitilesView(@Nullable GitilesView view) {
93 this.view = view;
94 return this;
95 }
96
97 public Builder setConfig(@Nullable MarkdownConfig config) {
98 this.config = config;
99 return this;
100 }
101
102 public Builder setFilePath(@Nullable String filePath) {
103 this.filePath = Strings.emptyToNull(filePath);
104 return this;
105 }
106
107 public Builder setReader(ObjectReader reader) {
108 this.reader = reader;
109 return this;
110 }
111
112 public Builder setRootTree(RevTree tree) {
113 this.root = tree;
114 return this;
115 }
116
117 public MarkdownToHtml build() {
118 return new MarkdownToHtml(this);
119 }
120 }
121
Shawn Pearce5c34e092017-06-29 21:18:30 -0700122 private HtmlBuilder html;
123 private TocFormatter toc;
Shawn Pearcec68ad0b2016-05-28 16:52:47 -0700124 private final String requestUri;
Shawn Pearce374f1842015-02-10 15:36:54 -0800125 private final GitilesView view;
Shawn Pearce47fd6562016-05-28 14:15:15 -0700126 private final MarkdownConfig config;
Shawn Pearcec32894e2016-05-17 15:25:20 -0400127 private final String filePath;
Shawn Pearce47fd6562016-05-28 14:15:15 -0700128 private final ImageLoader imageLoader;
Shawn Pearce25d91962015-06-22 15:35:36 -0700129 private boolean outputNamedAnchor = true;
Shawn Pearcec9549982015-02-11 13:09:01 -0800130
Shawn Pearce47fd6562016-05-28 14:15:15 -0700131 private MarkdownToHtml(Builder b) {
Shawn Pearcec68ad0b2016-05-28 16:52:47 -0700132 requestUri = b.requestUri;
Shawn Pearce47fd6562016-05-28 14:15:15 -0700133 view = b.view;
134 config = b.config;
135 filePath = b.filePath;
136 imageLoader = newImageLoader(b);
Shawn Pearce374f1842015-02-10 15:36:54 -0800137 }
138
Shawn Pearce47fd6562016-05-28 14:15:15 -0700139 private static ImageLoader newImageLoader(Builder b) {
140 if (b.reader != null && b.view != null && b.config != null && b.root != null) {
141 return new ImageLoader(b.reader, b.view, b.config, b.root);
142 }
143 return null;
Shawn Pearcef75a2c62015-02-12 21:39:00 -0800144 }
145
Shawn Pearcec9549982015-02-11 13:09:01 -0800146 /** Render the document AST to sanitized HTML. */
Shawn Pearce5c34e092017-06-29 21:18:30 -0700147 public void renderToHtml(HtmlBuilder out, Node node) {
148 if (node != null) {
149 html = out;
150 toc = new TocFormatter(html, 3);
151 toc.setRoot(node);
152 node.accept(this);
153 html.finish();
154 html = null;
155 toc = null;
Shawn Pearcec9549982015-02-11 13:09:01 -0800156 }
Shawn Pearce5c34e092017-06-29 21:18:30 -0700157 }
Shawn Pearcec9549982015-02-11 13:09:01 -0800158
Shawn Pearce5c34e092017-06-29 21:18:30 -0700159 /** Render the document AST to sanitized HTML. */
160 public SanitizedContent toSoyHtml(Node node) {
161 if (node != null) {
162 SoyHtmlBuilder out = new SoyHtmlBuilder();
163 renderToHtml(out, node);
164 return out.toSoy();
165 }
166 return null;
Shawn Pearcec9549982015-02-11 13:09:01 -0800167 }
168
169 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700170 public void visit(Document node) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800171 visitChildren(node);
172 }
173
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700174 private void visit(BlockNote node) {
175 html.open("div").attribute("class", node.getClassName());
176 Node f = node.getFirstChild();
177 if (f == node.getLastChild() && f instanceof Paragraph) {
178 // Avoid <p> inside <div> if there is only one <p>.
179 visitChildren(f);
180 } else {
181 visitChildren(node);
182 }
Shawn Pearce96f6d5f2015-02-06 22:45:58 -0800183 html.close("div");
184 }
185
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700186 private void visit(MultiColumnBlock node) {
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -0800187 html.open("div").attribute("class", "cols");
Shawn Pearceecddc612015-02-20 22:09:47 -0800188 visitChildren(node);
189 html.close("div");
190 }
191
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700192 private void visit(MultiColumnBlock.Column node) {
193 if (1 <= node.span && node.span <= MultiColumnBlock.GRID_WIDTH) {
Shawn Pearceecddc612015-02-20 22:09:47 -0800194 html.open("div").attribute("class", "col-" + node.span);
195 visitChildren(node);
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -0800196 html.close("div");
197 }
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -0800198 }
199
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700200 private void visit(IframeBlock node) {
Shawn Pearceee0b06e2015-02-13 00:13:01 -0800201 if (HtmlBuilder.isValidHttpUri(node.src)
202 && HtmlBuilder.isValidCssDimension(node.height)
203 && HtmlBuilder.isValidCssDimension(node.width)
Shawn Pearce47fd6562016-05-28 14:15:15 -0700204 && config != null
205 && config.isIFrameAllowed(node.src)) {
Shawn Pearceee0b06e2015-02-13 00:13:01 -0800206 html.open("iframe")
207 .attribute("src", node.src)
208 .attribute("height", node.height)
209 .attribute("width", node.width);
210 if (!node.border) {
211 html.attribute("class", "noborder");
212 }
213 html.close("iframe");
214 }
215 }
216
Shawn Pearceee0b06e2015-02-13 00:13:01 -0800217 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700218 public void visit(Heading node) {
Shawn Pearce263d6742015-06-23 17:36:07 -0700219 outputNamedAnchor = false;
220 String tag = "h" + node.getLevel();
221 html.open(tag);
Shawn Pearced57ade02015-06-22 12:34:38 -0700222 String id = toc.idFromHeader(node);
223 if (id != null) {
Shawn Pearce263d6742015-06-23 17:36:07 -0700224 html.open("a")
225 .attribute("class", "h")
226 .attribute("name", id)
227 .attribute("href", "#" + id)
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200228 .open("span")
229 .close("span")
Shawn Pearce263d6742015-06-23 17:36:07 -0700230 .close("a");
Darragh Baileybb828f22017-05-18 13:03:09 +0100231 // github markdown compatibility
Darragh Baileyd8d82fc2017-07-07 10:13:00 +0100232 if (id != id.toLowerCase()) {
233 html.open("a")
234 .attribute("class", "h")
235 .attribute("name", id.toLowerCase())
236 .attribute("href", "#" + id.toLowerCase())
237 .open("span")
238 .close("span")
239 .close("a");
240 }
Shawn Pearced57ade02015-06-22 12:34:38 -0700241 }
Shawn Pearce263d6742015-06-23 17:36:07 -0700242 visitChildren(node);
243 html.close(tag);
244 outputNamedAnchor = true;
Shawn Pearce25d91962015-06-22 15:35:36 -0700245 }
246
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700247 private void visit(NamedAnchor node) {
Shawn Pearce25d91962015-06-22 15:35:36 -0700248 if (outputNamedAnchor) {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700249 html.open("a").attribute("name", node.getName()).close("a");
Shawn Pearce25d91962015-06-22 15:35:36 -0700250 }
Shawn Pearcec9549982015-02-11 13:09:01 -0800251 }
252
253 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700254 public void visit(Paragraph node) {
255 if (isInTightList(node)) {
256 // Avoid unnecessary <p> tags within <ol><li> structures.
257 visitChildren(node);
258 } else {
259 wrapChildren("p", node);
260 }
261 }
262
263 private static boolean isInTightList(Paragraph c) {
264 Block b = c.getParent(); // b is probably a ListItem
265 if (b != null) {
266 Block a = b.getParent();
267 return a instanceof ListBlock && ((ListBlock) a).isTight();
268 }
269 return false;
Shawn Pearcec9549982015-02-11 13:09:01 -0800270 }
271
272 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700273 public void visit(BlockQuote node) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800274 wrapChildren("blockquote", node);
275 }
276
277 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700278 public void visit(OrderedList node) {
279 html.open("ol");
280 if (node.getStartNumber() != 1) {
281 html.attribute("start", Integer.toString(node.getStartNumber()));
282 }
283 visitChildren(node);
284 html.close("ol");
Shawn Pearcec9549982015-02-11 13:09:01 -0800285 }
286
287 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700288 public void visit(BulletList node) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800289 wrapChildren("ul", node);
290 }
291
292 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700293 public void visit(ListItem node) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800294 wrapChildren("li", node);
295 }
296
297 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700298 public void visit(FencedCodeBlock node) {
299 codeInPre(node.getInfo(), node.getLiteral());
Shawn Pearcec9549982015-02-11 13:09:01 -0800300 }
301
302 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700303 public void visit(IndentedCodeBlock node) {
304 codeInPre(null, node.getLiteral());
Shawn Pearcec9549982015-02-11 13:09:01 -0800305 }
306
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700307 private void codeInPre(String lang, String text) {
Shawn Pearcec10cc992015-02-09 00:22:33 -0800308 html.open("pre").attribute("class", "code");
309 text = printLeadingBlankLines(text);
310 List<ParseResult> parsed = parse(lang, text);
311 if (parsed != null) {
312 int last = 0;
313 for (ParseResult r : parsed) {
314 span(null, text, last, r.getOffset());
315 last = r.getOffset() + r.getLength();
316 span(r.getStyleKeysString(), text, r.getOffset(), last);
317 }
318 if (last < text.length()) {
319 span(null, text, last, text.length());
320 }
321 } else {
322 html.appendAndEscape(text);
Shawn Pearcec9549982015-02-11 13:09:01 -0800323 }
Shawn Pearcec9549982015-02-11 13:09:01 -0800324 html.close("pre");
325 }
326
Shawn Pearcec10cc992015-02-09 00:22:33 -0800327 private String printLeadingBlankLines(String text) {
328 int i = 0;
329 while (i < text.length() && text.charAt(i) == '\n') {
330 html.open("br");
331 i++;
332 }
333 return text.substring(i);
334 }
335
336 private void span(String classes, String s, int start, int end) {
337 if (end - start > 0) {
338 if (Strings.isNullOrEmpty(classes)) {
339 classes = Prettify.PR_PLAIN;
340 }
341 html.open("span").attribute("class", classes);
342 html.appendAndEscape(s.substring(start, end));
343 html.close("span");
344 }
345 }
346
347 private List<ParseResult> parse(String lang, String text) {
348 if (Strings.isNullOrEmpty(lang)) {
349 return null;
350 }
351 try {
352 return ThreadSafePrettifyParser.INSTANCE.parse(lang, text);
353 } catch (StackOverflowError e) {
354 return null;
355 }
356 }
357
Shawn Pearcec9549982015-02-11 13:09:01 -0800358 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700359 public void visit(Code node) {
Mark Mentovaic4cfe002016-11-07 20:38:58 -0500360 html.open("code").attribute("class", "code").appendAndEscape(node.getLiteral()).close("code");
Shawn Pearcec9549982015-02-11 13:09:01 -0800361 }
362
363 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700364 public void visit(Emphasis node) {
365 wrapChildren("em", node);
Shawn Pearcec9549982015-02-11 13:09:01 -0800366 }
367
368 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700369 public void visit(StrongEmphasis node) {
370 wrapChildren("strong", node);
Shawn Pearcec9549982015-02-11 13:09:01 -0800371 }
372
373 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700374 public void visit(Link node) {
375 html.open("a")
376 .attribute("href", href(node.getDestination()))
377 .attribute("title", node.getTitle());
Shawn Pearcec9549982015-02-11 13:09:01 -0800378 visitChildren(node);
379 html.close("a");
380 }
381
Shawn Pearcec8fac642016-05-16 13:15:43 -0600382 @VisibleForTesting
383 String href(String target) {
Shawn Pearce6b5c7d52016-05-20 16:37:00 -0600384 if (target.startsWith("#") || HtmlBuilder.isValidHttpUri(target)) {
Shawn Pearcec8fac642016-05-16 13:15:43 -0600385 return target;
Shawn Pearce532b62f2016-06-05 12:20:38 -0700386 } else if (target.startsWith("git:")) {
387 if (HtmlBuilder.isValidGitUri(target)) {
388 return target;
389 }
390 return FilterNormalizeUri.INSTANCE.getInnocuousOutput();
Shawn Pearce978fe8e2015-03-25 16:49:41 -0700391 }
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700392
Shawn Pearce6b5c7d52016-05-20 16:37:00 -0600393 String anchor = "";
394 int hash = target.indexOf('#');
395 if (hash >= 0) {
396 anchor = target.substring(hash);
397 target = target.substring(0, hash);
398 }
399
Shawn Pearce47fd6562016-05-28 14:15:15 -0700400 String dest = PathResolver.resolve(filePath, target);
401 if (dest == null || view == null) {
402 return FilterNormalizeUri.INSTANCE.getInnocuousOutput();
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700403 }
Shawn Pearcec8fac642016-05-16 13:15:43 -0600404
Shawn Pearcec32894e2016-05-17 15:25:20 -0400405 GitilesView.Builder b;
406 if (view.getType() == GitilesView.Type.ROOTED_DOC) {
407 b = GitilesView.rootedDoc();
408 } else {
409 b = GitilesView.path();
410 }
Shawn Pearcec68ad0b2016-05-28 16:52:47 -0700411 dest = b.copyFrom(view).setPathPart(dest).build().toUrl();
412
413 return PathResolver.relative(requestUri, dest) + anchor;
Shawn Pearce374f1842015-02-10 15:36:54 -0800414 }
415
Shawn Pearcec9549982015-02-11 13:09:01 -0800416 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700417 public void visit(Image node) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800418 html.open("img")
Shawn Pearce47fd6562016-05-28 14:15:15 -0700419 .attribute("src", image(node.getDestination()))
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700420 .attribute("title", node.getTitle())
Shawn Pearcec9549982015-02-11 13:09:01 -0800421 .attribute("alt", getInnerText(node));
422 }
423
Shawn Pearce47fd6562016-05-28 14:15:15 -0700424 String image(String dest) {
425 if (HtmlBuilder.isValidHttpUri(dest) || HtmlBuilder.isImageDataUri(dest)) {
426 return dest;
427 } else if (imageLoader != null) {
428 return imageLoader.inline(filePath, dest);
Shawn Pearcef75a2c62015-02-12 21:39:00 -0800429 }
Shawn Pearce47fd6562016-05-28 14:15:15 -0700430 return FilterImageDataUri.INSTANCE.getInnocuousOutput();
Shawn Pearcef75a2c62015-02-12 21:39:00 -0800431 }
432
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700433 public void visit(TableBlock node) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800434 wrapChildren("table", node);
Shawn Pearcec9549982015-02-11 13:09:01 -0800435 }
436
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700437 private void visit(TableRow node) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800438 wrapChildren("tr", node);
439 }
440
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700441 private void visit(TableCell cell) {
442 String tag = cell.isHeader() ? "th" : "td";
443 html.open(tag);
444 TableCell.Alignment alignment = cell.getAlignment();
445 if (alignment != null) {
446 html.attribute("align", toHtml(alignment));
Shawn Pearcec9549982015-02-11 13:09:01 -0800447 }
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700448 visitChildren(cell);
Shawn Pearcec9549982015-02-11 13:09:01 -0800449 html.close(tag);
Shawn Pearcec9549982015-02-11 13:09:01 -0800450 }
451
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700452 private static String toHtml(TableCell.Alignment alignment) {
453 switch (alignment) {
454 case LEFT:
455 return "left";
456 case CENTER:
457 return "center";
458 case RIGHT:
459 return "right";
460 default:
461 throw new IllegalArgumentException("unsupported alignment " + alignment);
462 }
Shawn Pearcec9549982015-02-11 13:09:01 -0800463 }
464
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700465 private void visit(SmartQuoted node) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800466 switch (node.getType()) {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700467 case DOUBLE:
Shawn Pearcec9549982015-02-11 13:09:01 -0800468 html.entity("&ldquo;");
469 visitChildren(node);
470 html.entity("&rdquo;");
471 break;
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700472 case SINGLE:
Shawn Pearcec9549982015-02-11 13:09:01 -0800473 html.entity("&lsquo;");
474 visitChildren(node);
475 html.entity("&rsquo;");
476 break;
477 default:
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700478 throw new IllegalArgumentException("unsupported quote " + node.getType());
479 }
480 }
481
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700482 @Override
483 public void visit(Text node) {
Shawn Pearceb2ed49d2016-06-05 10:31:11 -0700484 html.appendAndEscape(node.getLiteral());
Shawn Pearcec9549982015-02-11 13:09:01 -0800485 }
486
487 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700488 public void visit(SoftLineBreak node) {
489 html.space();
Shawn Pearcec9549982015-02-11 13:09:01 -0800490 }
491
492 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700493 public void visit(HardLineBreak node) {
494 html.open("br");
Shawn Pearcec9549982015-02-11 13:09:01 -0800495 }
496
497 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700498 public void visit(ThematicBreak thematicBreak) {
499 html.open("hr");
Shawn Pearcec9549982015-02-11 13:09:01 -0800500 }
501
502 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700503 public void visit(HtmlInline node) {
504 // Discard all HTML.
Shawn Pearcec9549982015-02-11 13:09:01 -0800505 }
506
507 @Override
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700508 public void visit(HtmlBlock node) {
509 // Discard all HTML.
Shawn Pearcec9549982015-02-11 13:09:01 -0800510 }
511
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700512 private void wrapChildren(String tag, Node node) {
Shawn Pearcec9549982015-02-11 13:09:01 -0800513 html.open(tag);
514 visitChildren(node);
515 html.close(tag);
516 }
517
518 private void visitChildren(Node node) {
Shawn Pearce12c8fab2016-05-15 16:55:21 -0700519 for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
520 c.accept(this);
521 }
522 }
523
524 @Override
525 public void visit(CustomNode node) {
526 if (node instanceof NamedAnchor) {
527 visit((NamedAnchor) node);
528 } else if (node instanceof SmartQuoted) {
529 visit((SmartQuoted) node);
530 } else if (node instanceof Strikethrough) {
531 wrapChildren("del", node);
532 } else if (node instanceof TableBody) {
533 wrapChildren("tbody", node);
534 } else if (node instanceof TableCell) {
535 visit((TableCell) node);
536 } else if (node instanceof TableHead) {
537 wrapChildren("thead", node);
538 } else if (node instanceof TableRow) {
539 visit((TableRow) node);
540 } else {
541 throw new IllegalArgumentException("cannot render " + node.getClass());
542 }
543 }
544
545 @Override
546 public void visit(CustomBlock node) {
547 if (node instanceof BlockNote) {
548 visit((BlockNote) node);
549 } else if (node instanceof IframeBlock) {
550 visit((IframeBlock) node);
551 } else if (node instanceof MultiColumnBlock) {
552 visit((MultiColumnBlock) node);
553 } else if (node instanceof MultiColumnBlock.Column) {
554 visit((MultiColumnBlock.Column) node);
555 } else if (node instanceof TableBlock) {
556 visit((TableBlock) node);
557 } else if (node instanceof TocBlock) {
558 toc.format();
559 } else {
560 throw new IllegalArgumentException("cannot render " + node.getClass());
Shawn Pearcec9549982015-02-11 13:09:01 -0800561 }
562 }
563}