Merge "Add additional anchor to match GitHub TOC rendering"
diff --git a/Documentation/config.md b/Documentation/config.md
index 65feff2..dc7977d 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 `githubFlavor`.
+
+* `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 a5d5ec1..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:
 
@@ -172,7 +174,9 @@
 Placing `:` in the separator line indicates how the column should be
 aligned.  A colon on the left side is a **left-aligned** column; a
 colon on the right-most side is **right-aligned**; a colon on both
-sides is **center-aligned**.
+sides is **center-aligned**. If no alignment is specified, the column
+is aligned with the default for HTML `<td>` tags (left-aligned by
+default unless overridden by css).
 
 Empty table cells are indicated by whitespace between the column
 dividers (`| |`) while multiple column cells omit the whitespace.
@@ -197,6 +201,8 @@
 
 ### Strikethrough
 
+Requires `markdown.strikethrough` to be true (default).
+
 Text can be ~~struck out~~ within a paragraph:
 
 ```
@@ -311,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:
 
 ```
 ---
@@ -378,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
@@ -458,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
 
@@ -469,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:
@@ -494,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.
@@ -536,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.
@@ -606,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/WORKSPACE b/WORKSPACE
index d22ec4d..9463f1a 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
 load("//tools:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "42244973783cd0b14c5dc24014a97c4148488d6a",
+    commit = "680d42de63b146e77d41685b38a295abb2980b6d",
     # local_path = "/home/<user>/projects/bazlets",
 )
 
@@ -15,7 +15,7 @@
 )
 
 maven_jar(
-    name = "commons_lang",
+    name = "commons_lang3",
     artifact = "org.apache.commons:commons-lang3:3.1",
     sha1 = "905075e6c80f206bbe6cf1e809d2caa69f420c76",
 )
@@ -28,8 +28,8 @@
 
 maven_jar(
     name = "guava",
-    artifact = "com.google.guava:guava:21.0",
-    sha1 = "3a3d111be1be1b745edfa7d91678a12d7ed38709",
+    artifact = "com.google.guava:guava:22.0",
+    sha1 = "3564ef3803de51fb0530a8377ec6100b33b0d073",
 )
 
 maven_jar(
@@ -105,8 +105,8 @@
 
 maven_jar(
     name = "soy",
-    artifact = "com.google.template:soy:2017-02-01",
-    sha1 = "8638940b207779fe3b75e55b6e65abbefb6af678",
+    artifact = "com.google.template:soy:2017-04-23",
+    sha1 = "52f32a5a3801ab97e0909373ef7f73a3460d0802",
 )
 
 maven_jar(
@@ -127,36 +127,36 @@
     sha1 = "198ea005f41219f038f4291f0b0e9f3259730e92",
 )
 
-JGIT_VERS = "4.7.0.201704051617-r"
+JGIT_VERS = "4.8.0.201706111038-r"
 
 JGIT_REPO = MAVEN_CENTRAL
 
 maven_jar(
-    name = "jgit",
+    name = "jgit_lib",
     artifact = "org.eclipse.jgit:org.eclipse.jgit:" + JGIT_VERS,
     repository = JGIT_REPO,
-    sha1 = "99be65d1827276b97d4f51668b60f4a38f282bda",
+    sha1 = "f0978a9e868accf9a405d9387bec091a99d87633",
 )
 
 maven_jar(
     name = "jgit_servlet",
     artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + JGIT_VERS,
     repository = JGIT_REPO,
-    sha1 = "72fa98ebf001aadd3dcb99ca8f7fcd90983da56b",
+    sha1 = "3c099afdc063bad438a3b87eea643e9722a07de8",
 )
 
 maven_jar(
     name = "jgit_junit",
     artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + JGIT_VERS,
     repository = JGIT_REPO,
-    sha1 = "e0dbc6d3568b2ba65c9421af2f06e4158a624bcb",
+    sha1 = "4f45f8f6714df649dbad8c1b1baf68b9510b5047",
 )
 
 maven_jar(
-    name = "jgit_archive_library",
+    name = "jgit_archive",
     artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + JGIT_VERS,
     repository = JGIT_REPO,
-    sha1 = "f825504a903dfe8d3daa61d6ab5c26fbad92c954",
+    sha1 = "1350a5cf1fad91dd33b66f9fb804dc8e68270890",
 )
 
 maven_jar(
@@ -236,46 +236,46 @@
     sha1 = "6975da39a7040257bd51d21a231b76c915872d38",
 )
 
-JETTY_VERSION = "9.3.17.v20170317"
+JETTY_VERSION = "9.3.18.v20170406"
 
 maven_jar(
     name = "servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERSION,
-    sha1 = "ed6986b0d0ca7b9b0f9015c9efb80442e3043a8e",
+    sha1 = "534e7fa0e4fb6e08f89eb3f6a8c48b4f81ff5738",
 )
 
 maven_jar(
     name = "security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERSION,
-    sha1 = "ca52535569445682d42aaa97c7039442719a0507",
+    sha1 = "16b900e91b04511f42b706c925c8af6023d2c05e",
 )
 
 maven_jar(
     name = "server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERSION,
-    sha1 = "194e9a02e6ba249ef4a3f4bd56b4993087992299",
+    sha1 = "0a32feea88cba2d43951d22b60861c643454bb3f",
 )
 
 maven_jar(
     name = "continuation",
     artifact = "org.eclipse.jetty:jetty-continuation:" + JETTY_VERSION,
-    sha1 = "63ff8e2716e20b72787a1dbc666022ef6c1f7b1e",
+    sha1 = "3c5d89c8204d4a48a360087f95e4cbd4520b5de0",
 )
 
 maven_jar(
     name = "http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERSION,
-    sha1 = "6c02d728e15d4868486254039c867a1ac3e4a52e",
+    sha1 = "30ece6d732d276442d513b94d914de6fa1075fae",
 )
 
 maven_jar(
     name = "io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERSION,
-    sha1 = "756a8cd2a1cbfb84a94973b6332dd3eccd47c0cd",
+    sha1 = "36cb411ee89be1b527b0c10747aa3153267fc3ec",
 )
 
 maven_jar(
     name = "util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERSION,
-    sha1 = "b8512ab02819de01f0f5a5c6026163041f579beb",
+    sha1 = "8600b7d028a38cb462eff338de91390b3ff5040e",
 )
diff --git a/blame-cache/BUILD b/blame-cache/BUILD
index 024399d..860198a 100644
--- a/blame-cache/BUILD
+++ b/blame-cache/BUILD
@@ -6,7 +6,7 @@
 ]
 
 java_library(
-    name = "lib",
+    name = "blame-cache",
     srcs = SRCS,
     visibility = ["//visibility:public"],
     deps = DEPS,
@@ -17,7 +17,7 @@
 java_doc(
     name = "javadoc",
     libs = [
-        ":lib",
+        ":blame-cache",
         "//lib:guava",
         "//lib/jgit:jgit",
     ],
diff --git a/gitiles-servlet/BUILD b/gitiles-servlet/BUILD
index 127608b..bc67ebd 100644
--- a/gitiles-servlet/BUILD
+++ b/gitiles-servlet/BUILD
@@ -4,9 +4,9 @@
 )
 
 DEPS = [
-    "//blame-cache:lib",
+    "//blame-cache:blame-cache",
     "//lib:autolink",
-    "//lib:commons-lang",
+    "//lib:commons-lang3",
     "//lib:gson",
     "//lib:guava",
     "//lib:joda-time",
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
index 3985b92..bf19e88 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/BaseServlet.java
@@ -52,7 +52,6 @@
 /** Base servlet class for Gitiles servlets that serve Soy templates. */
 public abstract class BaseServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
-  private static final String ACCESS_ATTRIBUTE = BaseServlet.class.getName() + "/GitilesAccess";
   private static final String DATA_ATTRIBUTE = BaseServlet.class.getName() + "/Data";
   private static final String STREAMING_ATTRIBUTE = BaseServlet.class.getName() + "/Streaming";
 
@@ -225,7 +224,42 @@
       HttpServletRequest req, HttpServletResponse res, String templateName, Map<String, ?> soyData)
       throws IOException {
     req.setAttribute(STREAMING_ATTRIBUTE, true);
-    return renderer.renderStreaming(res, templateName, startHtmlResponse(req, res, soyData));
+    return renderer.renderStreaming(res, false, templateName, startHtmlResponse(req, res, soyData));
+  }
+
+  /**
+   * Start a compressed, streaming HTML response with header and footer rendered by Soy.
+   *
+   * <p>A streaming template includes the special template {@code gitiles.streamingPlaceholder} at
+   * the point where data is to be streamed. The template before and after this placeholder is
+   * rendered using the provided data map.
+   *
+   * <p>The response will be gzip compressed (if the user agent supports it) to reduce bandwidth.
+   * This may delay rendering in the browser.
+   *
+   * @param req in-progress request.
+   * @param res in-progress response.
+   * @param templateName Soy template name; must be in one of the template files defined in {@link
+   *     Renderer}.
+   * @param soyData data for Soy.
+   * @return output stream to render to. The portion of the template before the placeholder is
+   *     already written and flushed; the portion after is written only on calling {@code close()}.
+   * @throws IOException an error occurred during rendering the header.
+   */
+  protected OutputStream startRenderCompressedStreamingHtml(
+      HttpServletRequest req,
+      HttpServletResponse res,
+      String templateName,
+      Map<String, ?> soyData)
+      throws IOException {
+    req.setAttribute(STREAMING_ATTRIBUTE, true);
+    boolean gzip = false;
+    if (acceptsGzipEncoding(req)) {
+      res.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
+      res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
+      gzip = true;
+    }
+    return renderer.renderStreaming(res, gzip, templateName, startHtmlResponse(req, res, soyData));
   }
 
   private Map<String, ?> startHtmlResponse(
@@ -245,7 +279,7 @@
     if (!allData.containsKey("repositoryName") && view.getRepositoryName() != null) {
       allData.put("repositoryName", view.getRepositoryName());
     }
-    if (!allData.containsKey("breadcrumbs")) {
+    if (!allData.containsKey("breadcrumbs") && view.getRepositoryName() != null) {
       allData.put("breadcrumbs", view.getBreadcrumbs());
     }
 
@@ -331,17 +365,12 @@
   }
 
   protected GitilesAccess getAccess(HttpServletRequest req) {
-    GitilesAccess access = (GitilesAccess) req.getAttribute(ACCESS_ATTRIBUTE);
-    if (access == null) {
-      access = accessFactory.forRequest(req);
-      req.setAttribute(ACCESS_ATTRIBUTE, access);
-    }
-    return access;
+    return GitilesAccess.getAccess(req, accessFactory);
   }
 
   protected void setCacheHeaders(HttpServletRequest req, HttpServletResponse res) {
-    if (Strings.nullToEmpty(req.getHeader(HttpHeaders.PRAGMA)).equalsIgnoreCase("no-cache") ||
-        Strings.nullToEmpty(req.getHeader(HttpHeaders.CACHE_CONTROL))
+    if (Strings.nullToEmpty(req.getHeader(HttpHeaders.PRAGMA)).equalsIgnoreCase("no-cache")
+        || Strings.nullToEmpty(req.getHeader(HttpHeaders.CACHE_CONTROL))
             .equalsIgnoreCase("no-cache")) {
       setNotCacheable(res);
       return;
@@ -350,16 +379,16 @@
     GitilesView view = ViewFilter.getView(req);
     Revision rev = view.getRevision();
     if (rev.nameIsId()) {
-      res.setHeader(HttpHeaders.CACHE_CONTROL,
-          "private, max-age=7200, stale-while-revalidate=604800");
+      res.setHeader(
+          HttpHeaders.CACHE_CONTROL, "private, max-age=7200, stale-while-revalidate=604800");
       return;
     }
 
     setNotCacheable(res);
   }
 
-  protected void setApiHeaders(
-       HttpServletRequest req, HttpServletResponse res, String contentType) throws IOException {
+  protected void setApiHeaders(HttpServletRequest req, HttpServletResponse res, String contentType)
+      throws IOException {
     if (!Strings.isNullOrEmpty(contentType)) {
       res.setContentType(contentType);
     }
@@ -390,8 +419,8 @@
     setApiHeaders(req, res, type.getMimeType());
   }
 
-  protected void setDownloadHeaders(HttpServletRequest req, HttpServletResponse res,
-      String filename, String contentType) {
+  protected void setDownloadHeaders(
+      HttpServletRequest req, HttpServletResponse res, String filename, String contentType) {
     res.setContentType(contentType);
     res.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename);
     setCacheHeaders(req, res);
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java b/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java
index 5ec22f2..791067b 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/DebugRenderer.java
@@ -15,9 +15,10 @@
 package com.google.gitiles;
 
 import static com.google.common.base.Preconditions.checkState;
+import static java.util.stream.Collectors.toList;
 
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Streams;
 import com.google.common.hash.HashCode;
 import com.google.template.soy.SoyFileSet;
 import com.google.template.soy.tofu.SoyTofu;
@@ -36,7 +37,7 @@
         fileUrlMapper(soyTemplatesRoot + File.separator),
         ImmutableMap.<String, String>of(),
         staticPrefix,
-        FluentIterable.from(customTemplatesFilenames).transform(fileUrlMapper()),
+        Streams.stream(customTemplatesFilenames).map(fileUrlMapper()).collect(toList()),
         siteTitle);
   }
 
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java
index c43c7a8..297350e 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/DefaultRenderer.java
@@ -19,7 +19,6 @@
 import com.google.common.io.Resources;
 import com.google.template.soy.SoyFileSet;
 import com.google.template.soy.tofu.SoyTofu;
-
 import java.net.URL;
 import java.util.Map;
 
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/FileJsonData.java b/gitiles-servlet/src/main/java/com/google/gitiles/FileJsonData.java
index 4409bde..c506fe0 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/FileJsonData.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/FileJsonData.java
@@ -14,7 +14,6 @@
 
 package com.google.gitiles;
 
-import java.io.IOException;
 import org.eclipse.jgit.lib.ObjectId;
 
 class FileJsonData {
@@ -25,8 +24,7 @@
     String path;
   }
 
-  static File toJsonData(ObjectId id, String repo, String revision, String path)
-      throws IOException {
+  static File toJsonData(ObjectId id, String repo, String revision, String path) {
     File file = new File();
     file.id = id.name();
     file.repo = repo;
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java
index ec3b5ff..505098a 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesAccess.java
@@ -16,6 +16,7 @@
 
 import java.io.IOException;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
@@ -30,6 +31,21 @@
  * information about the host and repository.
  */
 public interface GitilesAccess {
+  /** Access for the current request, if it has been initialized. */
+  public static Optional<GitilesAccess> getAccess(HttpServletRequest req) {
+    return Optional.ofNullable((GitilesAccess) req.getAttribute(GitilesAccess.class.getName()));
+  }
+
+  /** Access for the current request. */
+  public static GitilesAccess getAccess(HttpServletRequest req, Factory factory) {
+    GitilesAccess access = getAccess(req).orElse(null);
+    if (access == null) {
+      access = factory.forRequest(req);
+      req.setAttribute(GitilesAccess.class.getName(), access);
+    }
+    return access;
+  }
+
   /** Factory for per-request access. */
   public interface Factory {
     GitilesAccess forRequest(HttpServletRequest req);
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
index 3e8867d..b815f12 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/GitilesFilter.java
@@ -19,9 +19,9 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gitiles.GitilesServlet.STATIC_PREFIX;
 import static com.google.gitiles.Renderer.fileUrlMapper;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
@@ -317,9 +317,9 @@
       renderer =
           new DefaultRenderer(
               filterConfig.getServletContext().getContextPath() + STATIC_PREFIX,
-              FluentIterable.from(
-                      Arrays.asList(config.getStringList("gitiles", null, "customTemplates")))
-                  .transform(fileUrlMapper()),
+              Arrays.stream(config.getStringList("gitiles", null, "customTemplates"))
+                  .map(fileUrlMapper())
+                  .collect(toList()),
               firstNonNull(config.getString("gitiles", null, "siteTitle"), "Gitiles"));
     }
   }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java b/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java
index 40eeb83..666d5a2 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/HtmlDiffFormatter.java
@@ -35,7 +35,8 @@
 
 /** Formats a unified format patch as UTF-8 encoded HTML. */
 final class HtmlDiffFormatter extends DiffFormatter {
-  private static final byte[] DIFF_BEGIN = "<pre class=\"u-pre u-monospace Diff-unified\">".getBytes(UTF_8);
+  private static final byte[] DIFF_BEGIN =
+      "<pre class=\"u-pre u-monospace Diff-unified\">".getBytes(UTF_8);
   private static final byte[] DIFF_END = "</pre>".getBytes(UTF_8);
 
   private static final byte[] HUNK_BEGIN = "<span class=\"Diff-hunk\">".getBytes(UTF_8);
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/Linkifier.java b/gitiles-servlet/src/main/java/com/google/gitiles/Linkifier.java
index 6ef9e05..493e1c5 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/Linkifier.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/Linkifier.java
@@ -106,8 +106,7 @@
     String baseGerritUrl = urls.getBaseGerritUrl(req);
 
     if (baseGerritUrl != null) {
-      CommentLinkInfo changeIds =
-          new CommentLinkInfo(CHANGE_ID_PATTERN, baseGerritUrl + "#/q/$0");
+      CommentLinkInfo changeIds = new CommentLinkInfo(CHANGE_ID_PATTERN, baseGerritUrl + "#/q/$0");
       operationalCommentLinks.add(changeIds);
     }
 
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
index 55ea975..b4a8e03 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/LogServlet.java
@@ -212,7 +212,7 @@
 
   private static Optional<ObjectId> getStart(
       ListMultimap<String, String> params, ObjectReader reader)
-          throws IOException, InvalidStartValueException {
+      throws IOException, InvalidStartValueException {
     List<String> values = params.get(START_PARAM);
     switch (values.size()) {
       case 0:
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java b/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
index 629985d..0ecb875 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/PathServlet.java
@@ -261,10 +261,7 @@
               req,
               res,
               FileJsonData.toJsonData(
-                  wr.id,
-                  view.getRepositoryName(),
-                  view.getRevision().getName(),
-                  wr.path),
+                  wr.id, view.getRepositoryName(), view.getRevision().getName(), wr.path),
               FileJsonData.File.class);
           break;
         case TREE:
@@ -274,6 +271,9 @@
               TreeJsonData.toJsonData(wr.id, wr.tw, includeSizes, recursive),
               TreeJsonData.Tree.class);
           break;
+        case EXECUTABLE_FILE:
+        case GITLINK:
+        case SYMLINK:
         default:
           res.setStatus(SC_NOT_FOUND);
           break;
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/Renderer.java b/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
index 86a4a9c..12912c2 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/Renderer.java
@@ -19,7 +19,6 @@
 import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.MapMaker;
@@ -39,6 +38,8 @@
 import java.net.URL;
 import java.util.Map;
 import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
+import java.util.zip.GZIPOutputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -134,7 +135,7 @@
     URL u = templates.get(soyFile);
     checkState(u != null, "Missing Soy template %s", soyFile);
 
-    Hasher h = Hashing.sha1().newHasher();
+    Hasher h = Hashing.murmur3_128().newHasher();
     try (InputStream is = u.openStream();
         OutputStream os = Funnels.asOutputStream(h)) {
       ByteStreams.copy(is, os);
@@ -165,23 +166,26 @@
 
   OutputStream renderStreaming(HttpServletResponse res, String templateName, Map<String, ?> soyData)
       throws IOException {
-    final String html = newRenderer(templateName).setData(soyData).render();
+    return renderStreaming(res, false, templateName, soyData);
+  }
+
+  OutputStream renderStreaming(
+      HttpServletResponse res, boolean gzip, String templateName, Map<String, ?> soyData)
+      throws IOException {
+    String html = newRenderer(templateName).setData(soyData).render();
     int id = html.indexOf(PLACEHOLDER);
     checkArgument(id >= 0, "Template must contain %s", PLACEHOLDER);
 
     int lt = html.lastIndexOf('<', id);
-    final int gt = html.indexOf('>', id + PLACEHOLDER.length());
-    final OutputStream out = res.getOutputStream();
+    int gt = html.indexOf('>', id + PLACEHOLDER.length());
+
+    OutputStream out = gzip ? new GZIPOutputStream(res.getOutputStream()) : res.getOutputStream();
     out.write(html.substring(0, lt).getBytes(UTF_8));
     out.flush();
 
+    byte[] tail = html.substring(gt + 1).getBytes(UTF_8);
     return new OutputStream() {
       @Override
-      public void write(byte[] b) throws IOException {
-        out.write(b);
-      }
-
-      @Override
       public void write(byte[] b, int off, int len) throws IOException {
         out.write(b, off, len);
       }
@@ -198,8 +202,9 @@
 
       @Override
       public void close() throws IOException {
-        out.write(html.substring(gt + 1).getBytes(UTF_8));
-        out.close();
+        try (OutputStream o = out) {
+          o.write(tail);
+        }
       }
     };
   }
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 0f83cce..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
@@ -14,7 +14,6 @@
 
 package com.google.gitiles.doc;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
 import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
@@ -25,16 +24,19 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
 import com.google.common.net.HttpHeaders;
 import com.google.gitiles.BaseServlet;
-import com.google.gitiles.FormatType;
 import com.google.gitiles.GitilesAccess;
 import com.google.gitiles.GitilesView;
 import com.google.gitiles.Renderer;
 import com.google.gitiles.ViewFilter;
+import com.google.gitiles.doc.html.StreamHtmlBuilder;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
 import java.util.HashMap;
 import java.util.Map;
 import javax.annotation.Nullable;
@@ -127,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);
     }
   }
 
@@ -139,7 +142,7 @@
 
   private String etag(MarkdownFile srcmd, @Nullable MarkdownFile navmd) {
     byte[] b = new byte[Constants.OBJECT_ID_LENGTH];
-    Hasher h = Hashing.sha1().newHasher();
+    Hasher h = Hashing.murmur3_128().newHasher();
     h.putInt(ETAG_GEN);
 
     renderer.getTemplateHash(SOY_FILE).writeBytesTo(b, 0, b.length);
@@ -159,20 +162,15 @@
       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<>();
-    Navbar navbar = new Navbar();
-    if (navFile != null) {
-      navbar.setFormatter(fmt.setFilePath(navFile.path).build());
-      navbar.setMarkdown(navFile.content);
-    }
     data.putAll(navbar.toSoyData());
 
-    Node doc = GitilesMarkdown.parse(srcFile.content);
+    MarkdownConfig cfg = navbar.getConfig();
+    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());
@@ -182,21 +180,29 @@
     if (cfg.analyticsId != null) {
       data.put("analyticsId", cfg.analyticsId);
     }
-    data.put("bodyHtml", fmt.setFilePath(srcFile.path).build().toSoyHtml(doc));
 
-    String page = renderer.render(SOY_TEMPLATE, data);
-    byte[] raw = page.getBytes(UTF_8);
-    res.setContentType(FormatType.HTML.getMimeType());
-    res.setCharacterEncoding(UTF_8.name());
-    setCacheHeaders(req, res);
-    if (acceptsGzipEncoding(req)) {
-      res.addHeader(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
-      res.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
-      raw = gzip(raw);
+    try (OutputStream out = startRenderCompressedStreamingHtml(req, res, SOY_TEMPLATE, data)) {
+      Writer w = newWriter(out, res);
+      fmt.setConfig(cfg)
+          .setFilePath(srcFile.path)
+          .build()
+          .renderToHtml(new StreamHtmlBuilder(w), doc);
+      w.flush();
+    } catch (RuntimeIOException e) {
+      Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
+      throw e;
     }
-    res.setContentLength(raw.length);
-    res.setStatus(HttpServletResponse.SC_OK);
-    res.getOutputStream().write(raw);
+  }
+
+  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())
+          .setMarkdown(navFile.consumeContent());
+    }
+    return navbar;
   }
 
   private static String pageTitle(Node doc, MarkdownFile srcFile) {
@@ -282,5 +288,11 @@
     void read(ObjectReader reader, MarkdownConfig cfg) throws IOException {
       content = reader.open(id, OBJ_BLOB).getCachedBytes(cfg.inputLimit);
     }
+
+    byte[] consumeContent() {
+      byte[] c = content;
+      content = null;
+      return c;
+    }
   }
 }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesHtmlExtension.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesHtmlExtension.java
index 435bb16..834f5fd 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesHtmlExtension.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/GitilesHtmlExtension.java
@@ -36,10 +36,10 @@
  * rendering:
  *
  * <ul>
- * <li>{@link HardLineBreak}
- * <li>{@link ThematicBreak}
- * <li>{@link NamedAnchor}
- * <li>{@link IframeBlock}
+ *   <li>{@link HardLineBreak}
+ *   <li>{@link ThematicBreak}
+ *   <li>{@link NamedAnchor}
+ *   <li>{@link IframeBlock}
  * </ul>
  */
 public class GitilesHtmlExtension implements ParserExtension {
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..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;
@@ -27,7 +28,7 @@
     return cfg.get(CONFIG_PARSER);
   }
 
-  private static SectionParser<MarkdownConfig> CONFIG_PARSER =
+  private static final SectionParser<MarkdownConfig> CONFIG_PARSER =
       new SectionParser<MarkdownConfig>() {
         @Override
         public MarkdownConfig parse(Config cfg) {
@@ -41,6 +42,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 +62,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();
@@ -59,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;
@@ -70,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/MarkdownToHtml.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/MarkdownToHtml.java
index d4297dc..9de1a09 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
@@ -21,6 +21,7 @@
 import com.google.gitiles.GitilesView;
 import com.google.gitiles.ThreadSafePrettifyParser;
 import com.google.gitiles.doc.html.HtmlBuilder;
+import com.google.gitiles.doc.html.SoyHtmlBuilder;
 import com.google.template.soy.data.SanitizedContent;
 import com.google.template.soy.shared.restricted.EscapingConventions.FilterImageDataUri;
 import com.google.template.soy.shared.restricted.EscapingConventions.FilterNormalizeUri;
@@ -118,8 +119,8 @@
     }
   }
 
-  private final HtmlBuilder html = new HtmlBuilder();
-  private final TocFormatter toc = new TocFormatter(html, 3);
+  private HtmlBuilder html;
+  private TocFormatter toc;
   private final String requestUri;
   private final GitilesView view;
   private final MarkdownConfig config;
@@ -143,14 +144,26 @@
   }
 
   /** Render the document AST to sanitized HTML. */
-  public SanitizedContent toSoyHtml(Node node) {
-    if (node == null) {
-      return null;
+  public void renderToHtml(HtmlBuilder out, Node node) {
+    if (node != null) {
+      html = out;
+      toc = new TocFormatter(html, 3);
+      toc.setRoot(node);
+      node.accept(this);
+      html.finish();
+      html = null;
+      toc = null;
     }
+  }
 
-    toc.setRoot(node);
-    node.accept(this);
-    return html.toSoy();
+  /** Render the document AST to sanitized HTML. */
+  public SanitizedContent toSoyHtml(Node node) {
+    if (node != null) {
+      SoyHtmlBuilder out = new SoyHtmlBuilder();
+      renderToHtml(out, node);
+      return out.toSoy();
+    }
+    return null;
   }
 
   @Override
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..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,6 +43,15 @@
 
   Navbar() {}
 
+  MarkdownConfig getConfig() {
+    return cfg;
+  }
+
+  Navbar setConfig(MarkdownConfig cfg) {
+    this.cfg = cfg;
+    return this;
+  }
+
   Navbar setFormatter(MarkdownToHtml html) {
     this.fmt = html;
     return this;
@@ -74,10 +89,9 @@
   }
 
   private void parse(String markdown) {
-    node = GitilesMarkdown.parse(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/gitiles-servlet/src/main/java/com/google/gitiles/doc/RuntimeIOException.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/RuntimeIOException.java
new file mode 100644
index 0000000..8ef7923
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/RuntimeIOException.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// 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 java.io.IOException;
+
+/** {@link IOException} wrapped inside RuntimeException. */
+public class RuntimeIOException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  public RuntimeIOException(IOException cause) {
+    super(cause);
+  }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
index 9551e94..b8f43b0 100644
--- a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/HtmlBuilder.java
@@ -19,9 +19,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
-import com.google.template.soy.data.SanitizedContent;
-import com.google.template.soy.data.SanitizedContent.ContentKind;
-import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
+import com.google.gitiles.doc.RuntimeIOException;
 import com.google.template.soy.shared.restricted.EscapingConventions.EscapeHtml;
 import com.google.template.soy.shared.restricted.EscapingConventions.FilterImageDataUri;
 import com.google.template.soy.shared.restricted.EscapingConventions.FilterNormalizeUri;
@@ -37,8 +35,10 @@
  * <p>Useful but critical attributes like {@code href} on anchors or {@code src} on img permit only
  * safe subset of URIs, primarily {@code http://}, {@code https://}, and for image src {@code
  * data:image/*;base64,...}.
+ *
+ * <p>See concrete subclasses {@link SoyHtmlBuilder} and {@link StreamHtmlBuilder}.
  */
-public final class HtmlBuilder {
+public abstract class HtmlBuilder {
   private static final ImmutableSet<String> ALLOWED_TAGS =
       ImmutableSet.of(
           "h1",
@@ -112,12 +112,12 @@
     return GIT_URI.matcher(val).find();
   }
 
-  private final StringBuilder htmlBuf;
+  private final Appendable htmlBuf;
   private final Appendable textBuf;
   private String tag;
 
-  public HtmlBuilder() {
-    htmlBuf = new StringBuilder();
+  HtmlBuilder(Appendable out) {
+    htmlBuf = out;
     textBuf = EscapeHtml.INSTANCE.escape(htmlBuf);
   }
 
@@ -125,7 +125,11 @@
   public HtmlBuilder open(String tagName) {
     checkArgument(ALLOWED_TAGS.contains(tagName), "invalid HTML tag %s", tagName);
     finishActiveTag();
-    htmlBuf.append('<').append(tagName);
+    try {
+      htmlBuf.append('<').append(tagName);
+    } catch (IOException e) {
+      throw new RuntimeIOException(e);
+    }
     tag = tagName;
     return this;
   }
@@ -167,7 +171,7 @@
       htmlBuf.append('"');
       return this;
     } catch (IOException e) {
-      throw new IllegalStateException(e);
+      throw new RuntimeIOException(e);
     }
   }
 
@@ -190,10 +194,14 @@
 
   private void finishActiveTag() {
     if (tag != null) {
-      if (SELF_CLOSING_TAGS.contains(tag)) {
-        htmlBuf.append(" />");
-      } else {
-        htmlBuf.append('>');
+      try {
+        if (SELF_CLOSING_TAGS.contains(tag)) {
+          htmlBuf.append(" />");
+        } else {
+          htmlBuf.append('>');
+        }
+      } catch (IOException e) {
+        throw new RuntimeIOException(e);
       }
       tag = null;
     }
@@ -205,7 +213,11 @@
         ALLOWED_TAGS.contains(tag) && !SELF_CLOSING_TAGS.contains(tag), "invalid HTML tag %s", tag);
 
     finishActiveTag();
-    htmlBuf.append("</").append(tag).append('>');
+    try {
+      htmlBuf.append("</").append(tag).append('>');
+    } catch (IOException e) {
+      throw new RuntimeIOException(e);
+    }
     return this;
   }
 
@@ -216,14 +228,18 @@
       textBuf.append(in);
       return this;
     } catch (IOException e) {
-      throw new IllegalStateException(e);
+      throw new RuntimeIOException(e);
     }
   }
 
   /** Append a space outside of an element. */
   public HtmlBuilder space() {
     finishActiveTag();
-    htmlBuf.append(' ');
+    try {
+      htmlBuf.append(' ');
+    } catch (IOException e) {
+      throw new RuntimeIOException(e);
+    }
     return this;
   }
 
@@ -233,12 +249,15 @@
   public void entity(String entity) {
     checkArgument(HTML_ENTITY.matcher(entity).matches(), "invalid entity %s", entity);
     finishActiveTag();
-    htmlBuf.append(entity);
+    try {
+      htmlBuf.append(entity);
+    } catch (IOException e) {
+      throw new RuntimeIOException(e);
+    }
   }
 
-  /** Bless the current content as HTML. */
-  public SanitizedContent toSoy() {
+  /** Finish the document. */
+  public void finish() {
     finishActiveTag();
-    return UnsafeSanitizedContentOrdainer.ordainAsSafe(htmlBuf.toString(), ContentKind.HTML);
   }
 }
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/SoyHtmlBuilder.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/SoyHtmlBuilder.java
new file mode 100644
index 0000000..23e6ee6
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/SoyHtmlBuilder.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// 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.html;
+
+import com.google.template.soy.data.SanitizedContent;
+import com.google.template.soy.data.SanitizedContent.ContentKind;
+import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
+
+/** Builds a document fragment using a restricted subset of HTML. */
+public final class SoyHtmlBuilder extends HtmlBuilder {
+  private final StringBuilder buf;
+
+  public SoyHtmlBuilder() {
+    this(new StringBuilder());
+  }
+
+  private SoyHtmlBuilder(StringBuilder buf) {
+    super(buf);
+    this.buf = buf;
+  }
+
+  /** Bless the current content as HTML. */
+  public SanitizedContent toSoy() {
+    finish();
+    return UnsafeSanitizedContentOrdainer.ordainAsSafe(buf.toString(), ContentKind.HTML);
+  }
+}
diff --git a/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/StreamHtmlBuilder.java b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/StreamHtmlBuilder.java
new file mode 100644
index 0000000..467dd43
--- /dev/null
+++ b/gitiles-servlet/src/main/java/com/google/gitiles/doc/html/StreamHtmlBuilder.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// 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.html;
+
+import java.io.Writer;
+
+/** Writes sanitized HTML to a stream. */
+public final class StreamHtmlBuilder extends HtmlBuilder {
+  public StreamHtmlBuilder(Writer out) {
+    super(out);
+  }
+}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy
index 2193c93..eff4d53 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/Doc.soy
@@ -21,11 +21,10 @@
  * @param? logoUrl url of image logo.
  * @param? homeUrl url to jump to top of site.
  * @param? analyticsId Google Analytics Property ID.
- * @param sourceUrl url for source view of the page.
- * @param logUrl url for log history of page.
- * @param blameUrl url for blame of page source.
- * @param? navbarHtml markdown ast node to convert.
- * @param bodyHtml safe html to embed into the body of the page.
+ * @param? sourceUrl url for source view of the page.
+ * @param? logUrl url for log history of page.
+ * @param? blameUrl url for blame of page source.
+ * @param? navbarHtml navar.md converted to SafeHtml.
  */
 {template .markdownDoc}
 <!DOCTYPE html>
@@ -59,21 +58,17 @@
   <div class="Site-content Site-Content--markdown">
     <div class="Container">
       <div class="doc">
-        {$bodyHtml}
+        {call .streamingPlaceholder /}
       </div>
     </div>
   </div>
   <footer class="Site-footer">
     <div class="Footer">
-      <div class="Footer-poweredBy">
-        Powered by <a href="https://gerrit.googlesource.com/gitiles/">Gitiles</a>
-      </div>
+      {call gitiles.footerPoweredBy /}
       <div class="Footer-links">
         {if $sourceUrl}<a class="Footer-link" href="{$sourceUrl}">{msg desc="text for the source link"}source{/msg}</a>{/if}
         {if $logUrl}<a class="Footer-link" href="{$logUrl}">{msg desc="text for the log link"}log{/msg}</a>{/if}
         {if $blameUrl}<a class="Footer-link" href="{$blameUrl}">{msg desc="text for the blame link"}blame{/msg}</a>{/if}
-      </ul>
-
     </div>
   </footer>
   {if $analyticsId}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy
index a740370..ea0e06e 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/ObjectDetail.soy
@@ -324,7 +324,7 @@
  * @param name name.
  * @param email email.
  */
-{template .person_ private="true"}
+{template .person_}
 {$name}{if $email} &lt;{$email}&gt;{/if}
 {/template}
 
@@ -336,7 +336,7 @@
  *     text: raw text of the part.
  *     url: optional URL that should be linked to from the part.
  */
-{template .message_ private="true"}
+{template .message_ visibility="private"}
 <pre class="{$className}">
   {foreach $part in $message}
     {if $part.url}<a href="{$part.url}">{$part.text}</a>{else}{$part.text}{/if}
diff --git a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
index 3ab7d5b..78f5b79 100644
--- a/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
+++ b/gitiles-servlet/src/main/resources/com/google/gitiles/templates/RepositoryIndex.soy
@@ -101,7 +101,7 @@
  * @param? branches list of branch objects with url and name keys.
  * @param? moreBranchesUrl URL to show more branches, if necessary.
  */
-{template .branches_ private="true"}
+{template .branches_ visibility="private"}
   {if length($branches)}
     {call .refList}
       {param type: 'Branches' /}
@@ -119,7 +119,7 @@
  * @param? tags list of branch objects with url and name keys.
  * @param? moreTagsUrl URL to show more tags, if necessary.
  */
-{template .tags_ private="true"}
+{template .tags_ visibility="private"}
   {if length($tags)}
     {call .refList}
       {param type: 'Tags' /}
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/ConfigUtilTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/ConfigUtilTest.java
index b7735c5..a9d58bc 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/ConfigUtilTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/ConfigUtilTest.java
@@ -42,8 +42,7 @@
       getDuration(config, "core", "dht", "timeout", def);
       fail("expected IllegalArgumentException");
     } catch (IllegalArgumentException e) {
-      assertThat(e).hasMessageThat().isEqualTo(
-          "Invalid time unit value: core.dht.timeout=5.2 sec");
+      assertThat(e).hasMessageThat().isEqualTo("Invalid time unit value: core.dht.timeout=5.2 sec");
     }
 
     config.setString("core", "dht", "timeout", "1 min");
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/GitwebRedirectFilterTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/GitwebRedirectFilterTest.java
index 879815f..d9863f1 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/GitwebRedirectFilterTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/GitwebRedirectFilterTest.java
@@ -41,9 +41,7 @@
 
   @Before
   public void setUp() throws Exception {
-    repo =
-        new TestRepository<>(
-            new InMemoryRepository(new DfsRepositoryDescription("test")));
+    repo = new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription("test")));
     servlet = TestGitilesServlet.create(repo);
   }
 
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/HostIndexServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/HostIndexServletTest.java
index e890f61..d8332d1 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/HostIndexServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/HostIndexServletTest.java
@@ -42,9 +42,7 @@
   @Override
   @Before
   public void setUp() throws Exception {
-    repo =
-        new TestRepository<>(
-            new InMemoryRepository(new DfsRepositoryDescription(NAME)));
+    repo = new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription(NAME)));
     servlet = TestGitilesServlet.create(repo);
   }
 
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/PaginatorTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/PaginatorTest.java
index 7f8961d..f03a40b 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/PaginatorTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/PaginatorTest.java
@@ -39,9 +39,7 @@
 
   @Before
   public void setUp() throws Exception {
-    repo =
-        new TestRepository<>(
-            new InMemoryRepository(new DfsRepositoryDescription("test")));
+    repo = new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription("test")));
     walk = new RevWalk(repo.getRepository());
   }
 
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
index 7d6f737..c0f98b1 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/PathServletTest.java
@@ -358,14 +358,12 @@
   @Test
   public void rejectOrigin() throws Exception {
     repo.branch("master").commit().add("foo", "contents").create();
-    FakeHttpServletResponse res = buildResponse(
-        "/repo/+/master/foo", "format=text", SC_OK, "http://notlocalhost");
+    FakeHttpServletResponse res =
+        buildResponse("/repo/+/master/foo", "format=text", SC_OK, "http://notlocalhost");
     assertThat(res.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("text/plain");
-    assertThat(res.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN))
-        .isEqualTo(null);
+    assertThat(res.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo(null);
   }
 
-
   private Map<String, ?> getBlobData(Map<String, ?> data) {
     return ((Map<String, Map<String, ?>>) data).get("data");
   }
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/RevisionParserTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/RevisionParserTest.java
index ed3ed9b..f287685 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/RevisionParserTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/RevisionParserTest.java
@@ -41,9 +41,7 @@
 
   @Before
   public void setUp() throws Exception {
-    repo =
-        new TestRepository<>(
-            new InMemoryRepository(new DfsRepositoryDescription("test")));
+    repo = new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription("test")));
     parser =
         new RevisionParser(
             repo.getRepository(),
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;
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/TimeCacheTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/TimeCacheTest.java
index e8a05a4..8fe900a 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/TimeCacheTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/TimeCacheTest.java
@@ -50,9 +50,7 @@
 
   @Before
   public void setUp() throws Exception {
-    repo =
-        new TestRepository<>(
-            new InMemoryRepository(new DfsRepositoryDescription("test")));
+    repo = new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription("test")));
     walk = new RevWalk(repo.getRepository());
     cache = new TimeCache();
     start = repo.getDate().getTime() / 1000;
diff --git a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
index 68b57ba..2a8cb65 100644
--- a/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
+++ b/gitiles-servlet/src/test/java/com/google/gitiles/ViewFilterTest.java
@@ -38,9 +38,7 @@
 
   @Before
   public void setUp() throws Exception {
-    repo =
-        new TestRepository<>(
-            new InMemoryRepository(new DfsRepositoryDescription("repo")));
+    repo = new TestRepository<>(new InMemoryRepository(new DfsRepositoryDescription("repo")));
   }
 
   @Test
diff --git a/lib/BUILD b/lib/BUILD
index 595854e..fe4988e 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -8,12 +8,11 @@
 ) for n in [
     "autolink",
     "commonmark",
-    "commons-lang",
+    "commons-lang3",
     "cm-autolink",
     "gfm-strikethrough",
     "gfm-tables",
     "jsr305",
-    "jgit-archive-library",
     "joda-time",
     "servlet-api_2_5",
     "servlet-api_3_0",
diff --git a/lib/jgit/BUILD b/lib/jgit/BUILD
index 9fd1d4f..bd8c100 100644
--- a/lib/jgit/BUILD
+++ b/lib/jgit/BUILD
@@ -9,7 +9,7 @@
 
 java_library(
     name = "jgit",
-    exports = ["@jgit//jar"],
+    exports = ["@jgit_lib//jar"],
 )
 
 java_library(
@@ -33,7 +33,7 @@
 
 java_library(
     name = "jgit-archive_library",
-    exports = ["@jgit_archive_library//jar"],
+    exports = ["@jgit_archive//jar"],
 )
 
 java_library(
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
diff --git a/tools/maven/BUILD b/tools/maven/BUILD
index defb13a..5efc003 100644
--- a/tools/maven/BUILD
+++ b/tools/maven/BUILD
@@ -3,7 +3,7 @@
 
 maven_package(
     src = {
-        "blame-cache": "//blame-cache:liblib-src.jar",
+        "blame-cache": "//blame-cache:libblame-cache-src.jar",
         "gitiles-servlet": "//gitiles-servlet:libservlet-src.jar",
     },
     doc = {
@@ -12,7 +12,7 @@
     },
     group = "com.google.gitiles",
     jar = {
-        "blame-cache": "//blame-cache:lib",
+        "blame-cache": "//blame-cache:blame-cache",
         "gitiles-servlet": "//gitiles-servlet:servlet",
     },
     repository = "gerrit-maven-repository",
diff --git a/version.bzl b/version.bzl
index 83bdb48..46218e5 100644
--- a/version.bzl
+++ b/version.bzl
@@ -4,4 +4,4 @@
 # we currently have no stable releases, we use the "build number" scheme
 # described at:
 # http://mojo.codehaus.org/versions-maven-plugin/version-rules.html
-GITILES_VERSION = '0.2-1'
+GITILES_VERSION = '0.2-2'