Merge "Allow navar.md to enable Markdown extensions"
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 78d1cf8..001b3b2 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
@@ -129,8 +129,9 @@
               .setRequestUri(req.getRequestURI())
               .setReader(reader)
               .setRootTree(root);
+      Navbar navbar = createNavbar(cfg, fmt, navmd);
       res.setHeader(HttpHeaders.ETAG, curEtag);
-      showDoc(req, res, view, cfg, fmt, navmd, srcmd);
+      showDoc(req, res, view, fmt, navbar, srcmd);
     }
   }
 
@@ -161,14 +162,14 @@
       HttpServletRequest req,
       HttpServletResponse res,
       GitilesView view,
-      MarkdownConfig cfg,
       MarkdownToHtml.Builder fmt,
-      MarkdownFile navFile,
+      Navbar navbar,
       MarkdownFile srcFile)
       throws IOException {
     Map<String, Object> data = new HashMap<>();
-    data.putAll(buildNavbar(cfg, fmt, navFile));
+    data.putAll(navbar.toSoyData());
 
+    MarkdownConfig cfg = navbar.getConfig();
     Node doc = GitilesMarkdown.parse(cfg, srcFile.consumeContent());
     data.put("pageTitle", pageTitle(doc, srcFile));
     if (view.getType() != GitilesView.Type.ROOTED_DOC) {
@@ -182,7 +183,10 @@
 
     try (OutputStream out = startRenderCompressedStreamingHtml(req, res, SOY_TEMPLATE, data)) {
       Writer w = newWriter(out, res);
-      fmt.setFilePath(srcFile.path).build().renderToHtml(new StreamHtmlBuilder(w), doc);
+      fmt.setConfig(cfg)
+          .setFilePath(srcFile.path)
+          .build()
+          .renderToHtml(new StreamHtmlBuilder(w), doc);
       w.flush();
     } catch (RuntimeIOException e) {
       Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
@@ -190,14 +194,15 @@
     }
   }
 
-  private Map<String, Object> buildNavbar(
-      MarkdownConfig cfg, MarkdownToHtml.Builder fmt, MarkdownFile navFile) {
-    Navbar navbar = new Navbar();
+  private static Navbar createNavbar(
+      MarkdownConfig cfg, MarkdownToHtml.Builder fmt, @Nullable MarkdownFile navFile) {
+    Navbar navbar = new Navbar().setConfig(cfg);
     if (navFile != null) {
-      navbar.setFormatter(fmt.setFilePath(navFile.path).build());
-      navbar.setMarkdown(cfg, navFile.consumeContent());
+      navbar
+          .setFormatter(fmt.setFilePath(navFile.path).build())
+          .setMarkdown(navFile.consumeContent());
     }
-    return navbar.toSoyData();
+    return navbar;
   }
 
   private static String pageTitle(Node doc, MarkdownFile srcFile) {
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 1982e5a..b4add94 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
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Config.SectionParser;
 import org.eclipse.jgit.util.StringUtils;
@@ -85,6 +86,31 @@
     }
   }
 
+  private MarkdownConfig(MarkdownConfig p, Set<String> enable, Set<String> disable) {
+    render = p.render;
+    inputLimit = p.inputLimit;
+    imageLimit = p.imageLimit;
+    analyticsId = p.analyticsId;
+
+    autoLink = on("autolink", p.autoLink, enable, disable);
+    blockNote = on("blocknote", p.blockNote, enable, disable);
+    ghThematicBreak = on("ghthematicbreak", p.ghThematicBreak, enable, disable);
+    multiColumn = on("multicolumn", p.multiColumn, enable, disable);
+    namedAnchor = on("namedanchor", p.namedAnchor, enable, disable);
+    safeHtml = on("safehtml", p.safeHtml, enable, disable);
+    smartQuote = on("smartquote", p.smartQuote, enable, disable);
+    strikethrough = on("strikethrough", p.strikethrough, enable, disable);
+    tables = on("tables", p.tables, enable, disable);
+    toc = on("toc", p.toc, enable, disable);
+
+    allowAnyIFrame = safeHtml ? p.allowAnyIFrame : false;
+    allowIFrame = safeHtml ? p.allowIFrame : ImmutableList.of();
+  }
+
+  private static boolean on(String key, boolean val, Set<String> enable, Set<String> disable) {
+    return enable.contains(key) ? true : disable.contains(key) ? false : val;
+  }
+
   boolean isIFrameAllowed(String src) {
     if (allowAnyIFrame) {
       return true;
@@ -96,4 +122,8 @@
     }
     return false;
   }
+
+  MarkdownConfig copyWithExtensions(Set<String> enable, Set<String> disable) {
+    return new MarkdownConfig(this, enable, disable);
+  }
 }
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 a4581f1..a0f05bb 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,21 +14,27 @@
 
 package com.google.gitiles.doc;
 
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
 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 java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import static java.util.stream.Collectors.toSet;
 import org.commonmark.node.Heading;
 import org.commonmark.node.Node;
 import org.eclipse.jgit.util.RawParseUtils;
 
 class Navbar {
-  private static final Pattern REF_LINK =
-      Pattern.compile("^\\[(logo|home)\\]:\\s*(.+)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
+  private static final Pattern META_LINK =
+      Pattern.compile(
+          "^\\[(logo|home|extensions)\\]:\\s*(.+)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
 
+  private MarkdownConfig cfg;
   private MarkdownToHtml fmt;
   private Node node;
   private String siteTitle;
@@ -37,14 +43,23 @@
 
   Navbar() {}
 
+  MarkdownConfig getConfig() {
+    return cfg;
+  }
+
+  Navbar setConfig(MarkdownConfig cfg) {
+    this.cfg = cfg;
+    return this;
+  }
+
   Navbar setFormatter(MarkdownToHtml html) {
     this.fmt = html;
     return this;
   }
 
-  Navbar setMarkdown(MarkdownConfig cfg, byte[] md) {
+  Navbar setMarkdown(byte[] md) {
     if (md != null && md.length > 0) {
-      parse(cfg, RawParseUtils.decode(md));
+      parse(RawParseUtils.decode(md));
     }
     return this;
   }
@@ -73,11 +88,10 @@
     }
   }
 
-  private void parse(MarkdownConfig cfg, String markdown) {
+  private void parse(String markdown) {
+    extractMetadata(markdown);
     node = GitilesMarkdown.parse(cfg, markdown);
-
     extractSiteTitle();
-    extractRefLinks(markdown);
   }
 
   private void extractSiteTitle() {
@@ -93,8 +107,8 @@
     }
   }
 
-  private void extractRefLinks(String markdown) {
-    Matcher m = REF_LINK.matcher(markdown);
+  private void extractMetadata(String markdown) {
+    Matcher m = META_LINK.matcher(markdown);
     while (m.find()) {
       String key = m.group(1).toLowerCase();
       String url = m.group(2).trim();
@@ -105,7 +119,29 @@
         case "home":
           homeUrl = url;
           break;
+        case "extensions":
+          Set<String> names = splitExtensionNames(url);
+          cfg = cfg.copyWithExtensions(enabled(names), disabled(names));
+          break;
       }
     }
   }
+
+  private static Set<String> splitExtensionNames(String url) {
+    return Splitter.on(CharMatcher.whitespace().or(CharMatcher.is(',')))
+        .trimResults()
+        .omitEmptyStrings()
+        .splitToList(url)
+        .stream()
+        .map(String::toLowerCase)
+        .collect(toSet());
+  }
+
+  private static Set<String> enabled(Set<String> names) {
+    return names.stream().filter(n -> !n.startsWith("!")).collect(toSet());
+  }
+
+  private static Set<String> disabled(Set<String> names) {
+    return names.stream().filter(n -> n.startsWith("!")).map(n -> n.substring(1)).collect(toSet());
+  }
 }
diff --git a/navbar.md b/navbar.md
index 608531c..9f8ac26 100644
--- a/navbar.md
+++ b/navbar.md
@@ -2,3 +2,5 @@
 * [Markdown](/Documentation/markdown.md)
 * [Configuration](/Documentation/config.md)
 * [Developers](/Documentation/developer-guide.md)
+
+[extensions]: blocknote, multicolumn, namedanchor, smartquote, toc