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,