Add markdown.frontmatter option to strip YAML front matter
Markdown files authored for static site generators (Jekyll, Hugo, etc.)
often begin with a YAML front matter block delimited by "---" lines.
Gitiles has no front matter support, so CommonMark parses the block as
ordinary content: the closing "---" turns the metadata into a setext
<h2>, rendering an ugly heading of raw "key: value" pairs at the top of
the page.
Add an opt-in "markdown.frontmatter" option (default false, consistent
with the other non-GitHub-flavor extensions). When enabled,
GitilesMarkdown installs commonmark-ext-yaml-front-matter so the leading
block is parsed into a YamlFrontMatterBlock, and MarkdownToHtml omits
that block from the rendered output instead of throwing on the unknown
CustomBlock.
Documented in Documentation/{config,markdown}.md and covered by
GitilesMarkdownTest.
Change-Id: I2ace7227728e964d2e73d5149a06f2d9a884f0c8
diff --git a/Documentation/config.md b/Documentation/config.md
index 29f328f..c9c7448 100644
--- a/Documentation/config.md
+++ b/Documentation/config.md
@@ -186,6 +186,10 @@
* `blocknote`: Gitiles style note/promo/aside blocks to raise
awareness to important content. Default false.
+* `frontmatter`: strip a leading YAML front matter block (delimited
+ by `---` lines) from the rendered output instead of treating it as
+ document content. Default false.
+
* `ghthematicbreak`: accept `--` for `<hr>`, like GitHub Flavor
Markdown. Default follows `githubFlavor`.
diff --git a/Documentation/markdown.md b/Documentation/markdown.md
index 4c76388..83f14d7 100644
--- a/Documentation/markdown.md
+++ b/Documentation/markdown.md
@@ -474,6 +474,26 @@
Gitiles includes additional extensions to the Markdown language that
make documentation writing for the web easier without using raw HTML.
+### Front matter
+
+Requires `markdown.frontmatter` to be true.
+
+Files authored for static site generators (such as Jekyll or Hugo)
+often begin with a YAML *front matter* block delimited by `---` lines:
+
+```
+---
+title: My page
+layout: post
+---
+
+# My page
+```
+
+When enabled, Gitiles strips this leading block from the rendered
+output instead of displaying the raw `key: value` metadata as
+document content. The block must begin on the first line of the file.
+
### Table of contents
Requires `markdown.toc` to be true.
diff --git a/external_deps.lock.json b/external_deps.lock.json
index a5f46c2..36df3f9 100755
--- a/external_deps.lock.json
+++ b/external_deps.lock.json
@@ -50,6 +50,7 @@
"org.commonmark:commonmark-ext-autolink": -1853742120,
"org.commonmark:commonmark-ext-gfm-strikethrough": 350394231,
"org.commonmark:commonmark-ext-gfm-tables": 1881582931,
+ "org.commonmark:commonmark-ext-yaml-front-matter": -1519651186,
"org.eclipse.jetty.ee10:jetty-ee10-servlet": 835495267,
"org.eclipse.jetty.ee8:jetty-ee8-nested": 1867536598,
"org.eclipse.jetty.ee8:jetty-ee8-security": -374913491,
@@ -208,6 +209,8 @@
"org.commonmark:commonmark-ext-gfm-strikethrough:jar:sources": 992870423,
"org.commonmark:commonmark-ext-gfm-tables": -1205584749,
"org.commonmark:commonmark-ext-gfm-tables:jar:sources": 1341057091,
+ "org.commonmark:commonmark-ext-yaml-front-matter": 1918089254,
+ "org.commonmark:commonmark-ext-yaml-front-matter:jar:sources": -902462472,
"org.commonmark:commonmark:jar:sources": -1511261547,
"org.eclipse.jetty.ee10:jetty-ee10-servlet": 349394374,
"org.eclipse.jetty.ee10:jetty-ee10-servlet:jar:sources": 501419503,
@@ -736,6 +739,13 @@
},
"version": "0.24.0"
},
+ "org.commonmark:commonmark-ext-yaml-front-matter": {
+ "shasums": {
+ "jar": "a845baba681ccbf385695fbaa6d58eb40d5ecc68f3edb968c42f074f630e8fec",
+ "sources": "5cee64663842f1e7128af9c3e62ef183ea2cf205436d27a08b7cb5be0b20edb6"
+ },
+ "version": "0.24.0"
+ },
"org.eclipse.jetty.ee10:jetty-ee10-servlet": {
"shasums": {
"jar": "05cccc74bbe1478ceb765e967b1cb1a3c8d9b51e99acf586190887e467ffd87d",
@@ -1088,6 +1098,9 @@
"org.commonmark:commonmark-ext-gfm-tables": [
"org.commonmark:commonmark"
],
+ "org.commonmark:commonmark-ext-yaml-front-matter": [
+ "org.commonmark:commonmark"
+ ],
"org.eclipse.jetty.ee10:jetty-ee10-servlet": [
"jakarta.servlet:jakarta.servlet-api",
"org.eclipse.jetty:jetty-security",
@@ -2447,6 +2460,10 @@
"org.commonmark.ext.gfm.tables",
"org.commonmark.ext.gfm.tables.internal"
],
+ "org.commonmark:commonmark-ext-yaml-front-matter": [
+ "org.commonmark.ext.front.matter",
+ "org.commonmark.ext.front.matter.internal"
+ ],
"org.eclipse.jetty.ee10:jetty-ee10-servlet": [
"org.eclipse.jetty.ee10.servlet",
"org.eclipse.jetty.ee10.servlet.internal",
@@ -2819,6 +2836,8 @@
"org.commonmark:commonmark-ext-gfm-strikethrough:jar:sources",
"org.commonmark:commonmark-ext-gfm-tables",
"org.commonmark:commonmark-ext-gfm-tables:jar:sources",
+ "org.commonmark:commonmark-ext-yaml-front-matter",
+ "org.commonmark:commonmark-ext-yaml-front-matter:jar:sources",
"org.commonmark:commonmark:jar:sources",
"org.eclipse.jetty.ee10:jetty-ee10-servlet",
"org.eclipse.jetty.ee10:jetty-ee10-servlet:jar:sources",
diff --git a/java/com/google/gitiles/BUILD b/java/com/google/gitiles/BUILD
index 84e8166..404f413 100644
--- a/java/com/google/gitiles/BUILD
+++ b/java/com/google/gitiles/BUILD
@@ -6,6 +6,7 @@
"//java/com/google/gitiles/blame/cache",
"//lib:autolink",
"//lib:cm-autolink",
+ "//lib:cm-yaml-front-matter",
"//lib:commonmark",
"//lib:commons-io",
"//lib:commons-lang3",
diff --git a/java/com/google/gitiles/doc/GitilesMarkdown.java b/java/com/google/gitiles/doc/GitilesMarkdown.java
index e094931..f4c7d6e 100644
--- a/java/com/google/gitiles/doc/GitilesMarkdown.java
+++ b/java/com/google/gitiles/doc/GitilesMarkdown.java
@@ -18,6 +18,7 @@
import java.util.List;
import org.commonmark.Extension;
import org.commonmark.ext.autolink.AutolinkExtension;
+import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.node.Node;
@@ -38,6 +39,9 @@
if (cfg.blockNote) {
ext.add(BlockNoteExtension.create());
}
+ if (cfg.frontMatter) {
+ ext.add(YamlFrontMatterExtension.create());
+ }
if (cfg.safeHtml) {
ext.add(GitilesHtmlExtension.create());
}
diff --git a/java/com/google/gitiles/doc/MarkdownConfig.java b/java/com/google/gitiles/doc/MarkdownConfig.java
index 4758654..53d979a 100644
--- a/java/com/google/gitiles/doc/MarkdownConfig.java
+++ b/java/com/google/gitiles/doc/MarkdownConfig.java
@@ -44,6 +44,7 @@
final boolean autoLink;
final boolean blockNote;
+ final boolean frontMatter;
final boolean ghThematicBreak;
final boolean multiColumn;
final boolean namedAnchor;
@@ -65,6 +66,7 @@
boolean githubFlavor = cfg.getBoolean("markdown", "githubFlavor", true);
autoLink = cfg.getBoolean("markdown", "autolink", githubFlavor);
blockNote = cfg.getBoolean("markdown", "blocknote", false);
+ frontMatter = cfg.getBoolean("markdown", "frontmatter", false);
ghThematicBreak = cfg.getBoolean("markdown", "ghthematicbreak", githubFlavor);
multiColumn = cfg.getBoolean("markdown", "multicolumn", false);
namedAnchor = cfg.getBoolean("markdown", "namedanchor", false);
@@ -94,6 +96,7 @@
autoLink = on("autolink", p.autoLink, enable, disable);
blockNote = on("blocknote", p.blockNote, enable, disable);
+ frontMatter = on("frontmatter", p.frontMatter, enable, disable);
ghThematicBreak = on("ghthematicbreak", p.ghThematicBreak, enable, disable);
multiColumn = on("multicolumn", p.multiColumn, enable, disable);
namedAnchor = on("namedanchor", p.namedAnchor, enable, disable);
diff --git a/java/com/google/gitiles/doc/MarkdownToHtml.java b/java/com/google/gitiles/doc/MarkdownToHtml.java
index cb8afae..f0518e6 100644
--- a/java/com/google/gitiles/doc/MarkdownToHtml.java
+++ b/java/com/google/gitiles/doc/MarkdownToHtml.java
@@ -27,6 +27,7 @@
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
+import org.commonmark.ext.front.matter.YamlFrontMatterBlock;
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TableBody;
@@ -576,6 +577,10 @@
visit((TableBlock) node);
} else if (node instanceof TocBlock) {
toc.format();
+ } else if (node instanceof YamlFrontMatterBlock) {
+ // YAML front matter is document metadata: omit the whole block. We
+ // intentionally do not recurse into it, so its YamlFrontMatterNode
+ // children are never visited and need no visit(CustomNode) handling.
} else {
throw new IllegalArgumentException("cannot render " + node.getClass());
}
diff --git a/javatests/com/google/gitiles/BUILD b/javatests/com/google/gitiles/BUILD
index 692537d..b2410ff 100644
--- a/javatests/com/google/gitiles/BUILD
+++ b/javatests/com/google/gitiles/BUILD
@@ -42,8 +42,10 @@
"//java/com/google/gitiles:servlet",
":testutil",
"//lib:servlet-api",
+ "//lib:commonmark",
"//lib:commons-codec",
"//lib:commons-io",
+ "//lib:html-types",
"//lib/truth",
"//lib:jgit-junit",
"//lib/junit",
diff --git a/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java b/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java
new file mode 100644
index 0000000..74e4c3e
--- /dev/null
+++ b/javatests/com/google/gitiles/doc/GitilesMarkdownTest.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2026 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.html.types.SafeHtml;
+import com.google.gitiles.GitilesView;
+import org.commonmark.node.Node;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for YAML front matter handling in {@link GitilesMarkdown}. */
+@RunWith(JUnit4.class)
+public class GitilesMarkdownTest {
+ private static final String MARKDOWN =
+ "---\ntitle: Kittens\nlayout: post\n---\n\n# Heading\n\nBody text.\n";
+
+ @Test
+ public void frontMatterStrippedWhenEnabled() {
+ String html = render(MARKDOWN, /* frontMatter= */ true);
+ assertThat(html).doesNotContain("title: Kittens");
+ assertThat(html).doesNotContain("layout: post");
+ assertThat(html).contains("Heading</h1>");
+ assertThat(html).contains("Body text.");
+ }
+
+ @Test
+ public void frontMatterNodeChildrenAreStrippedWithoutError() {
+ // Front matter parses into a YamlFrontMatterBlock whose YamlFrontMatterNode
+ // (CustomNode) children hold the key/value data, including list values.
+ // visit(CustomBlock) omits the block without recursing, so those children
+ // are never visited: rendering neither throws (as it would for an unhandled
+ // CustomNode) nor leaks the metadata.
+ String markdown =
+ "---\n"
+ + "title: Kittens\n"
+ + "tags:\n"
+ + " - cats\n"
+ + " - fluffy\n"
+ + "---\n"
+ + "\n"
+ + "# Heading\n"
+ + "\n"
+ + "Body text.\n";
+ String html = render(markdown, /* frontMatter= */ true);
+ assertThat(html).doesNotContain("Kittens");
+ assertThat(html).doesNotContain("cats");
+ assertThat(html).doesNotContain("fluffy");
+ assertThat(html).contains("Heading</h1>");
+ assertThat(html).contains("Body text.");
+ }
+
+ @Test
+ public void frontMatterRenderedAsContentWhenDisabled() {
+ // Without the extension the delimiters and keys are parsed as ordinary
+ // Markdown, leaking the raw metadata into the output. This is the behavior
+ // markdown.frontmatter is meant to fix.
+ String html = render(MARKDOWN, /* frontMatter= */ false);
+ assertThat(html).contains("title: Kittens");
+ }
+
+ private static String render(String markdown, boolean frontMatter) {
+ Config cfg = new Config();
+ cfg.setBoolean("markdown", null, "frontmatter", frontMatter);
+ MarkdownConfig mc = new MarkdownConfig(cfg);
+ Node node = GitilesMarkdown.parse(mc, markdown);
+ GitilesView view =
+ GitilesView.revision()
+ .setHostName("127.0.0.1")
+ .setServletPath("/g")
+ .setRepositoryName("repo")
+ .setRevision("HEAD")
+ .build();
+ SafeHtml html =
+ MarkdownToHtml.builder()
+ .setGitilesView(view)
+ .setConfig(mc)
+ .setFilePath("index.md")
+ .build()
+ .toSoyHtml(node);
+ return html == null ? "" : html.getSafeHtmlString();
+ }
+}
diff --git a/lib/BUILD b/lib/BUILD
index c2c3df3..bd8b90a 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -17,6 +17,7 @@
("cm-autolink", "@external_deps//:org_commonmark_commonmark_ext_autolink"),
("gfm-strikethrough", "@external_deps//:org_commonmark_commonmark_ext_gfm_strikethrough"),
("gfm-tables", "@external_deps//:org_commonmark_commonmark_ext_gfm_tables"),
+ ("cm-yaml-front-matter", "@external_deps//:org_commonmark_commonmark_ext_yaml_front_matter"),
("html-types", "@external_deps//:com_google_common_html_types_types"),
("jsr305", "@external_deps//:com_google_code_findbugs_jsr305"),
("servlet-api", "@external_deps//:javax_servlet_javax_servlet_api"),
diff --git a/tools/java_deps.MODULE.bazel b/tools/java_deps.MODULE.bazel
index 77836a2..5a5a4e9 100644
--- a/tools/java_deps.MODULE.bazel
+++ b/tools/java_deps.MODULE.bazel
@@ -101,6 +101,7 @@
"org.commonmark:commonmark-ext-autolink:" + COMMONMARK_VERSION,
"org.commonmark:commonmark-ext-gfm-strikethrough:" + COMMONMARK_VERSION,
"org.commonmark:commonmark-ext-gfm-tables:" + COMMONMARK_VERSION,
+ "org.commonmark:commonmark-ext-yaml-front-matter:" + COMMONMARK_VERSION,
"org.eclipse.jetty:jetty-http:" + JETTY_VERSION,
"org.eclipse.jetty:jetty-io:" + JETTY_VERSION,
"org.eclipse.jetty:jetty-security:" + JETTY_VERSION,