Markdown: recognize special note, promo, and aside blocks

Extend the supported markdown syntax with a special aside format
useful for documentation writing.

  *** note
  Pay special attention here!

  You may have to watch out for crazy developers.
  ***

A note block starts with *** on its own line and ends with *** on its
own line.  Everything inside of the block is read as Markdown and
reprocessed through a recursive call whose AST is inlined.

Three kinds of notes are recognized, which are given different CSS:

  *** note
  Some sort of a warning you should watch out for.
  ***

  *** promo
  Look here this may interest you.
  ***

  *** aside
  The author felt you should read this here, but really
  it's just a distraction and maybe should be omitted or
  moved to another page buried under a link.
  ***

Change-Id: Icbfd27d3afd7d0137e37b7d5e821938c70207cf1
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
new file mode 100644
index 0000000..a84a041
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/DivNode.java
@@ -0,0 +1,42 @@
+// 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/GitilesMarkdown.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesMarkdown.java
index b79bf4d..43c589f 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,15 +17,19 @@
 import com.google.gitiles.GitilesView;
 
 import org.parboiled.Rule;
+import org.parboiled.support.StringBuilderVar;
 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.plugins.BlockPluginParser;
 import org.pegdown.plugins.PegDownPlugins;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.List;
+
 /**
  * Additional markdown extensions known to Gitiles.
  * <p>
@@ -66,13 +70,18 @@
     return new PegDownProcessor(MD_OPTIONS, plugins);
   }
 
+  private PegDownProcessor parser;
+
   GitilesMarkdown() {
     super(MD_OPTIONS, 2000L, DefaultParseRunnerProvider);
   }
 
   @Override
   public Rule[] blockPluginRules() {
-    return new Rule[]{ toc() };
+    return new Rule[]{
+        note(),
+        toc(),
+    };
   }
 
   public Rule toc() {
@@ -80,4 +89,32 @@
         string("[TOC]"),
         push(new TocNode()));
   }
+
+  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())));
+  }
+
+  public List<Node> parse(StringBuilderVar body) {
+    // The pegdown code doesn't provide enough visibility to directly
+    // use its existing parsing rules. Recurse manually for inner text
+    // parsing within a block.
+    if (parser == null) {
+      parser = newParser();
+    }
+    return parser.parseMarkdown(body.getChars()).getChildren();
+  }
 }
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 a3feb6c..1086bab 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
@@ -100,6 +100,13 @@
   }
 
   @Override
+  public void visit(DivNode node) {
+    html.open("div").attribute("class", node.getStyleName());
+    visitChildren(node);
+    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 b8cf64e..19a68cc 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,5 +15,6 @@
 package com.google.gitiles.doc;
 
 public interface Visitor extends org.pegdown.ast.Visitor {
+  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 e047f59..c324786 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
@@ -164,4 +164,33 @@
 }
 th {
   background-color: #f5f5f5;
-}
\ No newline at end of file
+}
+
+.note, .promo, .aside {
+  border: 1px solid;
+  border-radius: 4px;
+  margin: 10px 0;
+  padding: 10px;
+}
+.note {
+  background: #fffbe4;
+  border-color: #f8f6e6;
+}
+.promo {
+  background: #f6f9ff;
+  border-color: #eff2f9;
+}
+.aside {
+  background: #f9f9f9;
+  border-color: #f2f2f2;
+}
+.note p:first-child,
+.promo p:first-child,
+.aside p:first-child {
+  margin-top: 0;
+}
+.note p:last-child,
+.promo p:last-child,
+.aside p:last-child {
+  margin-bottom: 0;
+}