blob: 7b30e2769c008d11151f81895148d852e716ff95 [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 Pearcec8fac642016-05-16 13:15:43 -060020import com.google.common.annotations.VisibleForTesting;
Shawn Pearcec10cc992015-02-09 00:22:33 -080021import com.google.common.base.Strings;
Shawn Pearce374f1842015-02-10 15:36:54 -080022import com.google.gitiles.GitilesView;
Shawn Pearcec10cc992015-02-09 00:22:33 -080023import com.google.gitiles.ThreadSafePrettifyParser;
Shawn Pearcec9549982015-02-11 13:09:01 -080024import com.google.gitiles.doc.html.HtmlBuilder;
25import com.google.template.soy.data.SanitizedContent;
Shawn Pearcef75a2c62015-02-12 21:39:00 -080026import com.google.template.soy.shared.restricted.EscapingConventions.FilterImageDataUri;
Shawn Pearceb7e872d2015-07-10 15:21:47 -070027import com.google.template.soy.shared.restricted.EscapingConventions.FilterNormalizeUri;
Shawn Pearcec9549982015-02-11 13:09:01 -080028
Shawn Pearceee0b06e2015-02-13 00:13:01 -080029import org.eclipse.jgit.lib.Config;
30import org.eclipse.jgit.util.StringUtils;
Shawn Pearcec9549982015-02-11 13:09:01 -080031import org.pegdown.ast.AbbreviationNode;
32import org.pegdown.ast.AutoLinkNode;
33import org.pegdown.ast.BlockQuoteNode;
34import org.pegdown.ast.BulletListNode;
35import org.pegdown.ast.CodeNode;
36import org.pegdown.ast.DefinitionListNode;
37import org.pegdown.ast.DefinitionNode;
38import org.pegdown.ast.DefinitionTermNode;
39import org.pegdown.ast.ExpImageNode;
40import org.pegdown.ast.ExpLinkNode;
41import org.pegdown.ast.HeaderNode;
42import org.pegdown.ast.HtmlBlockNode;
43import org.pegdown.ast.InlineHtmlNode;
44import org.pegdown.ast.ListItemNode;
45import org.pegdown.ast.MailLinkNode;
46import org.pegdown.ast.Node;
47import org.pegdown.ast.OrderedListNode;
48import org.pegdown.ast.ParaNode;
49import org.pegdown.ast.QuotedNode;
50import org.pegdown.ast.RefImageNode;
51import org.pegdown.ast.RefLinkNode;
52import org.pegdown.ast.ReferenceNode;
53import org.pegdown.ast.RootNode;
54import org.pegdown.ast.SimpleNode;
55import org.pegdown.ast.SpecialTextNode;
56import org.pegdown.ast.StrikeNode;
57import org.pegdown.ast.StrongEmphSuperNode;
58import org.pegdown.ast.SuperNode;
59import org.pegdown.ast.TableBodyNode;
60import org.pegdown.ast.TableCaptionNode;
61import org.pegdown.ast.TableCellNode;
62import org.pegdown.ast.TableColumnNode;
63import org.pegdown.ast.TableHeaderNode;
64import org.pegdown.ast.TableNode;
65import org.pegdown.ast.TableRowNode;
66import org.pegdown.ast.TextNode;
67import org.pegdown.ast.VerbatimNode;
68import org.pegdown.ast.WikiLinkNode;
69
Shawn Pearcec10cc992015-02-09 00:22:33 -080070import java.util.List;
71
72import prettify.parser.Prettify;
73import syntaxhighlight.ParseResult;
74
Shawn Pearcec9549982015-02-11 13:09:01 -080075/**
76 * Formats parsed markdown AST into HTML.
77 * <p>
78 * Callers must create a new instance for each RootNode.
79 */
80public class MarkdownToHtml implements Visitor {
81 private final ReferenceMap references = new ReferenceMap();
82 private final HtmlBuilder html = new HtmlBuilder();
83 private final TocFormatter toc = new TocFormatter(html, 3);
Shawn Pearce374f1842015-02-10 15:36:54 -080084 private final GitilesView view;
Shawn Pearceee0b06e2015-02-13 00:13:01 -080085 private final Config cfg;
Shawn Pearcec32894e2016-05-17 15:25:20 -040086 private final String filePath;
Shawn Pearcef75a2c62015-02-12 21:39:00 -080087 private ImageLoader imageLoader;
Shawn Pearcec9549982015-02-11 13:09:01 -080088 private TableState table;
Shawn Pearce25d91962015-06-22 15:35:36 -070089 private boolean outputNamedAnchor = true;
Shawn Pearcec9549982015-02-11 13:09:01 -080090
Shawn Pearcec32894e2016-05-17 15:25:20 -040091 /**
92 * Initialize a Markdown to HTML converter.
93 *
94 * @param view view used to access this Markdown on the web. Some elements of
95 * the view may be used to generate hyperlinks to other files, e.g.
96 * repository name and revision.
97 * @param cfg
98 * @param filePath actual path of the Markdown file in the Git repository. This must
99 * always be a file, e.g. {@code doc/README.md}. The path is used to
100 * resolve relative links within the repository.
101 */
102 public MarkdownToHtml(GitilesView view, Config cfg, String filePath) {
Shawn Pearce374f1842015-02-10 15:36:54 -0800103 this.view = view;
Shawn Pearceee0b06e2015-02-13 00:13:01 -0800104 this.cfg = cfg;
Shawn Pearcec32894e2016-05-17 15:25:20 -0400105 this.filePath = filePath;
Shawn Pearce374f1842015-02-10 15:36:54 -0800106 }
107
Shawn Pearcef75a2c62015-02-12 21:39:00 -0800108 public MarkdownToHtml setImageLoader(ImageLoader img) {
109 imageLoader = img;
110 return this;
111 }
112
Shawn Pearcec9549982015-02-11 13:09:01 -0800113 /** Render the document AST to sanitized HTML. */
114 public SanitizedContent toSoyHtml(RootNode node) {
115 if (node == null) {
116 return null;
117 }
118
119 toc.setRoot(node);
120 node.accept(this);
121 return html.toSoy();
122 }
123
124 @Override
125 public void visit(RootNode node) {
126 references.add(node);
127 visitChildren(node);
128 }
129
130 @Override
131 public void visit(TocNode node) {
132 toc.format();
133 }
134
135 @Override
Shawn Pearce96f6d5f2015-02-06 22:45:58 -0800136 public void visit(DivNode node) {
137 html.open("div").attribute("class", node.getStyleName());
138 visitChildren(node);
139 html.close("div");
140 }
141
142 @Override
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -0800143 public void visit(ColsNode node) {
144 html.open("div").attribute("class", "cols");
Shawn Pearceecddc612015-02-20 22:09:47 -0800145 visitChildren(node);
146 html.close("div");
147 }
148
149 @Override
150 public void visit(ColsNode.Column node) {
151 if (1 <= node.span && node.span <= ColsNode.GRID_WIDTH) {
152 html.open("div").attribute("class", "col-" + node.span);
153 visitChildren(node);
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -0800154 html.close("div");
155 }
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -0800156 }
157
158 @Override
Shawn Pearceee0b06e2015-02-13 00:13:01 -0800159 public void visit(IframeNode node) {
160 if (HtmlBuilder.isValidHttpUri(node.src)
161 && HtmlBuilder.isValidCssDimension(node.height)
162 && HtmlBuilder.isValidCssDimension(node.width)
163 && canRender(node)) {
164 html.open("iframe")
165 .attribute("src", node.src)
166 .attribute("height", node.height)
167 .attribute("width", node.width);
168 if (!node.border) {
169 html.attribute("class", "noborder");
170 }
171 html.close("iframe");
172 }
173 }
174
175 private boolean canRender(IframeNode node) {
176 String[] ok = cfg.getStringList("markdown", null, "allowiframe");
177 if (ok.length == 1 && StringUtils.toBooleanOrNull(ok[0]) == Boolean.TRUE) {
178 return true;
179 }
180 for (String m : ok) {
181 if (m.equals(node.src) || (m.endsWith("/") && node.src.startsWith(m))) {
182 return true;
183 }
184 }
185 return false; // By default do not render iframe.
186 }
187
188 @Override
Shawn Pearcec9549982015-02-11 13:09:01 -0800189 public void visit(HeaderNode node) {
Shawn Pearce263d6742015-06-23 17:36:07 -0700190 outputNamedAnchor = false;
191 String tag = "h" + node.getLevel();
192 html.open(tag);
Shawn Pearced57ade02015-06-22 12:34:38 -0700193 String id = toc.idFromHeader(node);
194 if (id != null) {
Shawn Pearce263d6742015-06-23 17:36:07 -0700195 html.open("a")
196 .attribute("class", "h")
197 .attribute("name", id)
198 .attribute("href", "#" + id)
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200199 .open("span")
200 .close("span")
Shawn Pearce263d6742015-06-23 17:36:07 -0700201 .close("a");
Shawn Pearced57ade02015-06-22 12:34:38 -0700202 }
Shawn Pearce263d6742015-06-23 17:36:07 -0700203 visitChildren(node);
204 html.close(tag);
205 outputNamedAnchor = true;
Shawn Pearce25d91962015-06-22 15:35:36 -0700206 }
207
208 @Override
209 public void visit(NamedAnchorNode node) {
210 if (outputNamedAnchor) {
211 html.open("a").attribute("name", node.name).close("a");
212 }
Shawn Pearcec9549982015-02-11 13:09:01 -0800213 }
214
215 @Override
216 public void visit(ParaNode node) {
217 wrapChildren("p", node);
218 }
219
220 @Override
221 public void visit(BlockQuoteNode node) {
222 wrapChildren("blockquote", node);
223 }
224
225 @Override
226 public void visit(OrderedListNode node) {
227 wrapChildren("ol", node);
228 }
229
230 @Override
231 public void visit(BulletListNode node) {
232 wrapChildren("ul", node);
233 }
234
235 @Override
236 public void visit(ListItemNode node) {
237 wrapChildren("li", node);
238 }
239
240 @Override
241 public void visit(DefinitionListNode node) {
242 wrapChildren("dl", node);
243 }
244
245 @Override
246 public void visit(DefinitionNode node) {
247 wrapChildren("dd", node);
248 }
249
250 @Override
251 public void visit(DefinitionTermNode node) {
252 wrapChildren("dt", node);
253 }
254
255 @Override
256 public void visit(VerbatimNode node) {
Shawn Pearcec10cc992015-02-09 00:22:33 -0800257 String lang = node.getType();
Shawn Pearcec9549982015-02-11 13:09:01 -0800258 String text = node.getText();
Shawn Pearcec10cc992015-02-09 00:22:33 -0800259
260 html.open("pre").attribute("class", "code");
261 text = printLeadingBlankLines(text);
262 List<ParseResult> parsed = parse(lang, text);
263 if (parsed != null) {
264 int last = 0;
265 for (ParseResult r : parsed) {
266 span(null, text, last, r.getOffset());
267 last = r.getOffset() + r.getLength();
268 span(r.getStyleKeysString(), text, r.getOffset(), last);
269 }
270 if (last < text.length()) {
271 span(null, text, last, text.length());
272 }
273 } else {
274 html.appendAndEscape(text);
Shawn Pearcec9549982015-02-11 13:09:01 -0800275 }
Shawn Pearcec9549982015-02-11 13:09:01 -0800276 html.close("pre");
277 }
278
Shawn Pearcec10cc992015-02-09 00:22:33 -0800279 private String printLeadingBlankLines(String text) {
280 int i = 0;
281 while (i < text.length() && text.charAt(i) == '\n') {
282 html.open("br");
283 i++;
284 }
285 return text.substring(i);
286 }
287
288 private void span(String classes, String s, int start, int end) {
289 if (end - start > 0) {
290 if (Strings.isNullOrEmpty(classes)) {
291 classes = Prettify.PR_PLAIN;
292 }
293 html.open("span").attribute("class", classes);
294 html.appendAndEscape(s.substring(start, end));
295 html.close("span");
296 }
297 }
298
299 private List<ParseResult> parse(String lang, String text) {
300 if (Strings.isNullOrEmpty(lang)) {
301 return null;
302 }
303 try {
304 return ThreadSafePrettifyParser.INSTANCE.parse(lang, text);
305 } catch (StackOverflowError e) {
306 return null;
307 }
308 }
309
Shawn Pearcec9549982015-02-11 13:09:01 -0800310 @Override
311 public void visit(CodeNode node) {
312 wrapText("code", node);
313 }
314
315 @Override
316 public void visit(StrikeNode node) {
317 wrapChildren("del", node);
318 }
319
320 @Override
321 public void visit(StrongEmphSuperNode node) {
322 if (node.isClosed()) {
323 wrapChildren(node.isStrong() ? "strong" : "em", node);
324 } else {
325 // Unclosed (or unmatched) sequence is plain text.
326 html.appendAndEscape(node.getChars());
327 visitChildren(node);
328 }
329 }
330
331 @Override
332 public void visit(AutoLinkNode node) {
333 String url = node.getText();
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200334 html.open("a").attribute("href", href(url)).appendAndEscape(url).close("a");
Shawn Pearcec9549982015-02-11 13:09:01 -0800335 }
336
337 @Override
338 public void visit(MailLinkNode node) {
339 String addr = node.getText();
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200340 html.open("a").attribute("href", "mailto:" + addr).appendAndEscape(addr).close("a");
Shawn Pearcec9549982015-02-11 13:09:01 -0800341 }
342
343 @Override
344 public void visit(WikiLinkNode node) {
345 String text = node.getText();
346 String path = text.replace(' ', '-') + ".md";
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200347 html.open("a").attribute("href", href(path)).appendAndEscape(text).close("a");
Shawn Pearcec9549982015-02-11 13:09:01 -0800348 }
349
350 @Override
351 public void visit(ExpLinkNode node) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200352 html.open("a").attribute("href", href(node.url)).attribute("title", node.title);
Shawn Pearcec9549982015-02-11 13:09:01 -0800353 visitChildren(node);
354 html.close("a");
355 }
356
357 @Override
358 public void visit(RefLinkNode node) {
359 ReferenceNode ref = references.get(node.referenceKey, getInnerText(node));
360 if (ref != null) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200361 html.open("a").attribute("href", href(ref.getUrl())).attribute("title", ref.getTitle());
Shawn Pearcec9549982015-02-11 13:09:01 -0800362 visitChildren(node);
363 html.close("a");
364 } else {
365 // Treat a broken RefLink as plain text.
Shawn Pearce3d416592015-03-25 16:10:16 -0700366 html.appendAndEscape("[");
Shawn Pearcec9549982015-02-11 13:09:01 -0800367 visitChildren(node);
Shawn Pearce3d416592015-03-25 16:10:16 -0700368 html.appendAndEscape("]");
Shawn Pearcec9549982015-02-11 13:09:01 -0800369 }
370 }
371
Shawn Pearcec8fac642016-05-16 13:15:43 -0600372 @VisibleForTesting
373 String href(String target) {
Shawn Pearce6b5c7d52016-05-20 16:37:00 -0600374 if (target.startsWith("#") || HtmlBuilder.isValidHttpUri(target)) {
Shawn Pearcec8fac642016-05-16 13:15:43 -0600375 return target;
Shawn Pearce978fe8e2015-03-25 16:49:41 -0700376 }
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700377
Shawn Pearce6b5c7d52016-05-20 16:37:00 -0600378 String anchor = "";
379 int hash = target.indexOf('#');
380 if (hash >= 0) {
381 anchor = target.substring(hash);
382 target = target.substring(0, hash);
383 }
384
Shawn Pearcec8fac642016-05-16 13:15:43 -0600385 if (target.startsWith("/")) {
Shawn Pearce6b5c7d52016-05-20 16:37:00 -0600386 return toPath(target) + anchor;
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700387 }
Shawn Pearcec8fac642016-05-16 13:15:43 -0600388
Shawn Pearcec32894e2016-05-17 15:25:20 -0400389 String dir = trimLastComponent(filePath);
Shawn Pearcec8fac642016-05-16 13:15:43 -0600390 while (!target.isEmpty()) {
391 if (target.startsWith("../") || target.equals("..")) {
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700392 if (dir.isEmpty()) {
393 return FilterNormalizeUri.INSTANCE.getInnocuousOutput();
394 }
Shawn Pearcec8fac642016-05-16 13:15:43 -0600395 dir = trimLastComponent(dir);
396 target = target.equals("..") ? "" : target.substring(3);
397 } else if (target.startsWith("./")) {
398 target = target.substring(2);
399 } else if (target.equals(".")) {
400 target = "";
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700401 } else {
402 break;
Shawn Pearce978fe8e2015-03-25 16:49:41 -0700403 }
Shawn Pearce374f1842015-02-10 15:36:54 -0800404 }
Shawn Pearce6b5c7d52016-05-20 16:37:00 -0600405
406 return toPath(dir + '/' + target) + anchor;
Shawn Pearcec8fac642016-05-16 13:15:43 -0600407 }
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700408
Shawn Pearcec8fac642016-05-16 13:15:43 -0600409 private static String trimLastComponent(String path) {
410 int slash = path.lastIndexOf('/');
411 return slash < 0 ? "" : path.substring(0, slash);
412 }
Shawn Pearceb7e872d2015-07-10 15:21:47 -0700413
Shawn Pearcec8fac642016-05-16 13:15:43 -0600414 private String toPath(String path) {
Shawn Pearcec32894e2016-05-17 15:25:20 -0400415 GitilesView.Builder b;
416 if (view.getType() == GitilesView.Type.ROOTED_DOC) {
417 b = GitilesView.rootedDoc();
418 } else {
419 b = GitilesView.path();
420 }
421 return b.copyFrom(view).setPathPart(path).build().toUrl();
Shawn Pearce374f1842015-02-10 15:36:54 -0800422 }
423
Shawn Pearcec9549982015-02-11 13:09:01 -0800424 @Override
425 public void visit(ExpImageNode node) {
426 html.open("img")
Shawn Pearcef75a2c62015-02-12 21:39:00 -0800427 .attribute("src", resolveImageUrl(node.url))
Shawn Pearcec9549982015-02-11 13:09:01 -0800428 .attribute("title", node.title)
429 .attribute("alt", getInnerText(node));
430 }
431
432 @Override
433 public void visit(RefImageNode node) {
434 String alt = getInnerText(node);
435 String url, title = alt;
436 ReferenceNode ref = references.get(node.referenceKey, alt);
437 if (ref != null) {
Shawn Pearcef75a2c62015-02-12 21:39:00 -0800438 url = resolveImageUrl(ref.getUrl());
Shawn Pearcec9549982015-02-11 13:09:01 -0800439 title = ref.getTitle();
440 } else {
441 // If reference is missing, insert a broken image.
Shawn Pearcef75a2c62015-02-12 21:39:00 -0800442 url = FilterImageDataUri.INSTANCE.getInnocuousOutput();
Shawn Pearcec9549982015-02-11 13:09:01 -0800443 }
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200444 html.open("img").attribute("src", url).attribute("title", title).attribute("alt", alt);
Shawn Pearcec9549982015-02-11 13:09:01 -0800445 }
446
Shawn Pearcef75a2c62015-02-12 21:39:00 -0800447 private String resolveImageUrl(String url) {
448 if (imageLoader == null
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200449 || url.startsWith("https://")
450 || url.startsWith("http://")
Shawn Pearcef75a2c62015-02-12 21:39:00 -0800451 || url.startsWith("data:")) {
452 return url;
453 }
454 return imageLoader.loadImage(url);
455 }
456
Shawn Pearcec9549982015-02-11 13:09:01 -0800457 @Override
458 public void visit(TableNode node) {
459 table = new TableState(node);
460 wrapChildren("table", node);
461 table = null;
462 }
463
464 private void mustBeInsideTable(Node node) {
465 checkState(table != null, "%s must be in table", node);
466 }
467
468 @Override
469 public void visit(TableHeaderNode node) {
470 mustBeInsideTable(node);
471 table.inHeader = true;
472 wrapChildren("thead", node);
473 table.inHeader = false;
474 }
475
476 @Override
477 public void visit(TableBodyNode node) {
478 wrapChildren("tbody", node);
479 }
480
481 @Override
482 public void visit(TableCaptionNode node) {
483 wrapChildren("caption", node);
484 }
485
486 @Override
487 public void visit(TableRowNode node) {
488 mustBeInsideTable(node);
489 table.startRow();
490 wrapChildren("tr", node);
491 }
492
493 @Override
494 public void visit(TableCellNode node) {
495 mustBeInsideTable(node);
496 String tag = table.inHeader ? "th" : "td";
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200497 html.open(tag).attribute("align", table.getAlign());
Shawn Pearcec9549982015-02-11 13:09:01 -0800498 if (node.getColSpan() > 1) {
499 html.attribute("colspan", Integer.toString(node.getColSpan()));
500 }
501 visitChildren(node);
502 html.close(tag);
503 table.done(node);
504 }
505
506 @Override
507 public void visit(TableColumnNode node) {
508 // Not for output; should not be in the Visitor API.
509 }
510
511 @Override
512 public void visit(TextNode node) {
513 html.appendAndEscape(node.getText());
514 // TODO(sop) printWithAbbreviations
515 }
516
517 @Override
518 public void visit(SpecialTextNode node) {
519 html.appendAndEscape(node.getText());
520 }
521
522 @Override
523 public void visit(QuotedNode node) {
524 switch (node.getType()) {
525 case DoubleAngle:
526 html.entity("&laquo;");
527 visitChildren(node);
528 html.entity("&raquo;");
529 break;
530 case Double:
531 html.entity("&ldquo;");
532 visitChildren(node);
533 html.entity("&rdquo;");
534 break;
535 case Single:
536 html.entity("&lsquo;");
537 visitChildren(node);
538 html.entity("&rsquo;");
539 break;
540 default:
541 checkState(false, "unsupported quote %s", node.getType());
542 }
543 }
544
545 @Override
546 public void visit(SimpleNode node) {
547 switch (node.getType()) {
548 case Apostrophe:
549 html.entity("&rsquo;");
550 break;
551 case Ellipsis:
552 html.entity("&hellip;");
553 break;
554 case Emdash:
555 html.entity("&mdash;");
556 break;
557 case Endash:
558 html.entity("&ndash;");
559 break;
560 case HRule:
561 html.open("hr");
562 break;
563 case Linebreak:
564 html.open("br");
565 break;
566 case Nbsp:
567 html.entity("&nbsp;");
568 break;
569 default:
570 checkState(false, "unsupported node %s", node.getType());
571 }
572 }
573
574 @Override
575 public void visit(SuperNode node) {
576 visitChildren(node);
577 }
578
579 @Override
580 public void visit(Node node) {
581 checkState(false, "node %s unsupported", node.getClass());
582 }
583
584 @Override
585 public void visit(HtmlBlockNode node) {
586 // Drop all HTML nodes.
587 }
588
589 @Override
590 public void visit(InlineHtmlNode node) {
591 // Drop all HTML nodes.
592 }
593
594 @Override
595 public void visit(ReferenceNode node) {
596 // Reference nodes are not printed; they only declare an item.
597 }
598
599 @Override
600 public void visit(AbbreviationNode node) {
601 // Abbreviation nodes are not printed; they only declare an item.
602 }
603
604 private void wrapText(String tag, TextNode node) {
605 html.open(tag).appendAndEscape(node.getText()).close(tag);
606 }
607
608 private void wrapChildren(String tag, SuperNode node) {
609 html.open(tag);
610 visitChildren(node);
611 html.close(tag);
612 }
613
614 private void visitChildren(Node node) {
615 for (Node child : node.getChildren()) {
616 child.accept(this);
617 }
618 }
619}