Markdown: recognize special multi-column layout blocks

Documentation writers may want to organize clusters of information
and/or links into a multi-column based layout to help readers find
material quickly.

Extend the supported markdown syntax with a new multi-column region.
A multi-column region is delineated by |||---||| with other markdown
contained within it:

  |||---|||
  ## Markdown is easy
  You can write simple text.

  * [Basic markdown](http://...)
  * [GitHub flavor](http://github.com/...)
  * [This kind](kind.md)

  ## Safe
  No HTML found.

  ## Fast
  Well, faster than you can get coffee.
  |||---|||

Headers within the region define the columns.  For each header a new
column is started and the following content is placed below it into
the same column.

The above will render as three divs next to each other:

  Markdown is easy            Safe                 Fast

  You can write simple        No HTML found.       Well, faster than
  text.                                            you can get coffee.

  * Basic markdown
  * GitHub flavor
  * This kind

At present up to 4 columns may be fit across a page of documentation.
Additional columns should be placed into a new |||---||| block and
will appear below these columns.

The column widths are not configurable.

Change-Id: I088115bf4d4569817aca736cb0ce02f9d777d61b
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
new file mode 100644
index 0000000..00945ef
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/ColsNode.java
@@ -0,0 +1,36 @@
+// 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.SuperNode;
+
+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 {
+  ColsNode(List<Node> children) {
+    super(children);
+  }
+
+  @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 43c589f..512d320 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
@@ -30,12 +30,7 @@
 
 import java.util.List;
 
-/**
- * Additional markdown extensions known to Gitiles.
- * <p>
- * {@code [TOC]} as a stand-alone block will insert a table of contents
- * for the current document.
- */
+/** Parses Gitiles extensions to markdown. */
 class GitilesMarkdown extends Parser implements BlockPluginParser {
   private static final Logger log = LoggerFactory.getLogger(MarkdownHelper.class);
 
@@ -78,7 +73,8 @@
 
   @Override
   public Rule[] blockPluginRules() {
-    return new Rule[]{
+    return new Rule[] {
+        cols(),
         note(),
         toc(),
     };
@@ -108,6 +104,21 @@
         sequence(string("aside"), push(match())));
   }
 
+  public Rule cols() {
+    StringBuilderVar body = new StringBuilderVar();
+    return NodeSequence(
+        colsTag(), Newline(),
+        oneOrMore(
+            testNot(colsTag(), Newline()),
+            Line(body)),
+        colsTag(), Newline(),
+        push(new ColsNode(parse(body))));
+  }
+
+  public Rule colsTag() {
+    return string("|||---|||");
+  }
+
   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
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 1086bab..a11c0e2 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
@@ -107,6 +107,26 @@
   }
 
   @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) {
+      html.close("div");
+    }
+    html.close("div");
+  }
+
+  @Override
   public void visit(HeaderNode node) {
     String tag = "h" + node.getLevel();
     html.open(tag);
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 19a68cc..8e9ea60 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
@@ -15,6 +15,7 @@
 package com.google.gitiles.doc;
 
 public interface Visitor extends org.pegdown.ast.Visitor {
+  void visit(ColsNode node);
   void visit(DivNode 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 c324786..198f8f6 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
@@ -45,7 +45,7 @@
 .nav li a:hover {
   color: #0000f9;
 }
-.nav ul:after {
+.nav ul:after, .cols:after {
   clear: both;
   content: "";
   display: block;
@@ -184,9 +184,9 @@
   background: #f9f9f9;
   border-color: #f2f2f2;
 }
-.note p:first-child,
-.promo p:first-child,
-.aside p:first-child {
+.note :first-child,
+.promo :first-child,
+.aside :first-child {
   margin-top: 0;
 }
 .note p:last-child,
@@ -194,3 +194,16 @@
 .aside p:last-child {
   margin-bottom: 0;
 }
+
+.cols {
+  margin: 0 -1.533%;
+  width: 103.067%;
+}
+.col-3 {
+  float: left;
+  margin: 0 1.488% 20px;
+}
+.col-3 { width: 22.023%; }
+.cols hr {
+  width: 80%;
+}