Markdown: support column spans on |||---|||
Change-Id: Ia960a3fa9c01cccc23c4e12baf0fef99c6dea84a
diff --git a/Documentation/markdown.md b/Documentation/markdown.md
index 3a532c2..7ee70d9 100644
--- a/Documentation/markdown.md
+++ b/Documentation/markdown.md
@@ -458,8 +458,9 @@
### Column layout
-Gitiles markdown includes support for up to 4 columns of text within
-the width of the page.
+Gitiles markdown includes support for up to 12 columns of text across
+the width of the page. By default space is divided equally between
+the columns.
|||---|||
#### Columns
@@ -500,6 +501,31 @@
|||---|||
```
+Column spans can be specified on the first line as a comma separated
+list. In the example below the first column is 4 wide or 4/12ths of
+the page width, the second is 2 wide (or 2/12ths) and the final column
+is 6 wide (6/12ths or 50%) of the page.
+
+```
+|||---||| 4,2,6
+```
+
+An empty column can be inserted by prefixing its width with `:`,
+for example shifting content onto the right by padding 6 columns
+on the left:
+
+```
+|||---||| :6,3
+# Right
+|||---|||
+```
+
+renders as:
+
+|||---||| :6,3
+# Right
+|||---|||
+
### HTML IFrame
Although HTML is stripped the parser has special support for a limited
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
index 00945ef..7c31b0f 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/ColsNode.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/ColsNode.java
@@ -14,9 +14,11 @@
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;
/**
@@ -25,12 +27,74 @@
* Each header within the layout creates a new column in the HTML.
*/
public class ColsNode extends SuperNode {
- ColsNode(List<Node> children) {
- super(children);
+ 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/GitilesMarkdown.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java
index 5494cd3..9683472 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
@@ -17,7 +17,9 @@
import com.google.gitiles.GitilesView;
import org.parboiled.Rule;
+import org.parboiled.common.Factory;
import org.parboiled.support.StringBuilderVar;
+import org.parboiled.support.Var;
import org.pegdown.Parser;
import org.pegdown.ParsingTimeoutException;
import org.pegdown.PegDownProcessor;
@@ -29,6 +31,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
import java.util.List;
/** Parses Gitiles extensions to markdown. */
@@ -144,21 +147,50 @@
sequence(string("aside"), push(match())));
}
+ @SuppressWarnings("unchecked")
public Rule cols() {
StringBuilderVar body = new StringBuilderVar();
return NodeSequence(
- colsTag(), Newline(),
+ colsTag(), columnWidths(), Newline(),
oneOrMore(
testNot(colsTag(), Newline()),
Line(body)),
colsTag(), Newline(),
- push(new ColsNode(parse(body))));
+ 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
@@ -168,4 +200,16 @@
}
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<>();
+ }
+ });
+ }
+ }
}
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 62bc846..dd73ee4 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
@@ -119,21 +119,17 @@
@Override
public void visit(ColsNode node) {
html.open("div").attribute("class", "cols");
- boolean open = false;
- for (Node n : node.getChildren()) {
- if (n instanceof HeaderNode || n instanceof DivNode) {
- if (open) {
- html.close("div");
- }
- html.open("div").attribute("class", "col-3");
- open = true;
- }
- n.accept(this);
- }
- if (open) {
+ visitChildren(node);
+ html.close("div");
+ }
+
+ @Override
+ public void visit(ColsNode.Column node) {
+ if (1 <= node.span && node.span <= ColsNode.GRID_WIDTH) {
+ html.open("div").attribute("class", "col-" + node.span);
+ visitChildren(node);
html.close("div");
}
- html.close("div");
}
@Override
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
index e91e073..d8f269c 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/Visitor.java
@@ -16,6 +16,7 @@
public interface Visitor extends org.pegdown.ast.Visitor {
void visit(ColsNode node);
+ void visit(ColsNode.Column node);
void visit(DivNode node);
void visit(IframeNode node);
void visit(TocNode node);
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/static/doc.css b/gitiles-servlet/src/main/resources/com/google/gitiles/static/doc.css
index 24b9b79..c397424 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/static/doc.css
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/static/doc.css
@@ -282,11 +282,23 @@
margin: 0 -1.533%;
width: 103.067%;
}
-.col-3 {
+.col-1, .col-2, .col-3, .col-4, .col-5, .col-6,
+.col-7, .col-8, .col-9, .col-10, .col-11, .col-12 {
float: left;
margin: 0 1.488% 20px;
}
-.col-3 { width: 22.023%; }
+.col-1 { width: 5.357%; }
+.col-2 { width: 13.690%; }
+.col-3 { width: 22.024%; }
+.col-4 { width: 30.357%; }
+.col-5 { width: 38.690%; }
+.col-6 { width: 47.024%; }
+.col-7 { width: 55.357%; }
+.col-8 { width: 63.690%; }
+.col-9 { width: 72.024%; }
+.col-10 { width: 80.357%; }
+.col-11 { width: 88.690%; }
+.col-12 { width: 97.024%; }
.cols hr {
width: 80%;
}