Make all Markdown extensions configurable

Allow site admins to selectively enable or disable Markdown
extensions.  This can help a site avoid creating documentation
that isn't compatible with other CommonMark parsers.

Change-Id: I0466b03ef213a398d79f943af2ddf95a7e0853e7
diff --git a/Documentation/config.md b/Documentation/config.md
index 65feff2..27db57a 100644
--- a/Documentation/config.md
+++ b/Documentation/config.md
@@ -52,8 +52,51 @@
   imageLimit = 256K
 ```
 
+### Extensions
+
+The following extensions can be enabled/disabled in the markdown
+section:
+
+* `githubFlavor`: enable extensions that mirror GitHub Flavor
+  Markdown behavior.  Default is true.
+
+* `autolink`: automatically convert plain URLs and email
+  addresses into links. Default follows `gihubFlavor`.
+
+* `blocknote`: Gitiles style note/promo/aside blocks to raise
+  awareness to important content. Default false.
+
+* `ghthematicbreak`: accept `--` for `<hr>`, like GitHub Flavor
+  Markdown.  Default follows `githubFlavor`.
+
+* `multicolumn`: Gitiles extension to layout content in a 12 cell
+   grid, delinated by section headers. Default false.
+
+* `namedanchor`: Gitiles extension to extract named anchors using
+  `#{id}` syntax. Default false.
+
+* `safehtml`: Gitiles extension to accept very limited HTML; for
+   security reasons all other HTML is dropped regardless of this
+   setting.  Default follows `githubFlavor`.
+
+* `smartquote`: Gitiles extension to convert single and double quote
+  ASCII characters to Unicode smart quotes when in prose.  Default
+  false.
+
+* `strikethrough`: strikethrough text with GitHub Flavor Markdown
+  style `~~`.  Default follows `githubFlavor`.
+
+* `tables`: format tables with GitHub Flavor Markdown.  Default
+  follows `githubFlavor`.
+
+* `toc`: Gitiles extension to replace `[TOC]` in a paragraph by itself
+  with a server-side generated table of contents extracted from section
+  headers.  Default true.
+
 ### IFrames
 
+IFrame support requires `markdown.safehtml` to be true.
+
 IFrame source URLs can be whitelisted by providing a list of allowed
 URLs. URLs ending with a `/` are treated as prefixes, allowing any source
 URL beginning with that prefix.
diff --git a/Documentation/markdown.md b/Documentation/markdown.md
index 958fed9..23e3bae 100644
--- a/Documentation/markdown.md
+++ b/Documentation/markdown.md
@@ -150,6 +150,8 @@
 
 ### Tables
 
+Requires `markdown.tables` to be true (default).
+
 Simple tables are supported with column alignment.  The first line is
 the header row and subsequent lines are data rows:
 
@@ -199,6 +201,8 @@
 
 ### Strikethrough
 
+Requires `markdown.strikethrough` to be true (default).
+
 Text can be ~~struck out~~ within a paragraph:
 
 ```
@@ -313,9 +317,10 @@
 
 ### Horizontal rules
 
-A horizontal rule can be inserted using GitHub style `--` surrounded
-by blank lines.  Alternatively repeating `-` or `*` and space on a
-line will also create a horizontal rule:
+If `markdown.ghthematicbreak` is true, a horizontal rule can be
+inserted using GitHub style `--` surrounded by blank lines.
+Alternatively repeating `-` or `*` and space on a line will also
+create a horizontal rule:
 
 ```
 ---
@@ -380,7 +385,7 @@
 ### Named anchors
 
 Explicit anchors can be inserted anywhere in the document using
-`<a name="tag"></a>` or `{#tag}`.
+`<a name="tag"></a>`, or `{#tag}` if `markdown.namedanchor` is true.
 
 Implicit anchors are automatically created for each
 [heading](#Headings).  For example `## Section 1` will have
@@ -460,9 +465,9 @@
 by the parser with no warnings, and no output from that section of the
 document.
 
-There are small exceptions for `<br>`, `<hr>`, `<a name>` and
-`<iframe>` elements, see [named anchor](#Named-anchors) and
-[HTML IFrame](#HTML-IFrame).
+If `markdown.safehtml` is true there are small exceptions for `<br>`,
+`<hr>`, `<a name>` and `<iframe>` elements, see [named anchor](#Named-anchors)
+and [HTML IFrame](#HTML-IFrame).
 
 ## Markdown extensions
 
@@ -471,6 +476,8 @@
 
 ### Table of contents
 
+Requires `markdown.toc` to be true.
+
 Place `[TOC]` surrounded by blank lines to insert a generated
 table of contents extracted from the H1, H2, and H3 headers
 used within the document:
@@ -496,6 +503,8 @@
 
 ### Notification, aside, promotion blocks
 
+Requires `markdown.blocknote` to be true.
+
 Similar to fenced code blocks these blocks start and end with `***`,
 are surrounded by blank lines, and include the type of block on the
 opening line.
@@ -538,6 +547,8 @@
 
 ### Column layout
 
+Requires `markdown.multicolumn` to be true.
+
 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.
@@ -608,6 +619,8 @@
 
 ### HTML IFrame
 
+Requires `markdown.safehtml` to be true (default).
+
 Although HTML is stripped the parser has special support for a limited
 subset of `<iframe>` elements:
 
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 468b26e..911c4ef 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/ReadmeHelper.java
@@ -98,7 +98,7 @@
           .setReader(reader)
           .setRootTree(rootTree)
           .build()
-          .toSoyHtml(GitilesMarkdown.parse(raw));
+          .toSoyHtml(GitilesMarkdown.parse(config, raw));
     } catch (RuntimeException | IOException err) {
       log.error(
           String.format(
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 bd5c481..78d1cf8 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
@@ -167,9 +167,9 @@
       MarkdownFile srcFile)
       throws IOException {
     Map<String, Object> data = new HashMap<>();
-    data.putAll(buildNavbar(fmt, navFile));
+    data.putAll(buildNavbar(cfg, fmt, navFile));
 
-    Node doc = GitilesMarkdown.parse(srcFile.consumeContent());
+    Node doc = GitilesMarkdown.parse(cfg, srcFile.consumeContent());
     data.put("pageTitle", pageTitle(doc, srcFile));
     if (view.getType() != GitilesView.Type.ROOTED_DOC) {
       data.put("sourceUrl", GitilesView.show().copyFrom(view).toUrl());
@@ -190,11 +190,12 @@
     }
   }
 
-  private Map<String, Object> buildNavbar(MarkdownToHtml.Builder fmt, MarkdownFile navFile) {
+  private Map<String, Object> buildNavbar(
+      MarkdownConfig cfg, MarkdownToHtml.Builder fmt, MarkdownFile navFile) {
     Navbar navbar = new Navbar();
     if (navFile != null) {
       navbar.setFormatter(fmt.setFilePath(navFile.path).build());
-      navbar.setMarkdown(navFile.consumeContent());
+      navbar.setMarkdown(cfg, navFile.consumeContent());
     }
     return navbar.toSoyData();
   }
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 cc50048..e094931 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,7 +14,9 @@
 
 package com.google.gitiles.doc;
 
-import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import org.commonmark.Extension;
 import org.commonmark.ext.autolink.AutolinkExtension;
 import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
 import org.commonmark.ext.gfm.tables.TablesExtension;
@@ -24,28 +26,43 @@
 
 /** 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();
-
-  public static Node parse(byte[] md) {
-    return parse(RawParseUtils.decode(md));
+  public static Node parse(MarkdownConfig cfg, byte[] md) {
+    return parse(cfg, RawParseUtils.decode(md));
   }
 
-  public static Node parse(String md) {
-    return PARSER.parse(md);
+  public static Node parse(MarkdownConfig cfg, String md) {
+    List<Extension> ext = new ArrayList<>();
+    if (cfg.autoLink) {
+      ext.add(AutolinkExtension.create());
+    }
+    if (cfg.blockNote) {
+      ext.add(BlockNoteExtension.create());
+    }
+    if (cfg.safeHtml) {
+      ext.add(GitilesHtmlExtension.create());
+    }
+    if (cfg.ghThematicBreak) {
+      ext.add(GitHubThematicBreakExtension.create());
+    }
+    if (cfg.multiColumn) {
+      ext.add(MultiColumnExtension.create());
+    }
+    if (cfg.namedAnchor) {
+      ext.add(NamedAnchorExtension.create());
+    }
+    if (cfg.smartQuote) {
+      ext.add(SmartQuotedExtension.create());
+    }
+    if (cfg.strikethrough) {
+      ext.add(StrikethroughExtension.create());
+    }
+    if (cfg.tables) {
+      ext.add(TablesExtension.create());
+    }
+    if (cfg.toc) {
+      ext.add(TocExtension.create());
+    }
+    return Parser.builder().extensions(ext).build().parse(md);
   }
 
   private GitilesMarkdown() {}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownConfig.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownConfig.java
index 5737d85..79ddbb6 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownConfig.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownConfig.java
@@ -41,6 +41,17 @@
   final int imageLimit;
   final String analyticsId;
 
+  final boolean autoLink;
+  final boolean blockNote;
+  final boolean ghThematicBreak;
+  final boolean multiColumn;
+  final boolean namedAnchor;
+  final boolean safeHtml;
+  final boolean smartQuote;
+  final boolean strikethrough;
+  final boolean tables;
+  final boolean toc;
+
   private final boolean allowAnyIFrame;
   private final ImmutableList<String> allowIFrame;
 
@@ -50,7 +61,22 @@
     imageLimit = cfg.getInt("markdown", "imageLimit", IMAGE_LIMIT);
     analyticsId = Strings.emptyToNull(cfg.getString("google", null, "analyticsId"));
 
-    String[] f = cfg.getStringList("markdown", null, "allowiframe");
+    boolean githubFlavor = cfg.getBoolean("markdown", "githubFlavor", true);
+    autoLink = cfg.getBoolean("markdown", "autolink", githubFlavor);
+    blockNote = cfg.getBoolean("markdown", "blocknote", false);
+    ghThematicBreak = cfg.getBoolean("markdown", "ghthematicbreak", githubFlavor);
+    multiColumn = cfg.getBoolean("markdown", "multicolumn", false);
+    namedAnchor = cfg.getBoolean("markdown", "namedanchor", false);
+    safeHtml = cfg.getBoolean("markdown", "safehtml", githubFlavor);
+    smartQuote = cfg.getBoolean("markdown", "smartquote", false);
+    strikethrough = cfg.getBoolean("markdown", "strikethrough", githubFlavor);
+    tables = cfg.getBoolean("markdown", "tables", githubFlavor);
+    toc = cfg.getBoolean("markdown", "toc", true);
+
+    String[] f = {};
+    if (safeHtml) {
+      f = cfg.getStringList("markdown", null, "allowiframe");
+    }
     allowAnyIFrame = f.length == 1 && StringUtils.toBooleanOrNull(f[0]) == Boolean.TRUE;
     if (allowAnyIFrame) {
       allowIFrame = ImmutableList.of();
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 50e0b61..a4581f1 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
@@ -42,9 +42,9 @@
     return this;
   }
 
-  Navbar setMarkdown(byte[] md) {
+  Navbar setMarkdown(MarkdownConfig cfg, byte[] md) {
     if (md != null && md.length > 0) {
-      parse(RawParseUtils.decode(md));
+      parse(cfg, RawParseUtils.decode(md));
     }
     return this;
   }
@@ -73,8 +73,8 @@
     }
   }
 
-  private void parse(String markdown) {
-    node = GitilesMarkdown.parse(markdown);
+  private void parse(MarkdownConfig cfg, String markdown) {
+    node = GitilesMarkdown.parse(cfg, markdown);
 
     extractSiteTitle();
     extractRefLinks(markdown);
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java b/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java
index 9046e0c..27e10cc 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/TestGitilesAccess.java
@@ -78,6 +78,10 @@
       @Override
       public Config getConfig() {
         Config config = new Config();
+        config.setBoolean("markdown", null, "blocknote", true);
+        config.setBoolean("markdown", null, "multicolumn", true);
+        config.setBoolean("markdown", null, "namedanchor", true);
+        config.setBoolean("markdown", null, "smartquote", true);
         config.setStringList(
             "gitiles", null, "allowOriginRegex", ImmutableList.of("http://localhost"));
         return config;