Merge "Switch to commonmark 0.5.0"
diff --git a/Documentation/markdown.md b/Documentation/markdown.md
index e1c6da3..652d5ed 100644
--- a/Documentation/markdown.md
+++ b/Documentation/markdown.md
@@ -140,7 +140,6 @@
| Apple | 95 | Yes |
| Pear | 102 | Yes |
| Hay | 977 | |
-[Food and its benefits]
```
will render as:
@@ -150,35 +149,12 @@
| Apple | 95 | Yes |
| Pear | 102 | Yes |
| Hay | 977 | |
-[Food and its benefits]
Placing `:` in the separator line indicates how the column should be
aligned. A colon on the left side is a **left-aligned** column; a
colon on the right-most side is **right-aligned**; a colon on both
sides is **center-aligned**.
-An optional table title can be placed under the table in brackets
-(`[...]`).
-
-Cells may span multiple columns and include formatting accepted within
-paragraphs such as emphasis, images or links:
-
-| | Grouping ||
-| First Header | Second Header | Third Header |
-| ------------ | :-----------: | -----------: |
-| Content | *Long Cell* ||
-| Content | **Cell 2** | Cell 3 |
-
-the above table was created by:
-
-```
-| | Grouping ||
-| First Header | Second Header | Third Header |
-| ------------ | :-----------: | -----------: |
-| Content | *Long Cell* ||
-| Content | **Cell 2** | Cell 3 |
-```
-
Empty table cells are indicated by whitespace between the column
dividers (`| |`) while multiple column cells omit the whitespace.
@@ -213,14 +189,11 @@
### Smart quotes
-'Single', "double" and <<double angle>> quotes in paragraph text are
+'Single' and "double" quotes in paragraph text are
replaced with smart quotes. Apostrophes (this doc's text), ellipses
("...") and dashes ("--" and "---") are also replaced with HTML
entities to make the documentation appear typeset.
-To force use of the ASCII characters prefix with \, for example `\'`
-for a \' normal single quote.
-
### Blockquotes
Blockquoted text can be used to stand off text obtained from
@@ -253,20 +226,20 @@
Create a fenced code block using three backticks before and after a
block of code, preceeded and followed by blank lines:
+````
+This is a simple hello world program in C:
+
+``` c
+#include <stdio.h>
+
+int main() {
+ printf("Hello, World.\n");
+ return 0;
+}
```
- This is a simple hello world program in C:
- ```c
- #include <stdio.h>
-
- int main() {
- printf("Hello, World.\n");
- return 0;
- }
- ```
-
- To compile it use `gcc hello.c`.
-```
+To compile it use `gcc hello.c`.
+````
Text within a fenced code block is taken verbatim and is not
processed for Markdown markup.
@@ -331,7 +304,7 @@
line will also create a horizontal rule:
```
---
+---
- - - -
@@ -469,12 +442,13 @@
### HTML
-HTML tags are not supported. HTML will be dropped on the floor by the
-parser with no warnings, and no output from that section of the
+Most HTML tags are not supported. HTML will be dropped on the floor
+by the parser with no warnings, and no output from that section of the
document.
-There is a small exception for `<a name>` and `<iframe>` elements, see
-[named anchor](#Named-anchors) and [HTML IFrame](#HTML-IFrame).
+There are small exceptions for `<br>`, `<hr>`, `<a name>` and
+`<iframe>` elements, see [named anchor](#Named-anchors) and
+[HTML IFrame](#HTML-IFrame).
## Markdown extensions
@@ -768,17 +742,6 @@
imageLimit = 256K
```
-### Parsing timeout
-
-Parsing Markdown can be expensive so this implementation places
-a default upper bound of 2 seconds on running time per document.
-This is measured in wall clock time from the start of the request.
-
-```
-[markdown]
- parseTimeout = 2s
-```
-
### Google Analytics
[Google Analytics](https://www.google.com/analytics/) can be
diff --git a/README.md b/README.md
index 21bb473..686f964 100644
--- a/README.md
+++ b/README.md
@@ -63,20 +63,22 @@
Code Style
----------
-Java code in Gitiles follows the [Google Java Style Guide]
-(https://google.github.io/styleguide/javaguide.html) with a 100-column limit.
+Java code in Gitiles follows the [Google Java Style Guide][java-style]
+with a 100-column limit.
-Code should be automatically formatted using [google-java-format]
-(https://github.com/google/google-java-format) prior to sending a code review.
-There is currently no Eclipse formatter, but the tool can be run from the
-command line:
+Code should be automatically formatted using [google-java-format][fmt]
+prior to sending a code review. There is currently no Eclipse
+formatter, but the tool can be run from the command line:
```
java -jar /path/to/google-java-format-1.0-all-deps.jar -i path/to/java/File.java
```
-CSS in Gitiles follows the [SUIT CSS naming conventions]
-(https://github.com/suitcss/suit/blob/master/doc/naming-conventions.md).
+CSS in Gitiles follows the [SUIT CSS naming conventions][suit].
+
+[java-style]: https://google.github.io/styleguide/javaguide.html
+[fmt]: https://github.com/google/google-java-format
+[suit]: https://github.com/suitcss/suit/blob/master/doc/naming-conventions.md
Code Review
-----------
diff --git a/gitiles-servlet/BUCK b/gitiles-servlet/BUCK
index 9ce4283..97dde01 100644
--- a/gitiles-servlet/BUCK
+++ b/gitiles-servlet/BUCK
@@ -3,12 +3,14 @@
DEPS = [
'//blame-cache:lib',
'//lib:commons-lang',
- '//lib:grappa',
'//lib:gson',
'//lib:guava',
'//lib:joda-time',
'//lib:jsr305',
- '//lib:pegdown',
+ '//lib:commonmark',
+ '//lib:cm-autolink',
+ '//lib:gfm-tables',
+ '//lib:gfm-strikethrough',
'//lib:prettify',
'//lib/jgit:jgit',
'//lib/jgit:jgit-servlet',
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java b/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
index 10b2048..eee4d83 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
@@ -19,6 +19,7 @@
import com.google.gitiles.doc.MarkdownToHtml;
import com.google.template.soy.data.SanitizedContent;
+import org.commonmark.node.Node;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.LargeObjectException;
@@ -31,8 +32,6 @@
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.RawParseUtils;
-import org.joda.time.Duration;
-import org.pegdown.ast.RootNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -90,13 +89,10 @@
SanitizedContent render() {
try {
- Duration parseTimeout =
- ConfigUtil.getDuration(
- cfg, "markdown", null, "parseTimeout", Duration.standardSeconds(2));
int inputLimit = cfg.getInt("markdown", "inputLimit", 5 << 20);
byte[] raw = reader.open(readmeId, Constants.OBJ_BLOB).getCachedBytes(inputLimit);
String md = RawParseUtils.decode(raw);
- RootNode root = GitilesMarkdown.parseFile(parseTimeout, view, readmePath, md);
+ Node root = GitilesMarkdown.parse(md);
if (root == null) {
return null;
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNote.java
similarity index 63%
copy from gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
copy to gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNote.java
index 6c9f2f6..ef1f4c9 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNote.java
@@ -4,7 +4,7 @@
// 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
+// 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,
@@ -14,16 +14,17 @@
package com.google.gitiles.doc;
-public interface Visitor extends org.pegdown.ast.Visitor {
- void visit(ColsNode node);
+import org.commonmark.node.CustomBlock;
- void visit(ColsNode.Column node);
+/** Block note to render as {@code <div class="clazz">}. */
+public class BlockNote extends CustomBlock {
+ private String clazz;
- void visit(DivNode node);
+ public String getClassName() {
+ return clazz;
+ }
- void visit(IframeNode node);
-
- void visit(TocNode node);
-
- void visit(NamedAnchorNode node);
+ public void setClassName(String clazz) {
+ this.clazz = clazz;
+ }
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNoteExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNoteExtension.java
new file mode 100644
index 0000000..4a37eee
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/BlockNoteExtension.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// 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.collect.ImmutableSet;
+
+import org.commonmark.Extension;
+import org.commonmark.node.Block;
+import org.commonmark.parser.Parser;
+import org.commonmark.parser.Parser.ParserExtension;
+import org.commonmark.parser.block.AbstractBlockParser;
+import org.commonmark.parser.block.AbstractBlockParserFactory;
+import org.commonmark.parser.block.BlockContinue;
+import org.commonmark.parser.block.BlockStart;
+import org.commonmark.parser.block.MatchedBlockParser;
+import org.commonmark.parser.block.ParserState;
+
+/**
+ * CommonMark extension for block notes.
+ * <pre>
+ * *** note
+ * This is a note.
+ * ***
+ * </pre>
+ */
+public class BlockNoteExtension implements ParserExtension {
+ private static final ImmutableSet<String> VALID_STYLES =
+ ImmutableSet.of("note", "aside", "promo");
+
+ public static Extension create() {
+ return new BlockNoteExtension();
+ }
+
+ private BlockNoteExtension() {}
+
+ @Override
+ public void extend(Parser.Builder builder) {
+ builder.customBlockParserFactory(new NoteParserFactory());
+ }
+
+ private static class NoteParser extends AbstractBlockParser {
+ private final BlockNote block;
+ private boolean done;
+
+ NoteParser(String style) {
+ block = new BlockNote();
+ block.setClassName(style);
+ }
+
+ @Override
+ public Block getBlock() {
+ return block;
+ }
+
+ @Override
+ public BlockContinue tryContinue(ParserState state) {
+ if (done) {
+ return BlockContinue.none();
+ }
+ if (state.getIndent() == 0) {
+ int s = state.getNextNonSpaceIndex();
+ CharSequence line = state.getLine();
+ if ("***".contentEquals(line.subSequence(s, line.length()))) {
+ done = true;
+ return BlockContinue.atIndex(line.length());
+ }
+ }
+ return BlockContinue.atIndex(state.getIndex());
+ }
+
+ @Override
+ public boolean isContainer() {
+ return true;
+ }
+
+ @Override
+ public boolean canContain(Block block) {
+ return true;
+ }
+ }
+
+ private static class NoteParserFactory extends AbstractBlockParserFactory {
+ @Override
+ public BlockStart tryStart(ParserState state, MatchedBlockParser matched) {
+ if (state.getIndent() > 0) {
+ return BlockStart.none();
+ }
+
+ int s = state.getNextNonSpaceIndex();
+ CharSequence line = state.getLine();
+ CharSequence text = line.subSequence(s, line.length());
+ if (text.length() < 4 || !"*** ".contentEquals(text.subSequence(0, 4))) {
+ return BlockStart.none();
+ }
+
+ String style = text.subSequence(4, line.length()).toString().trim();
+ if (!VALID_STYLES.contains(style)) {
+ return BlockStart.none();
+ }
+
+ return BlockStart.of(new NoteParser(style)).atIndex(line.length());
+ }
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/ColsNode.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/ColsNode.java
deleted file mode 100644
index 502c39f..0000000
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/ColsNode.java
+++ /dev/null
@@ -1,96 +0,0 @@
-// 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 org.pegdown.ast.HeaderNode;
-import org.pegdown.ast.Node;
-import org.pegdown.ast.SuperNode;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Multi-column layout delineated by {@code |||---|||}.
- * <p>
- * Each header within the layout creates a new column in the HTML.
- */
-public class ColsNode extends SuperNode {
- static final int GRID_WIDTH = 12;
-
- ColsNode(List<Column> spec, List<Node> children) {
- super(wrap(spec, children));
- }
-
- @Override
- public void accept(org.pegdown.ast.Visitor visitor) {
- ((Visitor) visitor).visit(this);
- }
-
- private static List<Node> wrap(List<Column> spec, List<Node> children) {
- List<Column> columns = copyOf(spec);
- splitChildren(columns, children);
-
- int remaining = GRID_WIDTH;
- for (int i = 0; i < columns.size(); i++) {
- Column col = columns.get(i);
- if (col.span <= 0 || col.span > GRID_WIDTH) {
- col.span = remaining / (columns.size() - i);
- }
- remaining = Math.max(0, remaining - col.span);
- }
- return asNodeList(columns);
- }
-
- private static void splitChildren(List<Column> columns, List<Node> children) {
- int idx = 0;
- Column col = null;
- for (Node n : children) {
- if (col == null || n instanceof HeaderNode || n instanceof DivNode) {
- for (; ; ) {
- if (idx < columns.size()) {
- col = columns.get(idx);
- } else {
- col = new Column();
- columns.add(col);
- }
- idx++;
- if (!col.empty) {
- break;
- }
- }
- }
- col.getChildren().add(n);
- }
- }
-
- private static <T> ArrayList<T> copyOf(List<T> in) {
- return in != null && !in.isEmpty() ? new ArrayList<>(in) : new ArrayList<T>();
- }
-
- @SuppressWarnings("unchecked")
- private static List<Node> asNodeList(List<? extends Node> columns) {
- return (List<Node>) columns;
- }
-
- static class Column extends SuperNode {
- int span;
- boolean empty;
-
- @Override
- public void accept(org.pegdown.ast.Visitor visitor) {
- ((Visitor) visitor).visit(this);
- }
- }
-}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DivNode.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DivNode.java
deleted file mode 100644
index 9bdb926..0000000
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DivNode.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// 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 org.pegdown.ast.Node;
-import org.pegdown.ast.ParaNode;
-import org.pegdown.ast.SuperNode;
-
-import java.util.List;
-
-/** Block note to render as {@code <div class="clazz">}. */
-public class DivNode extends SuperNode {
- private final String style;
-
- DivNode(String style, List<Node> list) {
- super(
- list.size() == 1 && list.get(0) instanceof ParaNode
- ? ((ParaNode) list.get(0)).getChildren()
- : list);
- this.style = style;
- }
-
- public String getStyleName() {
- return style;
- }
-
- @Override
- public void accept(org.pegdown.ast.Visitor visitor) {
- ((Visitor) visitor).visit(this);
- }
-}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java
index 1ba14a8..8c30e26 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DocServlet.java
@@ -29,13 +29,13 @@
import com.google.common.hash.Hashing;
import com.google.common.net.HttpHeaders;
import com.google.gitiles.BaseServlet;
-import com.google.gitiles.ConfigUtil;
import com.google.gitiles.FormatType;
import com.google.gitiles.GitilesAccess;
import com.google.gitiles.GitilesView;
import com.google.gitiles.Renderer;
import com.google.gitiles.ViewFilter;
+import org.commonmark.node.Node;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.http.server.ServletUtils;
import org.eclipse.jgit.lib.Config;
@@ -48,8 +48,6 @@
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.RawParseUtils;
-import org.joda.time.Duration;
-import org.pegdown.ast.RootNode;
import java.io.IOException;
import java.util.HashMap;
@@ -69,7 +67,7 @@
// Generation of ETag logic. Bump this only if DocServlet logic changes
// significantly enough to impact cached pages. Soy template and source
// files are automatically hashed as part of the ETag.
- private static final int ETAG_GEN = 4;
+ private static final int ETAG_GEN = 5;
public DocServlet(GitilesAccess.Factory accessFactory, Renderer renderer) {
super(renderer, accessFactory);
@@ -109,26 +107,21 @@
return;
}
- Duration parseTimeout =
- ConfigUtil.getDuration(
- cfg, "markdown", null, "parseTimeout", Duration.standardSeconds(2));
view = view.toBuilder().setPathPart(srcmd.path).build();
int inputLimit = cfg.getInt("markdown", "inputLimit", 5 << 20);
- RootNode doc =
- GitilesMarkdown.parseFile(
- parseTimeout, view, srcmd.path, srcmd.read(rw.getObjectReader(), inputLimit));
+ Node doc = GitilesMarkdown.parse(srcmd.read(rw.getObjectReader(), inputLimit));
if (doc == null) {
res.sendRedirect(GitilesView.show().copyFrom(view).toUrl());
return;
}
String navPath = null;
- RootNode nav = null;
+ String navMarkdown = null;
+ Node nav = null;
if (navmd != null) {
navPath = navmd.path;
- nav =
- GitilesMarkdown.parseFile(
- parseTimeout, view, navPath, navmd.read(rw.getObjectReader(), inputLimit));
+ navMarkdown = navmd.read(rw.getObjectReader(), inputLimit);
+ nav = GitilesMarkdown.parse(navMarkdown);
if (nav == null) {
res.setStatus(SC_INTERNAL_SERVER_ERROR);
return;
@@ -142,7 +135,7 @@
}
res.setHeader(HttpHeaders.ETAG, curEtag);
- showDoc(req, res, view, cfg, img, navPath, nav, srcmd.path, doc);
+ showDoc(req, res, view, cfg, img, navPath, navMarkdown, nav, srcmd.path, doc);
}
}
@@ -179,19 +172,23 @@
Config cfg,
ImageLoader img,
String navPath,
- RootNode nav,
+ String navMarkdown,
+ Node nav,
String docPath,
- RootNode doc)
+ Node doc)
throws IOException {
Map<String, Object> data = new HashMap<>();
- data.putAll(Navbar.bannerSoyData(view, img, nav));
+
+ MarkdownToHtml navHtml = new MarkdownToHtml(view, cfg, navPath);
+ data.putAll(Navbar.bannerSoyData(img, navHtml, navMarkdown, nav));
+ data.put("navbarHtml", navHtml.toSoyHtml(nav));
+
data.put("pageTitle", MoreObjects.firstNonNull(MarkdownUtil.getTitle(doc), view.getPathPart()));
if (view.getType() != GitilesView.Type.ROOTED_DOC) {
data.put("sourceUrl", GitilesView.show().copyFrom(view).toUrl());
data.put("logUrl", GitilesView.log().copyFrom(view).toUrl());
data.put("blameUrl", GitilesView.blame().copyFrom(view).toUrl());
}
- data.put("navbarHtml", new MarkdownToHtml(view, cfg, navPath).toSoyHtml(nav));
data.put("bodyHtml", new MarkdownToHtml(view, cfg, docPath).setImageLoader(img).toSoyHtml(doc));
String analyticsId = cfg.getString("google", null, "analyticsId");
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitHubThematicBreakExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitHubThematicBreakExtension.java
new file mode 100644
index 0000000..3e32990
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitHubThematicBreakExtension.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// 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 org.commonmark.Extension;
+import org.commonmark.node.Block;
+import org.commonmark.node.ThematicBreak;
+import org.commonmark.parser.Parser;
+import org.commonmark.parser.Parser.ParserExtension;
+import org.commonmark.parser.block.AbstractBlockParser;
+import org.commonmark.parser.block.AbstractBlockParserFactory;
+import org.commonmark.parser.block.BlockContinue;
+import org.commonmark.parser.block.BlockStart;
+import org.commonmark.parser.block.MatchedBlockParser;
+import org.commonmark.parser.block.ParserState;
+
+/** Accepts just {@code --} for an {@code <hr>}. */
+public class GitHubThematicBreakExtension implements ParserExtension {
+ public static Extension create() {
+ return new GitHubThematicBreakExtension();
+ }
+
+ private GitHubThematicBreakExtension() {}
+
+ @Override
+ public void extend(Parser.Builder builder) {
+ builder.customBlockParserFactory(new BreakParserFactory());
+ }
+
+ private static class BreakParser extends AbstractBlockParser {
+ @Override
+ public Block getBlock() {
+ return new ThematicBreak();
+ }
+
+ @Override
+ public BlockContinue tryContinue(ParserState parserState) {
+ return BlockContinue.none();
+ }
+ }
+
+ private static class BreakParserFactory extends AbstractBlockParserFactory {
+ @Override
+ public BlockStart tryStart(ParserState state, MatchedBlockParser matched) {
+ if (state.getIndent() == 0) {
+ CharSequence line = state.getLine();
+ int s = state.getNextNonSpaceIndex();
+ if ("--".contentEquals(line.subSequence(s, line.length()))) {
+ return BlockStart.of(new BreakParser()).atIndex(line.length());
+ }
+ }
+ return BlockStart.none();
+ }
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesHtmlExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesHtmlExtension.java
new file mode 100644
index 0000000..01d4714
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesHtmlExtension.java
@@ -0,0 +1,203 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// 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.gitiles.doc.html.HtmlBuilder;
+
+import org.commonmark.Extension;
+import org.commonmark.node.AbstractVisitor;
+import org.commonmark.node.HardLineBreak;
+import org.commonmark.node.HtmlBlock;
+import org.commonmark.node.HtmlInline;
+import org.commonmark.node.Node;
+import org.commonmark.node.ThematicBreak;
+import org.commonmark.parser.Parser;
+import org.commonmark.parser.Parser.ParserExtension;
+import org.commonmark.parser.PostProcessor;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Convert some {@link HtmlInline} and {@link HtmlBlock} to safe types.
+ * <p>
+ * Gitiles style Markdown accepts only a very small subset of HTML that is safe
+ * for use within the document. This {@code PostProcessor} scans parsed nodes
+ * and converts them to safer types for rendering:
+ * <ul>
+ * <li>{@link HardLineBreak}
+ * <li>{@link ThematicBreak}
+ * <li>{@link NamedAnchor}
+ * <li>{@link IframeBlock}
+ * </ul>
+ */
+public class GitilesHtmlExtension implements ParserExtension {
+ private static final Pattern BREAK = Pattern.compile("<(hr|br)\\s*/?>", Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern ANCHOR_OPEN =
+ Pattern.compile("<a\\s+name=([\"'])([^\"'\\s]+)\\1>", Pattern.CASE_INSENSITIVE);
+ private static final Pattern ANCHOR_CLOSE = Pattern.compile("</[aA]>");
+
+ private static final Pattern IFRAME_OPEN =
+ Pattern.compile("<iframe\\s+", Pattern.CASE_INSENSITIVE);
+ private static final Pattern IFRAME_CLOSE =
+ Pattern.compile("(?:/?>|</iframe>)", Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern ATTR =
+ Pattern.compile(
+ "\\s+([a-z-]+)\\s*=\\s*([^\\s\"'=<>`]+|'[^']*'|\"[^\"]*\")", Pattern.CASE_INSENSITIVE);
+
+ public static Extension create() {
+ return new GitilesHtmlExtension();
+ }
+
+ private GitilesHtmlExtension() {}
+
+ @Override
+ public void extend(Parser.Builder builder) {
+ builder.postProcessor(new HtmlProcessor());
+ }
+
+ private static class HtmlProcessor implements PostProcessor {
+ @Override
+ public Node process(Node node) {
+ node.accept(new HtmlVisitor());
+ return node;
+ }
+ }
+
+ private static class HtmlVisitor extends AbstractVisitor {
+ @Override
+ public void visit(HtmlInline node) {
+ inline(node);
+ }
+
+ @Override
+ public void visit(HtmlBlock node) {
+ block(node);
+ }
+ }
+
+ private static void inline(HtmlInline curr) {
+ String html = curr.getLiteral();
+ Matcher m = BREAK.matcher(html);
+ if (m.matches()) {
+ switch (m.group(1).toLowerCase()) {
+ case "br":
+ curr.insertAfter(new HardLineBreak());
+ curr.unlink();
+ return;
+
+ case "hr":
+ curr.insertAfter(new ThematicBreak());
+ curr.unlink();
+ return;
+ }
+ }
+
+ m = ANCHOR_OPEN.matcher(html);
+ if (m.matches()) {
+ String name = m.group(2);
+ Node next = curr.getNext();
+
+ // HtmlInline{<a name="id">}HtmlInline{</a>}
+ if (isAnchorClose(next)) {
+ next.unlink();
+ next = curr.getNext();
+
+ NamedAnchor anchor = new NamedAnchor();
+ anchor.setName(name);
+ curr.insertAfter(anchor);
+ curr.unlink();
+ MarkdownUtil.trimPreviousWhitespace(anchor);
+ return;
+ }
+ }
+
+ // Discard potentially unsafe HtmlInline.
+ curr.unlink();
+ }
+
+ private static boolean isAnchorClose(Node n) {
+ return n instanceof HtmlInline && ANCHOR_CLOSE.matcher(((HtmlInline) n).getLiteral()).matches();
+ }
+
+ private static void block(HtmlBlock curr) {
+ String html = curr.getLiteral();
+ Matcher m = IFRAME_OPEN.matcher(html);
+ if (m.find()) {
+ int start = m.end() - 1 /* leave whitespace */;
+ m = IFRAME_CLOSE.matcher(html.substring(start));
+ if (m.find()) {
+ int end = start + m.start();
+ IframeBlock f = iframe(html.substring(start, end));
+ if (f != null) {
+ curr.insertAfter(f);
+ curr.unlink();
+ return;
+ }
+ }
+ }
+
+ // Discard potentially unsafe HtmlBlock.
+ curr.unlink();
+ }
+
+ private static IframeBlock iframe(String html) {
+ IframeBlock iframe = new IframeBlock();
+ Matcher m = ATTR.matcher(html);
+ while (m.find()) {
+ String att = m.group(1).toLowerCase();
+ String val = attributeValue(m);
+ switch (att) {
+ case "src":
+ if (!HtmlBuilder.isValidHttpUri(val)) {
+ return null;
+ }
+ iframe.src = val;
+ break;
+
+ case "height":
+ if (!HtmlBuilder.isValidCssDimension(val)) {
+ return null;
+ }
+ iframe.height = val;
+ break;
+
+ case "width":
+ if (!HtmlBuilder.isValidCssDimension(val)) {
+ return null;
+ }
+ iframe.width = val;
+ break;
+
+ case "frameborder":
+ iframe.border = !"0".equals(val);
+ break;
+ }
+ }
+ return iframe.src != null ? iframe : null;
+ }
+
+ private static String attributeValue(Matcher m) {
+ String val = m.group(2);
+ if (val.length() >= 2 && (val.charAt(0) == '\'' || val.charAt(0) == '"')) {
+ // Capture group includes the opening and closing quotation marks if the
+ // attribute value was quoted in the source document. Trim these.
+ val = val.substring(1, val.length() - 1);
+ }
+ return val;
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java
index c417932..541343b 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java
@@ -14,244 +14,35 @@
package com.google.gitiles.doc;
-import com.google.common.base.Throwables;
-import com.google.gitiles.GitilesView;
+import com.google.common.collect.ImmutableList;
-import org.joda.time.Duration;
-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 org.commonmark.ext.autolink.AutolinkExtension;
+import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
+import org.commonmark.ext.gfm.tables.TablesExtension;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
-import java.util.ArrayList;
-import java.util.List;
+/** Parses Gitiles style CommonMark Markdown. */
+public class GitilesMarkdown {
+ private static final Parser PARSER =
+ Parser.builder()
+ .extensions(
+ ImmutableList.of(
+ AutolinkExtension.create(),
+ BlockNoteExtension.create(),
+ GitilesHtmlExtension.create(),
+ GitHubThematicBreakExtension.create(),
+ MultiColumnExtension.create(),
+ NamedAnchorExtension.create(),
+ SmartQuotedExtension.create(),
+ StrikethroughExtension.create(),
+ TablesExtension.create(),
+ TocExtension.create()))
+ .build();
-/** 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(
- Duration parseTimeout, GitilesView view, String path, String md) {
- if (md == null) {
- return null;
- }
-
- try {
- try {
- return newParser(parseTimeout).parseMarkdown(md.toCharArray());
- } catch (ParserRuntimeException e) {
- Throwables.propagateIfInstanceOf(e.getCause(), ParsingTimeoutException.class);
- throw e;
- }
- } catch (ParsingTimeoutException e) {
- log.error(
- "timeout {} ms rendering {}/{} at {}",
- parseTimeout.getMillis(),
- view.getRepositoryName(),
- path,
- view.getRevision().getName());
- return null;
- }
+ public static Node parse(String md) {
+ return md != null ? PARSER.parse(md) : null;
}
- private static PegDownProcessor newParser(Duration parseDeadline) {
- PegDownPlugins plugins =
- new PegDownPlugins.Builder().withPlugin(GitilesMarkdown.class, parseDeadline).build();
- return new PegDownProcessor(MD_OPTIONS, parseDeadline.getMillis(), plugins);
- }
-
- private final Duration parseTimeout;
- private PegDownProcessor parser;
-
- GitilesMarkdown(Duration parseTimeout) {
- super(MD_OPTIONS, parseTimeout.getMillis(), DefaultParseRunnerProvider);
- this.parseTimeout = parseTimeout;
- }
-
- @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(parseTimeout);
- }
- 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<>();
- }
- });
- }
- }
+ private GitilesMarkdown() {}
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/IframeBlock.java
similarity index 71%
rename from gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
rename to gitiles-servlet/src/main/java/com/google/gitiles/doc/IframeBlock.java
index 6c9f2f6..3ad0153 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/IframeBlock.java
@@ -14,16 +14,12 @@
package com.google.gitiles.doc;
-public interface Visitor extends org.pegdown.ast.Visitor {
- void visit(ColsNode node);
+import org.commonmark.node.CustomBlock;
- void visit(ColsNode.Column node);
-
- void visit(DivNode node);
-
- void visit(IframeNode node);
-
- void visit(TocNode node);
-
- void visit(NamedAnchorNode node);
+/** Parsed {@code <iframe>} tag. */
+public class IframeBlock extends CustomBlock {
+ String src;
+ String height;
+ String width;
+ boolean border;
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/IframeNode.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/IframeNode.java
deleted file mode 100644
index f4d1ca3..0000000
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/IframeNode.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// 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.Strings;
-
-import org.pegdown.ast.AbstractNode;
-import org.pegdown.ast.Node;
-
-import java.util.Collections;
-import java.util.List;
-
-class IframeNode extends AbstractNode {
- final String src;
- final String height;
- final String width;
- final boolean border;
-
- IframeNode(String src, String height, String width, String border) {
- this.src = src;
- this.height = Strings.emptyToNull(height);
- this.width = Strings.emptyToNull(width);
- this.border = !"0".equals(border);
- }
-
- @Override
- public void accept(org.pegdown.ast.Visitor visitor) {
- ((Visitor) visitor).visit(this);
- }
-
- @Override
- public List<Node> getChildren() {
- return Collections.emptyList();
- }
-}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
index 7b30e27..1b88d5b 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
@@ -14,7 +14,6 @@
package com.google.gitiles.doc;
-import static com.google.common.base.Preconditions.checkState;
import static com.google.gitiles.doc.MarkdownUtil.getInnerText;
import com.google.common.annotations.VisibleForTesting;
@@ -23,69 +22,62 @@
import com.google.gitiles.ThreadSafePrettifyParser;
import com.google.gitiles.doc.html.HtmlBuilder;
import com.google.template.soy.data.SanitizedContent;
-import com.google.template.soy.shared.restricted.EscapingConventions.FilterImageDataUri;
import com.google.template.soy.shared.restricted.EscapingConventions.FilterNormalizeUri;
+import org.commonmark.ext.gfm.strikethrough.Strikethrough;
+import org.commonmark.ext.gfm.tables.TableBlock;
+import org.commonmark.ext.gfm.tables.TableBody;
+import org.commonmark.ext.gfm.tables.TableCell;
+import org.commonmark.ext.gfm.tables.TableHead;
+import org.commonmark.ext.gfm.tables.TableRow;
+import org.commonmark.node.Block;
+import org.commonmark.node.BlockQuote;
+import org.commonmark.node.BulletList;
+import org.commonmark.node.Code;
+import org.commonmark.node.CustomBlock;
+import org.commonmark.node.CustomNode;
+import org.commonmark.node.Document;
+import org.commonmark.node.Emphasis;
+import org.commonmark.node.FencedCodeBlock;
+import org.commonmark.node.HardLineBreak;
+import org.commonmark.node.Heading;
+import org.commonmark.node.HtmlBlock;
+import org.commonmark.node.HtmlInline;
+import org.commonmark.node.Image;
+import org.commonmark.node.IndentedCodeBlock;
+import org.commonmark.node.Link;
+import org.commonmark.node.ListBlock;
+import org.commonmark.node.ListItem;
+import org.commonmark.node.Node;
+import org.commonmark.node.OrderedList;
+import org.commonmark.node.Paragraph;
+import org.commonmark.node.SoftLineBreak;
+import org.commonmark.node.StrongEmphasis;
+import org.commonmark.node.Text;
+import org.commonmark.node.ThematicBreak;
+import org.commonmark.node.Visitor;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.util.StringUtils;
-import org.pegdown.ast.AbbreviationNode;
-import org.pegdown.ast.AutoLinkNode;
-import org.pegdown.ast.BlockQuoteNode;
-import org.pegdown.ast.BulletListNode;
-import org.pegdown.ast.CodeNode;
-import org.pegdown.ast.DefinitionListNode;
-import org.pegdown.ast.DefinitionNode;
-import org.pegdown.ast.DefinitionTermNode;
-import org.pegdown.ast.ExpImageNode;
-import org.pegdown.ast.ExpLinkNode;
-import org.pegdown.ast.HeaderNode;
-import org.pegdown.ast.HtmlBlockNode;
-import org.pegdown.ast.InlineHtmlNode;
-import org.pegdown.ast.ListItemNode;
-import org.pegdown.ast.MailLinkNode;
-import org.pegdown.ast.Node;
-import org.pegdown.ast.OrderedListNode;
-import org.pegdown.ast.ParaNode;
-import org.pegdown.ast.QuotedNode;
-import org.pegdown.ast.RefImageNode;
-import org.pegdown.ast.RefLinkNode;
-import org.pegdown.ast.ReferenceNode;
-import org.pegdown.ast.RootNode;
-import org.pegdown.ast.SimpleNode;
-import org.pegdown.ast.SpecialTextNode;
-import org.pegdown.ast.StrikeNode;
-import org.pegdown.ast.StrongEmphSuperNode;
-import org.pegdown.ast.SuperNode;
-import org.pegdown.ast.TableBodyNode;
-import org.pegdown.ast.TableCaptionNode;
-import org.pegdown.ast.TableCellNode;
-import org.pegdown.ast.TableColumnNode;
-import org.pegdown.ast.TableHeaderNode;
-import org.pegdown.ast.TableNode;
-import org.pegdown.ast.TableRowNode;
-import org.pegdown.ast.TextNode;
-import org.pegdown.ast.VerbatimNode;
-import org.pegdown.ast.WikiLinkNode;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import prettify.parser.Prettify;
import syntaxhighlight.ParseResult;
/**
- * Formats parsed markdown AST into HTML.
+ * Formats parsed Markdown AST into HTML.
* <p>
- * Callers must create a new instance for each RootNode.
+ * Callers must create a new instance for each document.
*/
public class MarkdownToHtml implements Visitor {
- private final ReferenceMap references = new ReferenceMap();
private final HtmlBuilder html = new HtmlBuilder();
private final TocFormatter toc = new TocFormatter(html, 3);
private final GitilesView view;
private final Config cfg;
private final String filePath;
private ImageLoader imageLoader;
- private TableState table;
private boolean outputNamedAnchor = true;
/**
@@ -111,7 +103,7 @@
}
/** Render the document AST to sanitized HTML. */
- public SanitizedContent toSoyHtml(RootNode node) {
+ public SanitizedContent toSoyHtml(Node node) {
if (node == null) {
return null;
}
@@ -122,41 +114,37 @@
}
@Override
- public void visit(RootNode node) {
- references.add(node);
+ public void visit(Document node) {
visitChildren(node);
}
- @Override
- public void visit(TocNode node) {
- toc.format();
- }
-
- @Override
- public void visit(DivNode node) {
- html.open("div").attribute("class", node.getStyleName());
- visitChildren(node);
+ private void visit(BlockNote node) {
+ html.open("div").attribute("class", node.getClassName());
+ Node f = node.getFirstChild();
+ if (f == node.getLastChild() && f instanceof Paragraph) {
+ // Avoid <p> inside <div> if there is only one <p>.
+ visitChildren(f);
+ } else {
+ visitChildren(node);
+ }
html.close("div");
}
- @Override
- public void visit(ColsNode node) {
+ private void visit(MultiColumnBlock node) {
html.open("div").attribute("class", "cols");
visitChildren(node);
html.close("div");
}
- @Override
- public void visit(ColsNode.Column node) {
- if (1 <= node.span && node.span <= ColsNode.GRID_WIDTH) {
+ private void visit(MultiColumnBlock.Column node) {
+ if (1 <= node.span && node.span <= MultiColumnBlock.GRID_WIDTH) {
html.open("div").attribute("class", "col-" + node.span);
visitChildren(node);
html.close("div");
}
}
- @Override
- public void visit(IframeNode node) {
+ private void visit(IframeBlock node) {
if (HtmlBuilder.isValidHttpUri(node.src)
&& HtmlBuilder.isValidCssDimension(node.height)
&& HtmlBuilder.isValidCssDimension(node.width)
@@ -172,7 +160,7 @@
}
}
- private boolean canRender(IframeNode node) {
+ private boolean canRender(IframeBlock node) {
String[] ok = cfg.getStringList("markdown", null, "allowiframe");
if (ok.length == 1 && StringUtils.toBooleanOrNull(ok[0]) == Boolean.TRUE) {
return true;
@@ -186,7 +174,7 @@
}
@Override
- public void visit(HeaderNode node) {
+ public void visit(Heading node) {
outputNamedAnchor = false;
String tag = "h" + node.getLevel();
html.open(tag);
@@ -205,58 +193,67 @@
outputNamedAnchor = true;
}
- @Override
- public void visit(NamedAnchorNode node) {
+ private void visit(NamedAnchor node) {
if (outputNamedAnchor) {
- html.open("a").attribute("name", node.name).close("a");
+ html.open("a").attribute("name", node.getName()).close("a");
}
}
@Override
- public void visit(ParaNode node) {
- wrapChildren("p", node);
+ public void visit(Paragraph node) {
+ if (isInTightList(node)) {
+ // Avoid unnecessary <p> tags within <ol><li> structures.
+ visitChildren(node);
+ } else {
+ wrapChildren("p", node);
+ }
+ }
+
+ private static boolean isInTightList(Paragraph c) {
+ Block b = c.getParent(); // b is probably a ListItem
+ if (b != null) {
+ Block a = b.getParent();
+ return a instanceof ListBlock && ((ListBlock) a).isTight();
+ }
+ return false;
}
@Override
- public void visit(BlockQuoteNode node) {
+ public void visit(BlockQuote node) {
wrapChildren("blockquote", node);
}
@Override
- public void visit(OrderedListNode node) {
- wrapChildren("ol", node);
+ public void visit(OrderedList node) {
+ html.open("ol");
+ if (node.getStartNumber() != 1) {
+ html.attribute("start", Integer.toString(node.getStartNumber()));
+ }
+ visitChildren(node);
+ html.close("ol");
}
@Override
- public void visit(BulletListNode node) {
+ public void visit(BulletList node) {
wrapChildren("ul", node);
}
@Override
- public void visit(ListItemNode node) {
+ public void visit(ListItem node) {
wrapChildren("li", node);
}
@Override
- public void visit(DefinitionListNode node) {
- wrapChildren("dl", node);
+ public void visit(FencedCodeBlock node) {
+ codeInPre(node.getInfo(), node.getLiteral());
}
@Override
- public void visit(DefinitionNode node) {
- wrapChildren("dd", node);
+ public void visit(IndentedCodeBlock node) {
+ codeInPre(null, node.getLiteral());
}
- @Override
- public void visit(DefinitionTermNode node) {
- wrapChildren("dt", node);
- }
-
- @Override
- public void visit(VerbatimNode node) {
- String lang = node.getType();
- String text = node.getText();
-
+ private void codeInPre(String lang, String text) {
html.open("pre").attribute("class", "code");
text = printLeadingBlankLines(text);
List<ParseResult> parsed = parse(lang, text);
@@ -308,67 +305,29 @@
}
@Override
- public void visit(CodeNode node) {
- wrapText("code", node);
+ public void visit(Code node) {
+ html.open("code").appendAndEscape(node.getLiteral()).close("code");
}
@Override
- public void visit(StrikeNode node) {
- wrapChildren("del", node);
+ public void visit(Emphasis node) {
+ wrapChildren("em", node);
}
@Override
- public void visit(StrongEmphSuperNode node) {
- if (node.isClosed()) {
- wrapChildren(node.isStrong() ? "strong" : "em", node);
- } else {
- // Unclosed (or unmatched) sequence is plain text.
- html.appendAndEscape(node.getChars());
- visitChildren(node);
- }
+ public void visit(StrongEmphasis node) {
+ wrapChildren("strong", node);
}
@Override
- public void visit(AutoLinkNode node) {
- String url = node.getText();
- html.open("a").attribute("href", href(url)).appendAndEscape(url).close("a");
- }
-
- @Override
- public void visit(MailLinkNode node) {
- String addr = node.getText();
- html.open("a").attribute("href", "mailto:" + addr).appendAndEscape(addr).close("a");
- }
-
- @Override
- public void visit(WikiLinkNode node) {
- String text = node.getText();
- String path = text.replace(' ', '-') + ".md";
- html.open("a").attribute("href", href(path)).appendAndEscape(text).close("a");
- }
-
- @Override
- public void visit(ExpLinkNode node) {
- html.open("a").attribute("href", href(node.url)).attribute("title", node.title);
+ public void visit(Link node) {
+ html.open("a")
+ .attribute("href", href(node.getDestination()))
+ .attribute("title", node.getTitle());
visitChildren(node);
html.close("a");
}
- @Override
- public void visit(RefLinkNode node) {
- ReferenceNode ref = references.get(node.referenceKey, getInnerText(node));
- if (ref != null) {
- html.open("a").attribute("href", href(ref.getUrl())).attribute("title", ref.getTitle());
- visitChildren(node);
- html.close("a");
- } else {
- // Treat a broken RefLink as plain text.
- html.appendAndEscape("[");
- visitChildren(node);
- html.appendAndEscape("]");
- }
- }
-
@VisibleForTesting
String href(String target) {
if (target.startsWith("#") || HtmlBuilder.isValidHttpUri(target)) {
@@ -422,28 +381,13 @@
}
@Override
- public void visit(ExpImageNode node) {
+ public void visit(Image node) {
html.open("img")
- .attribute("src", resolveImageUrl(node.url))
- .attribute("title", node.title)
+ .attribute("src", resolveImageUrl(node.getDestination()))
+ .attribute("title", node.getTitle())
.attribute("alt", getInnerText(node));
}
- @Override
- public void visit(RefImageNode node) {
- String alt = getInnerText(node);
- String url, title = alt;
- ReferenceNode ref = references.get(node.referenceKey, alt);
- if (ref != null) {
- url = resolveImageUrl(ref.getUrl());
- title = ref.getTitle();
- } else {
- // If reference is missing, insert a broken image.
- url = FilterImageDataUri.INSTANCE.getInnocuousOutput();
- }
- html.open("img").attribute("src", url).attribute("title", title).attribute("alt", alt);
- }
-
private String resolveImageUrl(String url) {
if (imageLoader == null
|| url.startsWith("https://")
@@ -454,166 +398,162 @@
return imageLoader.loadImage(url);
}
- @Override
- public void visit(TableNode node) {
- table = new TableState(node);
+ public void visit(TableBlock node) {
wrapChildren("table", node);
- table = null;
}
- private void mustBeInsideTable(Node node) {
- checkState(table != null, "%s must be in table", node);
- }
-
- @Override
- public void visit(TableHeaderNode node) {
- mustBeInsideTable(node);
- table.inHeader = true;
- wrapChildren("thead", node);
- table.inHeader = false;
- }
-
- @Override
- public void visit(TableBodyNode node) {
- wrapChildren("tbody", node);
- }
-
- @Override
- public void visit(TableCaptionNode node) {
- wrapChildren("caption", node);
- }
-
- @Override
- public void visit(TableRowNode node) {
- mustBeInsideTable(node);
- table.startRow();
+ private void visit(TableRow node) {
wrapChildren("tr", node);
}
- @Override
- public void visit(TableCellNode node) {
- mustBeInsideTable(node);
- String tag = table.inHeader ? "th" : "td";
- html.open(tag).attribute("align", table.getAlign());
- if (node.getColSpan() > 1) {
- html.attribute("colspan", Integer.toString(node.getColSpan()));
+ private void visit(TableCell cell) {
+ String tag = cell.isHeader() ? "th" : "td";
+ html.open(tag);
+ TableCell.Alignment alignment = cell.getAlignment();
+ if (alignment != null) {
+ html.attribute("align", toHtml(alignment));
}
- visitChildren(node);
+ visitChildren(cell);
html.close(tag);
- table.done(node);
}
- @Override
- public void visit(TableColumnNode node) {
- // Not for output; should not be in the Visitor API.
+ private static String toHtml(TableCell.Alignment alignment) {
+ switch (alignment) {
+ case LEFT:
+ return "left";
+ case CENTER:
+ return "center";
+ case RIGHT:
+ return "right";
+ default:
+ throw new IllegalArgumentException("unsupported alignment " + alignment);
+ }
}
- @Override
- public void visit(TextNode node) {
- html.appendAndEscape(node.getText());
- // TODO(sop) printWithAbbreviations
- }
-
- @Override
- public void visit(SpecialTextNode node) {
- html.appendAndEscape(node.getText());
- }
-
- @Override
- public void visit(QuotedNode node) {
+ private void visit(SmartQuoted node) {
switch (node.getType()) {
- case DoubleAngle:
- html.entity("«");
- visitChildren(node);
- html.entity("»");
- break;
- case Double:
+ case DOUBLE:
html.entity("“");
visitChildren(node);
html.entity("”");
break;
- case Single:
+ case SINGLE:
html.entity("‘");
visitChildren(node);
html.entity("’");
break;
default:
- checkState(false, "unsupported quote %s", node.getType());
+ throw new IllegalArgumentException("unsupported quote " + node.getType());
+ }
+ }
+
+ private static final Pattern PRETTY = Pattern.compile("('|[.]{3}|-{2,3})");
+
+ @Override
+ public void visit(Text node) {
+ String text = node.getLiteral();
+ Matcher pretty = PRETTY.matcher(text);
+ int i = 0;
+ while (pretty.find()) {
+ int s = pretty.start();
+ if (i < s) {
+ html.appendAndEscape(text.substring(i, s));
+ }
+ switch (pretty.group(0)) {
+ case "'":
+ html.entity("’");
+ break;
+ case "...":
+ html.entity("…");
+ break;
+ case "--":
+ html.entity("–");
+ break;
+ case "---":
+ html.entity("—");
+ break;
+ }
+ i = pretty.end();
+ }
+ if (i < text.length()) {
+ html.appendAndEscape(text.substring(i));
}
}
@Override
- public void visit(SimpleNode node) {
- switch (node.getType()) {
- case Apostrophe:
- html.entity("’");
- break;
- case Ellipsis:
- html.entity("…");
- break;
- case Emdash:
- html.entity("—");
- break;
- case Endash:
- html.entity("–");
- break;
- case HRule:
- html.open("hr");
- break;
- case Linebreak:
- html.open("br");
- break;
- case Nbsp:
- html.entity(" ");
- break;
- default:
- checkState(false, "unsupported node %s", node.getType());
- }
+ public void visit(SoftLineBreak node) {
+ html.space();
}
@Override
- public void visit(SuperNode node) {
- visitChildren(node);
+ public void visit(HardLineBreak node) {
+ html.open("br");
}
@Override
- public void visit(Node node) {
- checkState(false, "node %s unsupported", node.getClass());
+ public void visit(ThematicBreak thematicBreak) {
+ html.open("hr");
}
@Override
- public void visit(HtmlBlockNode node) {
- // Drop all HTML nodes.
+ public void visit(HtmlInline node) {
+ // Discard all HTML.
}
@Override
- public void visit(InlineHtmlNode node) {
- // Drop all HTML nodes.
+ public void visit(HtmlBlock node) {
+ // Discard all HTML.
}
- @Override
- public void visit(ReferenceNode node) {
- // Reference nodes are not printed; they only declare an item.
- }
-
- @Override
- public void visit(AbbreviationNode node) {
- // Abbreviation nodes are not printed; they only declare an item.
- }
-
- private void wrapText(String tag, TextNode node) {
- html.open(tag).appendAndEscape(node.getText()).close(tag);
- }
-
- private void wrapChildren(String tag, SuperNode node) {
+ private void wrapChildren(String tag, Node node) {
html.open(tag);
visitChildren(node);
html.close(tag);
}
private void visitChildren(Node node) {
- for (Node child : node.getChildren()) {
- child.accept(this);
+ for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
+ c.accept(this);
+ }
+ }
+
+ @Override
+ public void visit(CustomNode node) {
+ if (node instanceof NamedAnchor) {
+ visit((NamedAnchor) node);
+ } else if (node instanceof SmartQuoted) {
+ visit((SmartQuoted) node);
+ } else if (node instanceof Strikethrough) {
+ wrapChildren("del", node);
+ } else if (node instanceof TableBody) {
+ wrapChildren("tbody", node);
+ } else if (node instanceof TableCell) {
+ visit((TableCell) node);
+ } else if (node instanceof TableHead) {
+ wrapChildren("thead", node);
+ } else if (node instanceof TableRow) {
+ visit((TableRow) node);
+ } else {
+ throw new IllegalArgumentException("cannot render " + node.getClass());
+ }
+ }
+
+ @Override
+ public void visit(CustomBlock node) {
+ if (node instanceof BlockNote) {
+ visit((BlockNote) node);
+ } else if (node instanceof IframeBlock) {
+ visit((IframeBlock) node);
+ } else if (node instanceof MultiColumnBlock) {
+ visit((MultiColumnBlock) node);
+ } else if (node instanceof MultiColumnBlock.Column) {
+ visit((MultiColumnBlock.Column) node);
+ } else if (node instanceof TableBlock) {
+ visit((TableBlock) node);
+ } else if (node instanceof TocBlock) {
+ toc.format();
+ } else {
+ throw new IllegalArgumentException("cannot render " + node.getClass());
}
}
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownUtil.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownUtil.java
index 5130216..6392cd0 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownUtil.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownUtil.java
@@ -14,21 +14,17 @@
package com.google.gitiles.doc;
+import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
-import org.pegdown.ast.HeaderNode;
-import org.pegdown.ast.Node;
-import org.pegdown.ast.TextNode;
+import org.commonmark.node.Heading;
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
class MarkdownUtil {
- /** Check if anchor URL is like {@code /top.md}. */
- static boolean isAbsolutePathToMarkdown(String url) {
- return url.length() >= 5 && url.charAt(0) == '/' && url.charAt(1) != '/' && url.endsWith(".md");
- }
-
/** Combine child nodes as string; this must be escaped for HTML. */
static String getInnerText(Node node) {
- if (node == null || node.getChildren().isEmpty()) {
+ if (node == null || node.getFirstChild() == null) {
return null;
}
@@ -38,25 +34,25 @@
}
private static void appendTextFromChildren(StringBuilder b, Node node) {
- for (Node child : node.getChildren()) {
- if (child instanceof TextNode) {
- b.append(((TextNode) child).getText());
+ for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
+ if (c instanceof Text) {
+ b.append(((Text) c).getLiteral());
} else {
- appendTextFromChildren(b, child);
+ appendTextFromChildren(b, c);
}
}
}
static String getTitle(Node node) {
- if (node instanceof HeaderNode) {
- if (((HeaderNode) node).getLevel() == 1) {
+ if (node instanceof Heading) {
+ if (((Heading) node).getLevel() == 1) {
return getInnerText(node);
}
return null;
}
- for (Node child : node.getChildren()) {
- String title = getTitle(child);
+ for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
+ String title = getTitle(c);
if (title != null) {
return title;
}
@@ -64,5 +60,14 @@
return null;
}
+ static void trimPreviousWhitespace(Node node) {
+ Node prev = node.getPrevious();
+ if (prev instanceof Text) {
+ Text prevText = (Text) prev;
+ String s = prevText.getLiteral();
+ prevText.setLiteral(CharMatcher.whitespace().trimTrailingFrom(s));
+ }
+ }
+
private MarkdownUtil() {}
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnBlock.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnBlock.java
new file mode 100644
index 0000000..3651474
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnBlock.java
@@ -0,0 +1,35 @@
+// 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 org.commonmark.node.CustomBlock;
+import org.commonmark.node.Heading;
+
+/**
+ * Multi-column layout delineated by {@code |||---|||}.
+ * <p>
+ * Each {@link Heading} or {@link BlockNote} within the layout begins a new
+ * {@link Column} in the HTML.
+ */
+public class MultiColumnBlock extends CustomBlock {
+ /** Grid is 12 columns wide. */
+ public static final int GRID_WIDTH = 12;
+
+ /** Column within a {@link MultiColumnBlock}. */
+ public static class Column extends CustomBlock {
+ int span;
+ boolean empty;
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnExtension.java
new file mode 100644
index 0000000..ef53fe3
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MultiColumnExtension.java
@@ -0,0 +1,172 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// 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.Splitter;
+import com.google.common.primitives.Ints;
+import com.google.gitiles.doc.MultiColumnBlock.Column;
+
+import org.commonmark.Extension;
+import org.commonmark.node.Block;
+import org.commonmark.node.Heading;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.commonmark.parser.Parser.ParserExtension;
+import org.commonmark.parser.block.AbstractBlockParser;
+import org.commonmark.parser.block.AbstractBlockParserFactory;
+import org.commonmark.parser.block.BlockContinue;
+import org.commonmark.parser.block.BlockStart;
+import org.commonmark.parser.block.MatchedBlockParser;
+import org.commonmark.parser.block.ParserState;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** CommonMark extension for multicolumn layouts. */
+public class MultiColumnExtension implements ParserExtension {
+ private static final String MARKER = "|||---|||";
+
+ public static Extension create() {
+ return new MultiColumnExtension();
+ }
+
+ private MultiColumnExtension() {}
+
+ @Override
+ public void extend(Parser.Builder builder) {
+ builder.customBlockParserFactory(new DivParserFactory());
+ }
+
+ private static class MultiColumnParser extends AbstractBlockParser {
+ private final MultiColumnBlock block = new MultiColumnBlock();
+ private final List<Column> cols;
+ private boolean done;
+
+ MultiColumnParser(String layout) {
+ List<String> specList = Splitter.on(',').trimResults().splitToList(layout);
+ cols = new ArrayList<>(specList.size());
+ for (String spec : specList) {
+ cols.add(parseColumn(spec));
+ }
+ }
+
+ private MultiColumnBlock.Column parseColumn(String spec) {
+ MultiColumnBlock.Column col = new MultiColumnBlock.Column();
+ if (spec.startsWith(":")) {
+ col.empty = true;
+ spec = spec.substring(1);
+ }
+
+ Integer width = Ints.tryParse(spec, 10);
+ if (width != null) {
+ col.span = width;
+ }
+ return col;
+ }
+
+ @Override
+ public Block getBlock() {
+ return block;
+ }
+
+ @Override
+ public BlockContinue tryContinue(ParserState state) {
+ if (done) {
+ return BlockContinue.none();
+ }
+ if (state.getIndent() == 0) {
+ int s = state.getNextNonSpaceIndex();
+ CharSequence line = state.getLine();
+ if (MARKER.contentEquals(line.subSequence(s, line.length()))) {
+ done = true;
+ return BlockContinue.atIndex(line.length());
+ }
+ }
+ return BlockContinue.atIndex(state.getIndex());
+ }
+
+ @Override
+ public void closeBlock() {
+ splitChildren();
+ rebalanceSpans();
+
+ for (MultiColumnBlock.Column c : cols) {
+ block.appendChild(c);
+ }
+ }
+
+ private void splitChildren() {
+ int colIdx = 0;
+ Column col = null;
+ Node next = null;
+
+ for (Node child = block.getFirstChild(); child != null; child = next) {
+ if (col == null || child instanceof Heading || child instanceof BlockNote) {
+ for (; ; ) {
+ if (colIdx == cols.size()) {
+ cols.add(new Column());
+ }
+ col = cols.get(colIdx++);
+ if (!col.empty) {
+ break;
+ }
+ }
+ }
+ next = child.getNext();
+ col.appendChild(child);
+ }
+ }
+
+ private void rebalanceSpans() {
+ int remaining = MultiColumnBlock.GRID_WIDTH;
+ for (int i = 0; i < cols.size(); i++) {
+ Column col = cols.get(i);
+ if (col.span <= 0 || col.span > MultiColumnBlock.GRID_WIDTH) {
+ col.span = remaining / (cols.size() - i);
+ }
+ remaining = Math.max(0, remaining - col.span);
+ }
+ }
+
+ @Override
+ public boolean isContainer() {
+ return true;
+ }
+
+ @Override
+ public boolean canContain(Block block) {
+ return !(block instanceof MultiColumnBlock);
+ }
+ }
+
+ private static class DivParserFactory extends AbstractBlockParserFactory {
+ @Override
+ public BlockStart tryStart(ParserState state, MatchedBlockParser matched) {
+ if (state.getIndent() > 0) {
+ return BlockStart.none();
+ }
+
+ int s = state.getNextNonSpaceIndex();
+ CharSequence line = state.getLine();
+ CharSequence text = line.subSequence(s, line.length());
+ if (text.length() >= MARKER.length()
+ && MARKER.contentEquals(text.subSequence(0, MARKER.length()))) {
+ String layout = text.subSequence(MARKER.length(), line.length()).toString().trim();
+ return BlockStart.of(new MultiColumnParser(layout)).atIndex(line.length());
+ }
+ return BlockStart.none();
+ }
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/NamedAnchor.java
similarity index 61%
copy from gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
copy to gitiles-servlet/src/main/java/com/google/gitiles/doc/NamedAnchor.java
index 6c9f2f6..5e93c55 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/NamedAnchor.java
@@ -4,7 +4,7 @@
// 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
+// 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,
@@ -14,16 +14,18 @@
package com.google.gitiles.doc;
-public interface Visitor extends org.pegdown.ast.Visitor {
- void visit(ColsNode node);
+import org.commonmark.node.CustomNode;
+import org.commonmark.node.Heading;
- void visit(ColsNode.Column node);
+/** A {@code <a name="...">} tag, usually inside a {@link Heading}. */
+public class NamedAnchor extends CustomNode {
+ private String name;
- void visit(DivNode node);
+ public String getName() {
+ return name;
+ }
- void visit(IframeNode node);
-
- void visit(TocNode node);
-
- void visit(NamedAnchorNode node);
+ public void setName(String name) {
+ this.name = name;
+ }
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/NamedAnchorExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/NamedAnchorExtension.java
new file mode 100644
index 0000000..b72b202
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/NamedAnchorExtension.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// 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 org.commonmark.Extension;
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
+import org.commonmark.parser.DelimiterProcessor;
+import org.commonmark.parser.Parser;
+import org.commonmark.parser.Parser.ParserExtension;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Parses <code>{#foo}</code> into {@link NamedAnchor}. */
+public class NamedAnchorExtension implements ParserExtension {
+ public static Extension create() {
+ return new NamedAnchorExtension();
+ }
+
+ private NamedAnchorExtension() {}
+
+ @Override
+ public void extend(Parser.Builder builder) {
+ builder.customDelimiterProcessor(new Processor());
+ }
+
+ private static class Processor implements DelimiterProcessor {
+ private static final Pattern ID = Pattern.compile("#([^\\s}]+)");
+
+ @Override
+ public char getOpeningDelimiterChar() {
+ return '{';
+ }
+
+ @Override
+ public char getClosingDelimiterChar() {
+ return '}';
+ }
+
+ @Override
+ public int getMinDelimiterCount() {
+ return 1;
+ }
+
+ @Override
+ public int getDelimiterUse(int openerCount, int closerCount) {
+ return 1;
+ }
+
+ @Override
+ public void process(Text opener, Text closer, int delimiterUse) {
+ Node content = opener.getNext();
+ if (content instanceof Text && content.getNext() == closer) {
+ Matcher m = ID.matcher(((Text) content).getLiteral());
+ if (m.matches()) {
+ content.unlink();
+
+ NamedAnchor anchor = new NamedAnchor();
+ anchor.setName(m.group(1));
+ opener.insertAfter(anchor);
+ MarkdownUtil.trimPreviousWhitespace(opener);
+ return;
+ }
+ }
+
+ // If its not exactly one well formed Text node; restore the delimiter text.
+ opener.insertAfter(new Text("{"));
+ closer.insertBefore(new Text("}"));
+ }
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/NamedAnchorNode.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/NamedAnchorNode.java
deleted file mode 100644
index d5c4f8c..0000000
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/NamedAnchorNode.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// 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 org.pegdown.ast.AbstractNode;
-import org.pegdown.ast.Node;
-
-import java.util.Collections;
-import java.util.List;
-
-class NamedAnchorNode extends AbstractNode {
- final String name;
-
- NamedAnchorNode(String name) {
- this.name = name;
- }
-
- @Override
- public void accept(org.pegdown.ast.Visitor visitor) {
- ((Visitor) visitor).visit(this);
- }
-
- @Override
- public List<Node> getChildren() {
- return Collections.emptyList();
- }
-}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Navbar.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/Navbar.java
index aa8e0eb..c1ed246 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Navbar.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/Navbar.java
@@ -14,22 +14,24 @@
package com.google.gitiles.doc;
-import com.google.gitiles.GitilesView;
import com.google.gitiles.doc.html.HtmlBuilder;
import com.google.template.soy.shared.restricted.EscapingConventions.FilterImageDataUri;
import com.google.template.soy.shared.restricted.Sanitizers;
-import org.pegdown.ast.HeaderNode;
-import org.pegdown.ast.Node;
-import org.pegdown.ast.ReferenceNode;
-import org.pegdown.ast.RootNode;
+import org.commonmark.node.Heading;
+import org.commonmark.node.Node;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
class Navbar {
- static Map<String, Object> bannerSoyData(GitilesView view, ImageLoader img, RootNode nav) {
+ private static final Pattern REF_LINK =
+ Pattern.compile("^\\[(logo|home)\\]:\\s*(.+)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
+
+ static Map<String, Object> bannerSoyData(
+ ImageLoader img, MarkdownToHtml toHtml, String navMarkdown, Node nav) {
Map<String, Object> data = new HashMap<>();
data.put("siteTitle", null);
data.put("logoUrl", null);
@@ -39,42 +41,50 @@
return data;
}
- for (Iterator<Node> i = nav.getChildren().iterator(); i.hasNext(); ) {
- Node n = i.next();
- if (n instanceof HeaderNode) {
- HeaderNode h = (HeaderNode) n;
+ for (Node c = nav.getFirstChild(); c != null; c = c.getNext()) {
+ if (c instanceof Heading) {
+ Heading h = (Heading) c;
if (h.getLevel() == 1) {
data.put("siteTitle", MarkdownUtil.getInnerText(h));
- i.remove();
+ h.unlink();
break;
}
}
}
- for (ReferenceNode r : nav.getReferences()) {
- String key = MarkdownUtil.getInnerText(r);
- String url = r.getUrl();
- if ("logo".equalsIgnoreCase(key)) {
- Object src;
- if (HtmlBuilder.isValidHttpUri(url)) {
- src = url;
- } else if (HtmlBuilder.isImageDataUri(url)) {
- src = Sanitizers.filterImageDataUri(url);
- } else if (img != null) {
- src = Sanitizers.filterImageDataUri(img.loadImage(url));
- } else {
- src = FilterImageDataUri.INSTANCE.getInnocuousOutput();
- }
- data.put("logoUrl", src);
- } else if ("home".equalsIgnoreCase(key)) {
- if (MarkdownUtil.isAbsolutePathToMarkdown(url)) {
- url = GitilesView.doc().copyFrom(view).setPathPart(url).toUrl();
- }
- data.put("homeUrl", url);
+ Matcher m = REF_LINK.matcher(navMarkdown);
+ while (m.find()) {
+ String key = m.group(1).toLowerCase();
+ String url = m.group(2).trim();
+ switch (key) {
+ case "logo":
+ data.put("logoUrl", toImgSrc(img, url));
+ break;
+
+ case "home":
+ data.put("homeUrl", toHtml.href(url));
+ break;
}
}
+
return data;
}
+ private static Object toImgSrc(ImageLoader img, String url) {
+ if (HtmlBuilder.isValidHttpUri(url)) {
+ return url;
+ }
+
+ if (HtmlBuilder.isImageDataUri(url)) {
+ return Sanitizers.filterImageDataUri(url);
+ }
+
+ if (img != null) {
+ return Sanitizers.filterImageDataUri(img.loadImage(url));
+ }
+
+ return FilterImageDataUri.INSTANCE.getInnocuousOutput();
+ }
+
private Navbar() {}
}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/ReferenceMap.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/ReferenceMap.java
deleted file mode 100644
index 34800a8..0000000
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/ReferenceMap.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// 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 static com.google.gitiles.doc.MarkdownUtil.getInnerText;
-
-import org.pegdown.ast.ReferenceNode;
-import org.pegdown.ast.RootNode;
-import org.pegdown.ast.SuperNode;
-
-import java.util.HashMap;
-import java.util.Map;
-
-class ReferenceMap {
- private final Map<String, ReferenceNode> references = new HashMap<>();
-
- void add(RootNode node) {
- for (ReferenceNode ref : node.getReferences()) {
- String id = getInnerText(ref);
- references.put(key(id), ref);
- }
- }
-
- ReferenceNode get(SuperNode keyNode, String text) {
- String id = keyNode != null ? getInnerText(keyNode) : text;
- if (id == null || id.isEmpty()) {
- return null;
- }
- return references.get(key(id));
- }
-
- private static String key(String in) {
- // Strip whitespace and normalize to lower case. Pegdown's default
- // HTML formatter also applies this type of normalization to make
- // it easier for document authors to reference links. Links should
- // be case insensitive to allow for easier formatting of title case
- // in prose vs. in the reference table, especially if a link is used
- // both at the start of a sentence and later in the middle of sentence.
- //
- // Whitespace stripping is also performed by pegdown's default code.
- // This allows references to to be declared as "foobar" but prose to
- // mention it as "Foo Bar".
- StringBuilder r = new StringBuilder(in.length());
- for (int i = 0; i < in.length(); i++) {
- char c = in.charAt(i);
- if (!Character.isWhitespace(c)) {
- r.append(Character.toLowerCase(c));
- }
- }
- return r.toString();
- }
-}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/SmartQuoted.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/SmartQuoted.java
new file mode 100644
index 0000000..001b830
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/SmartQuoted.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// 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 org.commonmark.node.CustomNode;
+
+/** SmartQuotes around text. */
+public class SmartQuoted extends CustomNode {
+ public enum Type {
+ DOUBLE,
+ SINGLE;
+ }
+
+ private Type type;
+
+ public Type getType() {
+ return type;
+ }
+
+ public void setType(Type type) {
+ this.type = type;
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/SmartQuotedExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/SmartQuotedExtension.java
new file mode 100644
index 0000000..92c271e
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/SmartQuotedExtension.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// 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 static com.google.gitiles.doc.SmartQuoted.Type.*;
+
+import com.google.gitiles.doc.SmartQuoted.Type;
+
+import org.commonmark.Extension;
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
+import org.commonmark.parser.DelimiterProcessor;
+import org.commonmark.parser.Parser;
+import org.commonmark.parser.Parser.ParserExtension;
+
+/** Uses smart quotes for ' and ". */
+public class SmartQuotedExtension implements ParserExtension {
+ public static Extension create() {
+ return new SmartQuotedExtension();
+ }
+
+ private SmartQuotedExtension() {}
+
+ @Override
+ public void extend(Parser.Builder builder) {
+ builder.customDelimiterProcessor(new QuotedProcessor(SINGLE, '\''));
+ builder.customDelimiterProcessor(new QuotedProcessor(DOUBLE, '"'));
+ }
+
+ private static void quote(Type type, Text opener, Text closer) {
+ SmartQuoted quote = new SmartQuoted();
+ quote.setType(type);
+ for (Node t = opener.getNext(); t != null && t != closer; ) {
+ Node next = t.getNext();
+ quote.appendChild(t);
+ t = next;
+ }
+ opener.insertAfter(quote);
+ }
+
+ /** Parses single and double quoted strings for smart quotes. */
+ private static class QuotedProcessor implements DelimiterProcessor {
+ private final SmartQuoted.Type type;
+ private final char delim;
+
+ QuotedProcessor(SmartQuoted.Type type, char open) {
+ this.type = type;
+ this.delim = open;
+ }
+
+ @Override
+ public char getOpeningDelimiterChar() {
+ return delim;
+ }
+
+ @Override
+ public char getClosingDelimiterChar() {
+ return delim;
+ }
+
+ @Override
+ public int getMinDelimiterCount() {
+ return 1;
+ }
+
+ @Override
+ public int getDelimiterUse(int openerCount, int closerCount) {
+ return 1;
+ }
+
+ @Override
+ public void process(Text opener, Text closer, int delimiterUse) {
+ quote(type, opener, closer);
+ }
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/TableState.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/TableState.java
deleted file mode 100644
index 396a86b..0000000
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/TableState.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// 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 org.pegdown.ast.TableCellNode;
-import org.pegdown.ast.TableColumnNode;
-import org.pegdown.ast.TableNode;
-
-import java.util.List;
-
-class TableState {
- private final List<TableColumnNode> columns;
-
- boolean inHeader;
- int column;
-
- TableState(TableNode node) {
- columns = node.getColumns();
- }
-
- void startRow() {
- column = 0;
- }
-
- String getAlign() {
- int pos = Math.min(column, columns.size() - 1);
- TableColumnNode c = columns.get(pos);
- switch (c.getAlignment()) {
- case None:
- return null;
- case Left:
- return "left";
- case Right:
- return "right";
- case Center:
- return "center";
- default:
- throw new IllegalStateException(
- String.format("unsupported alignment %s on column %d", c.getAlignment(), pos));
- }
- }
-
- void done(TableCellNode cell) {
- column += cell.getColSpan();
- }
-}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocBlock.java
similarity index 71%
copy from gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
copy to gitiles-servlet/src/main/java/com/google/gitiles/doc/TocBlock.java
index 6c9f2f6..6a52720 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocBlock.java
@@ -14,16 +14,7 @@
package com.google.gitiles.doc;
-public interface Visitor extends org.pegdown.ast.Visitor {
- void visit(ColsNode node);
+import org.commonmark.node.CustomBlock;
- void visit(ColsNode.Column node);
-
- void visit(DivNode node);
-
- void visit(IframeNode node);
-
- void visit(TocNode node);
-
- void visit(NamedAnchorNode node);
-}
+/** Block node {@code [TOC]} to display table of contents. */
+public class TocBlock extends CustomBlock {}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocExtension.java
new file mode 100644
index 0000000..34db0cf
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocExtension.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// 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 org.commonmark.Extension;
+import org.commonmark.node.Block;
+import org.commonmark.parser.Parser;
+import org.commonmark.parser.Parser.ParserExtension;
+import org.commonmark.parser.block.AbstractBlockParser;
+import org.commonmark.parser.block.AbstractBlockParserFactory;
+import org.commonmark.parser.block.BlockContinue;
+import org.commonmark.parser.block.BlockStart;
+import org.commonmark.parser.block.MatchedBlockParser;
+import org.commonmark.parser.block.ParserState;
+
+/** CommonMark extension for {@code [TOC]}. */
+public class TocExtension implements ParserExtension {
+ public static Extension create() {
+ return new TocExtension();
+ }
+
+ private TocExtension() {}
+
+ @Override
+ public void extend(Parser.Builder builder) {
+ builder.customBlockParserFactory(new TocParserFactory());
+ }
+
+ private static class TocParser extends AbstractBlockParser {
+ private final TocBlock block = new TocBlock();
+
+ @Override
+ public Block getBlock() {
+ return block;
+ }
+
+ @Override
+ public BlockContinue tryContinue(ParserState parserState) {
+ return BlockContinue.none();
+ }
+ }
+
+ private static class TocParserFactory extends AbstractBlockParserFactory {
+ @Override
+ public BlockStart tryStart(ParserState state, MatchedBlockParser matched) {
+ if (state.getIndent() == 0) {
+ CharSequence line = state.getLine();
+ int s = state.getNextNonSpaceIndex();
+ if ("[TOC]".contentEquals(line.subSequence(s, line.length()))) {
+ return BlockStart.of(new TocParser()).atIndex(line.length());
+ }
+ }
+ return BlockStart.none();
+ }
+ }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocFormatter.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocFormatter.java
index cb82f54..9c415a8 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocFormatter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocFormatter.java
@@ -21,9 +21,8 @@
import com.google.gitiles.doc.html.HtmlBuilder;
import org.apache.commons.lang3.StringUtils;
-import org.pegdown.ast.HeaderNode;
-import org.pegdown.ast.Node;
-import org.pegdown.ast.RootNode;
+import org.commonmark.node.Heading;
+import org.commonmark.node.Node;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -38,8 +37,8 @@
private final int maxLevel;
private int countH1;
- private List<HeaderNode> outline;
- private Map<HeaderNode, String> ids;
+ private List<Heading> outline;
+ private Map<Heading, String> ids;
private int level;
@@ -48,21 +47,21 @@
this.maxLevel = maxLevel;
}
- void setRoot(RootNode doc) {
+ void setRoot(Node doc) {
outline = new ArrayList<>();
Multimap<String, TocEntry> entries = ArrayListMultimap.create(16, 4);
- scan(doc, entries, new ArrayDeque<HeaderNode>());
+ scan(doc, entries, new ArrayDeque<Heading>());
ids = generateIds(entries);
}
- private boolean include(HeaderNode h) {
+ private boolean include(Heading h) {
if (h.getLevel() == 1) {
return countH1 > 1;
}
return h.getLevel() <= maxLevel;
}
- String idFromHeader(HeaderNode header) {
+ String idFromHeader(Heading header) {
return ids.get(header);
}
@@ -79,7 +78,7 @@
.open("div")
.attribute("class", "toc-aux")
.open("ul");
- for (HeaderNode header : outline) {
+ for (Heading header : outline) {
outline(header);
}
while (level >= startLevel) {
@@ -89,7 +88,7 @@
html.close("div").close("div");
}
- private void outline(HeaderNode h) {
+ private void outline(Heading h) {
if (!include(h)) {
return;
}
@@ -116,18 +115,17 @@
.close("li");
}
- private void scan(Node node, Multimap<String, TocEntry> entries, Deque<HeaderNode> stack) {
- if (node instanceof HeaderNode) {
- scan((HeaderNode) node, entries, stack);
+ private void scan(Node node, Multimap<String, TocEntry> entries, Deque<Heading> stack) {
+ if (node instanceof Heading) {
+ scan((Heading) node, entries, stack);
} else {
- for (Node child : node.getChildren()) {
- scan(child, entries, stack);
+ for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
+ scan(c, entries, stack);
}
}
}
- private void scan(
- HeaderNode header, Multimap<String, TocEntry> entries, Deque<HeaderNode> stack) {
+ private void scan(Heading header, Multimap<String, TocEntry> entries, Deque<Heading> stack) {
if (header.getLevel() == 1) {
countH1++;
}
@@ -135,9 +133,9 @@
stack.removeLast();
}
- NamedAnchorNode node = findAnchor(header);
+ NamedAnchor node = findAnchor(header);
if (node != null) {
- entries.put(node.name, new TocEntry(stack, header, false, node.name));
+ entries.put(node.getName(), new TocEntry(stack, header, false, node.getName()));
stack.add(header);
outline.add(header);
return;
@@ -152,12 +150,12 @@
}
}
- private static NamedAnchorNode findAnchor(Node node) {
- for (Node child : node.getChildren()) {
- if (child instanceof NamedAnchorNode) {
- return (NamedAnchorNode) child;
+ private static NamedAnchor findAnchor(Node node) {
+ for (Node c = node.getFirstChild(); c != null; c = c.getNext()) {
+ if (c instanceof NamedAnchor) {
+ return (NamedAnchor) c;
}
- NamedAnchorNode anchor = findAnchor(child);
+ NamedAnchor anchor = findAnchor(c);
if (anchor != null) {
return anchor;
}
@@ -165,7 +163,7 @@
return null;
}
- private Map<HeaderNode, String> generateIds(Multimap<String, TocEntry> entries) {
+ private Map<Heading, String> generateIds(Multimap<String, TocEntry> entries) {
Multimap<String, TocEntry> tmp = ArrayListMultimap.create(entries.size(), 2);
for (Collection<TocEntry> headers : entries.asMap().values()) {
if (headers.size() == 1) {
@@ -182,7 +180,7 @@
}
StringBuilder b = new StringBuilder();
- for (HeaderNode p : entry.path) {
+ for (Heading p : entry.path) {
if (p.getLevel() > 1 || countH1 > 1) {
String text = MarkdownUtil.getInnerText(p);
if (text != null) {
@@ -196,7 +194,7 @@
}
}
- Map<HeaderNode, String> ids = Maps.newHashMapWithExpectedSize(tmp.size());
+ Map<Heading, String> ids = Maps.newHashMapWithExpectedSize(tmp.size());
for (Collection<TocEntry> headers : tmp.asMap().values()) {
if (headers.size() == 1) {
TocEntry entry = Iterables.getOnlyElement(headers);
@@ -212,13 +210,13 @@
}
private static class TocEntry {
- final HeaderNode[] path;
- final HeaderNode header;
+ final Heading[] path;
+ final Heading header;
final boolean generated;
String id;
- TocEntry(Deque<HeaderNode> stack, HeaderNode header, boolean generated, String id) {
- this.path = stack.toArray(new HeaderNode[stack.size()]);
+ TocEntry(Deque<Heading> stack, Heading header, boolean generated, String id) {
+ this.path = stack.toArray(new Heading[stack.size()]);
this.header = header;
this.generated = generated;
this.id = id;
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocNode.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocNode.java
deleted file mode 100644
index 33dd31c..0000000
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/TocNode.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// 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 org.pegdown.ast.AbstractNode;
-import org.pegdown.ast.Node;
-
-import java.util.Collections;
-import java.util.List;
-
-/** Block node {@code [TOC]} to display table of contents. */
-public class TocNode extends AbstractNode {
- @Override
- public void accept(org.pegdown.ast.Visitor visitor) {
- ((Visitor) visitor).visit(this);
- }
-
- @Override
- public List<Node> getChildren() {
- return Collections.emptyList();
- }
-}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
index 89611da..9639176 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
@@ -138,6 +138,8 @@
// allow
} else if ("name".equals(att) && "a".equals(tag)) {
// allow
+ } else if ("start".equals(att) && "ol".equals(tag)) {
+ // allow
} else if (("colspan".equals(att) || "align".equals(att))
&& ("td".equals(tag) || "th".equals(tag))) {
// allow
@@ -205,6 +207,13 @@
}
}
+ /** Append a space outside of an element. */
+ public HtmlBuilder space() {
+ finishActiveTag();
+ htmlBuf.append(' ');
+ return this;
+ }
+
private static final Pattern HTML_ENTITY = Pattern.compile("&[a-z]+;");
/** Append constant entity reference like {@code }. */
diff --git a/lib/BUCK b/lib/BUCK
index 2e4205f..a7f0ee2 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -41,34 +41,44 @@
)
maven_jar(
- name = 'pegdown',
- id = 'org.pegdown:pegdown:1.4.2',
- sha1 = 'd96db502ed832df867ff5d918f05b51ba3879ea7',
- license = 'Apache2.0',
- deps = [':grappa'],
+ name = 'commonmark',
+ id = 'com.atlassian.commonmark:commonmark:0.5.0',
+ sha1 = '16a2927ba798d41f6d569b22c2639b27cf0288ae',
+ license = 'commonmark',
)
maven_jar(
- name = 'grappa',
- id = 'com.github.parboiled1:grappa:1.0.4',
- sha1 = 'ad4b44b9c305dad7aa1e680d4b5c8eec9c4fd6f5',
- license = 'Apache2.0',
+ name = 'cm-autolink',
+ id = 'com.atlassian.commonmark:commonmark-ext-autolink:0.5.0',
+ sha1 = '7bd5e683b32a1ba7849a355472409a0cfaf86176',
+ license = 'commonmark',
deps = [
- ':guava',
- ':jitescript',
- '//lib/ow2:ow2-asm',
- '//lib/ow2:ow2-asm-analysis',
- '//lib/ow2:ow2-asm-tree',
- '//lib/ow2:ow2-asm-util',
+ ':commonmark',
+ ':autolink',
],
)
maven_jar(
- name = 'jitescript',
- id = 'me.qmx.jitescript:jitescript:0.4.0',
- sha1 = '2e35862b0435c1b027a21f3d6eecbe50e6e08d54',
- license = 'Apache2.0',
- visibility = ['//lib:grappa'],
+ name = 'autolink',
+ id = 'org.nibor.autolink:autolink:0.4.0',
+ sha1 = '764f7b0147a0675d971a34282dce9ec76b8307c9',
+ license = 'autolink',
+)
+
+maven_jar(
+ name = 'gfm-strikethrough',
+ id = 'com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:0.5.0',
+ sha1 = 'dd5679fdaae45a9f60250feb075a5b062719625e',
+ license = 'commonmark',
+ deps = [':commonmark'],
+)
+
+maven_jar(
+ name = 'gfm-tables',
+ id = 'com.atlassian.commonmark:commonmark-ext-gfm-tables:0.5.0',
+ sha1 = '082b177b9c886f4cad2c60374783081b04135528',
+ license = 'commonmark',
+ deps = [':commonmark'],
)
maven_jar(
diff --git a/lib/ow2/BUCK b/lib/ow2/BUCK
deleted file mode 100644
index c4339ce..0000000
--- a/lib/ow2/BUCK
+++ /dev/null
@@ -1,29 +0,0 @@
-VERSION = '5.0.3'
-
-maven_jar(
- name = 'ow2-asm',
- id = 'org.ow2.asm:asm:' + VERSION,
- sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa',
- license = 'ow2',
-)
-
-maven_jar(
- name = 'ow2-asm-analysis',
- id = 'org.ow2.asm:asm-analysis:' + VERSION,
- sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3',
- license = 'ow2',
-)
-
-maven_jar(
- name = 'ow2-asm-tree',
- id = 'org.ow2.asm:asm-tree:' + VERSION,
- sha1 = '287749b48ba7162fb67c93a026d690b29f410bed',
- license = 'ow2',
-)
-
-maven_jar(
- name = 'ow2-asm-util',
- id = 'org.ow2.asm:asm-util:' + VERSION,
- sha1 = '1512e5571325854b05fb1efce1db75fcced54389',
- license = 'ow2',
-)