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} <{$email}>{/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'