blob: dcef245646af35b7c5cd99f182c10531d0531f58 [file] [log] [blame] [raw]
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gitiles.doc;
import com.google.common.base.Throwables;
import com.google.gitiles.GitilesView;
import org.parboiled.Rule;
import org.parboiled.common.Factory;
import org.parboiled.errors.ParserRuntimeException;
import org.parboiled.support.StringBuilderVar;
import org.parboiled.support.Var;
import org.pegdown.Parser;
import org.pegdown.ParsingTimeoutException;
import org.pegdown.PegDownProcessor;
import org.pegdown.ast.Node;
import org.pegdown.ast.RootNode;
import org.pegdown.ast.SimpleNode;
import org.pegdown.plugins.BlockPluginParser;
import org.pegdown.plugins.InlinePluginParser;
import org.pegdown.plugins.PegDownPlugins;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/** Parses Gitiles extensions to markdown. */
public class GitilesMarkdown extends Parser
implements BlockPluginParser, InlinePluginParser {
private static final Logger log = LoggerFactory.getLogger(MarkdownUtil.class);
// SUPPRESS_ALL_HTML is enabled to permit hosting arbitrary user content
// while avoiding XSS style HTML, CSS and JavaScript injection attacks.
//
// HARDWRAPS is disabled to permit line wrapping within paragraphs to
// make the source file easier to read in 80 column terminals without
// this impacting the rendered formatting.
private static final int MD_OPTIONS = (ALL | SUPPRESS_ALL_HTML) & ~(HARDWRAPS);
public static RootNode parseFile(GitilesView view, String path, String md) {
if (md == null) {
return null;
}
try {
try {
return newParser().parseMarkdown(md.toCharArray());
} catch (ParserRuntimeException e) {
Throwables.propagateIfInstanceOf(e.getCause(), ParsingTimeoutException.class);
throw e;
}
} catch (ParsingTimeoutException e) {
log.error("timeout rendering {}/{} at {}",
view.getRepositoryName(),
path,
view.getRevision().getName());
return null;
}
}
private static PegDownProcessor newParser() {
PegDownPlugins plugins = new PegDownPlugins.Builder()
.withPlugin(GitilesMarkdown.class)
.build();
return new PegDownProcessor(MD_OPTIONS, plugins);
}
private PegDownProcessor parser;
GitilesMarkdown() {
super(MD_OPTIONS, 2000L, DefaultParseRunnerProvider);
}
@Override
public Rule[] blockPluginRules() {
return new Rule[] {
cols(),
hr(),
iframe(),
note(),
toc(),
};
}
@Override
public Rule[] inlinePluginRules() {
return new Rule[]{
namedAnchorHtmlStyle(),
namedAnchorMarkdownExtensionStyle(),
};
}
public Rule toc() {
return NodeSequence(
string("[TOC]"),
push(new TocNode()));
}
public Rule hr() {
// GitHub flavor markdown recognizes "--" as a rule.
return NodeSequence(
NonindentSpace(), string("--"), zeroOrMore('-'), Newline(),
oneOrMore(BlankLine()),
push(new SimpleNode(SimpleNode.Type.HRule)));
}
public Rule namedAnchorHtmlStyle() {
StringBuilderVar name = new StringBuilderVar();
return NodeSequence(
Sp(), string("<a"),
Spn1(),
sequence(string("name="), attribute(name)),
Spn1(), '>',
Spn1(), string("</a>"),
push(new NamedAnchorNode(name.getString())));
}
public Rule namedAnchorMarkdownExtensionStyle() {
StringBuilderVar name = new StringBuilderVar();
return NodeSequence(
Sp(), string("{#"), anchorId(name), '}',
push(new NamedAnchorNode(name.getString())));
}
public Rule anchorId(StringBuilderVar name) {
return sequence(zeroOrMore(testNot('}'), ANY), name.append(match()));
}
public Rule iframe() {
StringBuilderVar src = new StringBuilderVar();
StringBuilderVar h = new StringBuilderVar();
StringBuilderVar w = new StringBuilderVar();
StringBuilderVar b = new StringBuilderVar();
return NodeSequence(
string("<iframe"),
oneOrMore(
sequence(
Spn1(),
firstOf(
sequence(string("src="), attribute(src)),
sequence(string("height="), attribute(h)),
sequence(string("width="), attribute(w)),
sequence(string("frameborder="), attribute(b))
))),
Spn1(), '>',
Spn1(), string("</iframe>"),
push(new IframeNode(src.getString(),
h.getString(), w.getString(),
b.getString())));
}
public Rule attribute(StringBuilderVar var) {
return firstOf(
sequence('"', zeroOrMore(testNot('"'), ANY), var.append(match()), '"'),
sequence('\'', zeroOrMore(testNot('\''), ANY), var.append(match()), '\''));
}
public Rule note() {
StringBuilderVar body = new StringBuilderVar();
return NodeSequence(
string("***"), Sp(), typeOfNote(), Newline(),
oneOrMore(
testNot(string("***"), Newline()),
Line(body)),
string("***"), Newline(),
push(new DivNode(popAsString(), parse(body))));
}
public Rule typeOfNote() {
return firstOf(
sequence(string("note"), push(match())),
sequence(string("promo"), push(match())),
sequence(string("aside"), push(match())));
}
@SuppressWarnings("unchecked")
public Rule cols() {
StringBuilderVar body = new StringBuilderVar();
return NodeSequence(
colsTag(), columnWidths(), Newline(),
oneOrMore(
testNot(colsTag(), Newline()),
Line(body)),
colsTag(), Newline(),
push(new ColsNode((List<ColsNode.Column>) pop(), parse(body))));
}
public Rule colsTag() {
return string("|||---|||");
}
public Rule columnWidths() {
ListVar widths = new ListVar();
return sequence(
zeroOrMore(
sequence(
Sp(), optional(ch(',')), Sp(),
columnWidth(widths))),
push(widths.get()));
}
public Rule columnWidth(ListVar widths) {
StringBuilderVar s = new StringBuilderVar();
return sequence(
optional(sequence(ch(':'), s.append(':'))),
oneOrMore(digit()), s.append(match()),
widths.get().add(parse(s.get().toString())));
}
static ColsNode.Column parse(String spec) {
ColsNode.Column c = new ColsNode.Column();
if (spec.startsWith(":")) {
c.empty = true;
spec = spec.substring(1);
}
c.span = Integer.parseInt(spec, 10);
return c;
}
public List<Node> parse(StringBuilderVar body) {
// The pegdown code doesn't provide enough visibility to directly
// use its existing parsing rules. Recurse manually for inner text
// parsing within a block.
if (parser == null) {
parser = newParser();
}
return parser.parseMarkdown(body.getChars()).getChildren();
}
public static class ListVar extends Var<List<Object>> {
@SuppressWarnings({"rawtypes", "unchecked"})
public ListVar() {
super(new Factory() {
@Override
public Object create() {
return new ArrayList<>();
}
});
}
}
}