blob: c417932d6db70459666ea1e9596a50057b6fc3c0 [file] [log] [blame]
Shawn Pearce374f1842015-02-10 15:36:54 -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 Pearcecb285012015-03-30 09:10:28 -070017import com.google.common.base.Throwables;
Shawn Pearce374f1842015-02-10 15:36:54 -080018import com.google.gitiles.GitilesView;
19
Shawn Pearce0e5fc542016-02-15 14:27:05 -080020import org.joda.time.Duration;
Shawn Pearce374f1842015-02-10 15:36:54 -080021import org.parboiled.Rule;
Shawn Pearceecddc612015-02-20 22:09:47 -080022import org.parboiled.common.Factory;
Shawn Pearcecb285012015-03-30 09:10:28 -070023import org.parboiled.errors.ParserRuntimeException;
Shawn Pearce96f6d5f2015-02-06 22:45:58 -080024import org.parboiled.support.StringBuilderVar;
Shawn Pearceecddc612015-02-20 22:09:47 -080025import org.parboiled.support.Var;
Shawn Pearce374f1842015-02-10 15:36:54 -080026import org.pegdown.Parser;
27import org.pegdown.ParsingTimeoutException;
28import org.pegdown.PegDownProcessor;
Shawn Pearce96f6d5f2015-02-06 22:45:58 -080029import org.pegdown.ast.Node;
Shawn Pearce374f1842015-02-10 15:36:54 -080030import org.pegdown.ast.RootNode;
Shawn Pearce9119b642015-02-12 22:10:48 -080031import org.pegdown.ast.SimpleNode;
Shawn Pearce374f1842015-02-10 15:36:54 -080032import org.pegdown.plugins.BlockPluginParser;
Shawn Pearce25d91962015-06-22 15:35:36 -070033import org.pegdown.plugins.InlinePluginParser;
Shawn Pearce374f1842015-02-10 15:36:54 -080034import org.pegdown.plugins.PegDownPlugins;
35import org.slf4j.Logger;
36import org.slf4j.LoggerFactory;
37
Shawn Pearceecddc612015-02-20 22:09:47 -080038import java.util.ArrayList;
Shawn Pearce96f6d5f2015-02-06 22:45:58 -080039import java.util.List;
40
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -080041/** Parses Gitiles extensions to markdown. */
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020042public class GitilesMarkdown extends Parser implements BlockPluginParser, InlinePluginParser {
Shawn Pearce108599e2015-02-11 13:28:37 -080043 private static final Logger log = LoggerFactory.getLogger(MarkdownUtil.class);
Shawn Pearce374f1842015-02-10 15:36:54 -080044
45 // SUPPRESS_ALL_HTML is enabled to permit hosting arbitrary user content
46 // while avoiding XSS style HTML, CSS and JavaScript injection attacks.
47 //
48 // HARDWRAPS is disabled to permit line wrapping within paragraphs to
49 // make the source file easier to read in 80 column terminals without
50 // this impacting the rendered formatting.
51 private static final int MD_OPTIONS = (ALL | SUPPRESS_ALL_HTML) & ~(HARDWRAPS);
52
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020053 public static RootNode parseFile(
54 Duration parseTimeout, GitilesView view, String path, String md) {
Shawn Pearce374f1842015-02-10 15:36:54 -080055 if (md == null) {
56 return null;
57 }
58
59 try {
Shawn Pearcecb285012015-03-30 09:10:28 -070060 try {
Shawn Pearce0e5fc542016-02-15 14:27:05 -080061 return newParser(parseTimeout).parseMarkdown(md.toCharArray());
Shawn Pearcecb285012015-03-30 09:10:28 -070062 } catch (ParserRuntimeException e) {
63 Throwables.propagateIfInstanceOf(e.getCause(), ParsingTimeoutException.class);
64 throw e;
65 }
Shawn Pearce374f1842015-02-10 15:36:54 -080066 } catch (ParsingTimeoutException e) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020067 log.error(
68 "timeout {} ms rendering {}/{} at {}",
Shawn Pearce0e5fc542016-02-15 14:27:05 -080069 parseTimeout.getMillis(),
Shawn Pearce374f1842015-02-10 15:36:54 -080070 view.getRepositoryName(),
71 path,
72 view.getRevision().getName());
73 return null;
74 }
75 }
76
Shawn Pearce0e5fc542016-02-15 14:27:05 -080077 private static PegDownProcessor newParser(Duration parseDeadline) {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020078 PegDownPlugins plugins =
79 new PegDownPlugins.Builder().withPlugin(GitilesMarkdown.class, parseDeadline).build();
Shawn Pearce0e5fc542016-02-15 14:27:05 -080080 return new PegDownProcessor(MD_OPTIONS, parseDeadline.getMillis(), plugins);
Shawn Pearce374f1842015-02-10 15:36:54 -080081 }
82
Shawn Pearce0e5fc542016-02-15 14:27:05 -080083 private final Duration parseTimeout;
Shawn Pearce96f6d5f2015-02-06 22:45:58 -080084 private PegDownProcessor parser;
85
Shawn Pearce0e5fc542016-02-15 14:27:05 -080086 GitilesMarkdown(Duration parseTimeout) {
87 super(MD_OPTIONS, parseTimeout.getMillis(), DefaultParseRunnerProvider);
88 this.parseTimeout = parseTimeout;
Shawn Pearce374f1842015-02-10 15:36:54 -080089 }
90
91 @Override
92 public Rule[] blockPluginRules() {
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -080093 return new Rule[] {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +020094 cols(), hr(), iframe(), note(), toc(),
Shawn Pearce96f6d5f2015-02-06 22:45:58 -080095 };
Shawn Pearce374f1842015-02-10 15:36:54 -080096 }
97
Shawn Pearce25d91962015-06-22 15:35:36 -070098 @Override
99 public Rule[] inlinePluginRules() {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200100 return new Rule[] {
101 namedAnchorHtmlStyle(), namedAnchorMarkdownExtensionStyle(),
Shawn Pearce25d91962015-06-22 15:35:36 -0700102 };
103 }
104
Shawn Pearce374f1842015-02-10 15:36:54 -0800105 public Rule toc() {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200106 return NodeSequence(string("[TOC]"), push(new TocNode()));
Shawn Pearce374f1842015-02-10 15:36:54 -0800107 }
Shawn Pearce96f6d5f2015-02-06 22:45:58 -0800108
Shawn Pearce9119b642015-02-12 22:10:48 -0800109 public Rule hr() {
110 // GitHub flavor markdown recognizes "--" as a rule.
111 return NodeSequence(
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200112 NonindentSpace(),
113 string("--"),
114 zeroOrMore('-'),
115 Newline(),
Shawn Pearce9119b642015-02-12 22:10:48 -0800116 oneOrMore(BlankLine()),
117 push(new SimpleNode(SimpleNode.Type.HRule)));
118 }
119
Shawn Pearce25d91962015-06-22 15:35:36 -0700120 public Rule namedAnchorHtmlStyle() {
121 StringBuilderVar name = new StringBuilderVar();
122 return NodeSequence(
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200123 Sp(),
124 string("<a"),
Shawn Pearce25d91962015-06-22 15:35:36 -0700125 Spn1(),
126 sequence(string("name="), attribute(name)),
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200127 Spn1(),
128 '>',
129 Spn1(),
130 string("</a>"),
Shawn Pearce25d91962015-06-22 15:35:36 -0700131 push(new NamedAnchorNode(name.getString())));
132 }
133
134 public Rule namedAnchorMarkdownExtensionStyle() {
135 StringBuilderVar name = new StringBuilderVar();
136 return NodeSequence(
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200137 Sp(), string("{#"), anchorId(name), '}', push(new NamedAnchorNode(name.getString())));
Shawn Pearce25d91962015-06-22 15:35:36 -0700138 }
139
140 public Rule anchorId(StringBuilderVar name) {
141 return sequence(zeroOrMore(testNot('}'), ANY), name.append(match()));
142 }
143
Shawn Pearceee0b06e2015-02-13 00:13:01 -0800144 public Rule iframe() {
145 StringBuilderVar src = new StringBuilderVar();
146 StringBuilderVar h = new StringBuilderVar();
147 StringBuilderVar w = new StringBuilderVar();
148 StringBuilderVar b = new StringBuilderVar();
149 return NodeSequence(
150 string("<iframe"),
151 oneOrMore(
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200152 sequence(
153 Spn1(),
154 firstOf(
155 sequence(string("src="), attribute(src)),
156 sequence(string("height="), attribute(h)),
157 sequence(string("width="), attribute(w)),
158 sequence(string("frameborder="), attribute(b))))),
159 Spn1(),
160 '>',
161 Spn1(),
162 string("</iframe>"),
163 push(new IframeNode(src.getString(), h.getString(), w.getString(), b.getString())));
Shawn Pearceee0b06e2015-02-13 00:13:01 -0800164 }
165
166 public Rule attribute(StringBuilderVar var) {
167 return firstOf(
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200168 sequence('"', zeroOrMore(testNot('"'), ANY), var.append(match()), '"'),
169 sequence('\'', zeroOrMore(testNot('\''), ANY), var.append(match()), '\''));
Shawn Pearceee0b06e2015-02-13 00:13:01 -0800170 }
171
Shawn Pearce96f6d5f2015-02-06 22:45:58 -0800172 public Rule note() {
173 StringBuilderVar body = new StringBuilderVar();
174 return NodeSequence(
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200175 string("***"),
176 Sp(),
177 typeOfNote(),
178 Newline(),
179 oneOrMore(testNot(string("***"), Newline()), Line(body)),
180 string("***"),
181 Newline(),
Shawn Pearce96f6d5f2015-02-06 22:45:58 -0800182 push(new DivNode(popAsString(), parse(body))));
183 }
184
185 public Rule typeOfNote() {
186 return firstOf(
187 sequence(string("note"), push(match())),
188 sequence(string("promo"), push(match())),
189 sequence(string("aside"), push(match())));
190 }
191
Shawn Pearceecddc612015-02-20 22:09:47 -0800192 @SuppressWarnings("unchecked")
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -0800193 public Rule cols() {
194 StringBuilderVar body = new StringBuilderVar();
195 return NodeSequence(
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200196 colsTag(),
197 columnWidths(),
198 Newline(),
199 oneOrMore(testNot(colsTag(), Newline()), Line(body)),
200 colsTag(),
201 Newline(),
Shawn Pearceecddc612015-02-20 22:09:47 -0800202 push(new ColsNode((List<ColsNode.Column>) pop(), parse(body))));
Shawn Pearce6f6f1cd2015-02-07 00:10:24 -0800203 }
204
205 public Rule colsTag() {
206 return string("|||---|||");
207 }
208
Shawn Pearceecddc612015-02-20 22:09:47 -0800209 public Rule columnWidths() {
210 ListVar widths = new ListVar();
211 return sequence(
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200212 zeroOrMore(sequence(Sp(), optional(ch(',')), Sp(), columnWidth(widths))),
213 push(widths.get()));
Shawn Pearceecddc612015-02-20 22:09:47 -0800214 }
215
216 public Rule columnWidth(ListVar widths) {
217 StringBuilderVar s = new StringBuilderVar();
218 return sequence(
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200219 optional(sequence(ch(':'), s.append(':'))),
220 oneOrMore(digit()),
221 s.append(match()),
222 widths.get().add(parse(s.get().toString())));
Shawn Pearceecddc612015-02-20 22:09:47 -0800223 }
224
225 static ColsNode.Column parse(String spec) {
226 ColsNode.Column c = new ColsNode.Column();
227 if (spec.startsWith(":")) {
228 c.empty = true;
229 spec = spec.substring(1);
230 }
231 c.span = Integer.parseInt(spec, 10);
232 return c;
233 }
234
Shawn Pearce96f6d5f2015-02-06 22:45:58 -0800235 public List<Node> parse(StringBuilderVar body) {
236 // The pegdown code doesn't provide enough visibility to directly
237 // use its existing parsing rules. Recurse manually for inner text
238 // parsing within a block.
239 if (parser == null) {
Shawn Pearce0e5fc542016-02-15 14:27:05 -0800240 parser = newParser(parseTimeout);
Shawn Pearce96f6d5f2015-02-06 22:45:58 -0800241 }
242 return parser.parseMarkdown(body.getChars()).getChildren();
243 }
Shawn Pearceecddc612015-02-20 22:09:47 -0800244
245 public static class ListVar extends Var<List<Object>> {
246 @SuppressWarnings({"rawtypes", "unchecked"})
247 public ListVar() {
Han-Wen Nienhuysc0200f62016-05-02 17:34:51 +0200248 super(
249 new Factory() {
250 @Override
251 public Object create() {
252 return new ArrayList<>();
253 }
254 });
Shawn Pearceecddc612015-02-20 22:09:47 -0800255 }
256 }
Shawn Pearce374f1842015-02-10 15:36:54 -0800257}