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