Forráskód Böngészése

Build a website. (#4189)

Demo site: https://jonmeow.carbon-lang.dev/

I'm trying to keep work under the `/website` subdirectory so that the
misc files don't interfere with unrelated views of the repository. The
`prebuild.py` script does some work to move things around and add
frontmatter, helping the jekyll generation.

I'm using the "just-the-docs" theme because I think it's a decent match
for what we want, and getting jekyll up and running with it wasn't too
difficult. Note #1526 proposed using Docusaurus; I started out there,
but was having trouble getting it working with newer versions. The
plugins in particular I got stuck trying to make work, which sent me
looking for options that we could have working with less customization.
I do lean towards jekyll though, because it's what GH uses so hopefully
we can get a more consistent experience.

Having a website has been approved for a while under #1492, but hasn't
been a priority. I'm mainly doing this because I want to just be able to
point people to carbon-lang.dev and have easy links that way.

---------

Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
Jon Ross-Perkins 1 éve
szülő
commit
b6396e97f8

+ 1 - 0
.codespell_ignore

@@ -16,4 +16,5 @@ inout
 parameteras
 parameteras
 pullrequest
 pullrequest
 rightt
 rightt
+rouge
 statics
 statics

+ 41 - 0
.github/workflows/gh_pages_ci.yaml

@@ -0,0 +1,41 @@
+# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+name: GitHub Pages CI
+
+on:
+  pull_request:
+
+# Cancel previous workflows on the PR when there are multiple fast commits.
+# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
+  cancel-in-progress: true
+
+permissions:
+  contents: read
+
+jobs:
+  # Build job
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
+        with:
+          egress-policy: audit
+
+      - name: Checkout
+        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - name: Prebuild actions
+        run: ./website/prebuild.py
+      - name: Setup Ruby
+        uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
+        with:
+          # Runs 'bundle install' and caches installed gems automatically.
+          bundler-cache: true
+          # Increment this number if you need to re-download cached gems.
+          cache-version: 0
+      - name: Build with Jekyll
+        run: bundle exec jekyll build

+ 76 - 0
.github/workflows/gh_pages_deploy.yaml

@@ -0,0 +1,76 @@
+# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+name: GitHub Pages deploy
+
+on:
+  # Runs on pushes targeting the default branch.
+  push:
+    branches: ['trunk']
+
+  # Allows you to run this workflow manually from the Actions tab.
+  workflow_dispatch:
+
+# Cancel previous workflows on the PR when there are multiple fast commits.
+# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
+  cancel-in-progress: true
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages.
+permissions:
+  contents: read
+  pages: write
+  id-token: write
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
+        with:
+          egress-policy: audit
+
+      - name: Checkout
+        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+      - name: Prebuild actions
+        run: ./website/prebuild.py
+      - name: Setup Pages
+        uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
+      - name: Setup Ruby
+        uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
+        with:
+          # Runs 'bundle install' and caches installed gems automatically.
+          bundler-cache: true
+          # Increment this number if you need to re-download cached gems.
+          cache-version: 0
+      - name: Build with Jekyll
+        env:
+          JEKYLL_ENV: production
+        run: |
+          bundle exec jekyll build --verbose \
+            --source ./ \
+            --destination ./_site \
+            --baseurl "${{ steps.pages.outputs.base_path }}"
+      - name: Upload artifact
+        # Automatically uploads an artifact from the './_site' directory by
+        # default.
+        uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
+
+  deploy:
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+    runs-on: ubuntu-latest
+    needs: build
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@5c7944e73c4c2a096b17a9cb74d65b6c2bbafbde # v2.9.1
+        with:
+          egress-policy: audit
+
+      - name: Deploy to GitHub Pages
+        id: deployment
+        uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5

+ 3 - 1
.pre-commit-config.yaml

@@ -180,7 +180,7 @@ repos:
             Exceptions. See /LICENSE for license information.
             Exceptions. See /LICENSE for license information.
             SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
             SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
           - --custom_format
           - --custom_format
-          - '\.(carbon|c|json|proto|ypp)(\.tmpl)?$'
+          - '\.(carbon|c|json|proto|scss|ypp)(\.tmpl)?$'
           - ''
           - ''
           - '// '
           - '// '
           - ''
           - ''
@@ -216,6 +216,8 @@ repos:
               compile_flags.txt|
               compile_flags.txt|
               github_tools/requirements.txt|
               github_tools/requirements.txt|
               third_party/.*|
               third_party/.*|
+              website/.ruby-version|
+              website/Gemfile.lock|
               .*\.def|
               .*\.def|
               .*\.svg|
               .*\.svg|
               .*/fuzzer_corpus/.*|
               .*/fuzzer_corpus/.*|

+ 3 - 3
SECURITY.md

@@ -1,11 +1,11 @@
+# Security policy
+
 <!--
 <!--
 Part of the Carbon Language project, under the Apache License v2.0 with LLVM
 Part of the Carbon Language project, under the Apache License v2.0 with LLVM
 Exceptions. See /LICENSE for license information.
 Exceptions. See /LICENSE for license information.
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 -->
 -->
 
 
-# Security policy
-
 It's important to us that the Carbon Language provides a secure implementation.
 It's important to us that the Carbon Language provides a secure implementation.
 Thank you for taking the time to report vulnerabilities.
 Thank you for taking the time to report vulnerabilities.
 
 
@@ -13,7 +13,7 @@ The Carbon Language is still an
 [experimental project](/README.md#project-status), so please be careful if using
 [experimental project](/README.md#project-status), so please be careful if using
 it in security-sensitive environments.
 it in security-sensitive environments.
 
 
-# Reporting a vulnerability
+## Reporting a vulnerability
 
 
 Please use
 Please use
 <https://github.com/carbon-language/carbon-lang/security/advisories/new> to
 <https://github.com/carbon-language/carbon-lang/security/advisories/new> to

+ 9 - 0
docs/images/snippets.md

@@ -6,6 +6,11 @@ Exceptions. See /LICENSE for license information.
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 -->
 -->
 
 
+<!--
+{% raw %}
+Hides `{{` from jekyll's liquid parsing. Note endraw at the bottom.
+-->
+
 ## Images
 ## Images
 
 
 Images are managed in
 Images are managed in
@@ -138,3 +143,7 @@ auto main() -> int {
   return 0;
   return 0;
 }
 }
 ```
 ```
+
+<!--
+{% endraw %}
+-->

+ 9 - 0
explorer/README.md

@@ -6,6 +6,11 @@ Exceptions. See /LICENSE for license information.
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 -->
 -->
 
 
+<!--
+{% raw %}
+Hides `{{` from jekyll's liquid parsing. Note endraw at the bottom.
+-->
+
 `explorer` is an implementation of Carbon whose primary purpose is to act as a
 `explorer` is an implementation of Carbon whose primary purpose is to act as a
 clear specification of the language. As an extension of that goal, it can also
 clear specification of the language. As an extension of that goal, it can also
 be used as a platform for prototyping and validating changes to the language.
 be used as a platform for prototyping and validating changes to the language.
@@ -334,3 +339,7 @@ information and provide visual separation for different sections.
 - - - - -  Sub Heading - - - - -
 - - - - -  Sub Heading - - - - -
 --------------------------------
 --------------------------------
 ```
 ```
+
+<!--
+{% endraw %}
+-->

+ 2 - 0
explorer/ast/README.md

@@ -1,3 +1,5 @@
+# Explorer AST
+
 <!--
 <!--
 Part of the Carbon Language project, under the Apache License v2.0 with LLVM
 Part of the Carbon Language project, under the Apache License v2.0 with LLVM
 Exceptions. See /LICENSE for license information.
 Exceptions. See /LICENSE for license information.

+ 2 - 0
explorer/syntax/README.md

@@ -1,3 +1,5 @@
+# Explorer Syntax
+
 <!--
 <!--
 Part of the Carbon Language project, under the Apache License v2.0 with LLVM
 Part of the Carbon Language project, under the Apache License v2.0 with LLVM
 Exceptions. See /LICENSE for license information.
 Exceptions. See /LICENSE for license information.

+ 9 - 0
testing/README.md

@@ -0,0 +1,9 @@
+# Testing
+
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+Testing-specific libraries.

+ 9 - 0
testing/file_test/README.md

@@ -6,6 +6,11 @@ Exceptions. See /LICENSE for license information.
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 -->
 -->
 
 
+<!--
+{% raw %}
+Hides `{{` from jekyll's liquid parsing. Note endraw at the bottom.
+-->
+
 ## BUILD
 ## BUILD
 
 
 A typical BUILD target will look like:
 A typical BUILD target will look like:
@@ -156,3 +161,7 @@ Supported comment markers are:
     Tips like this are added by autoupdate, for example providing commands to
     Tips like this are added by autoupdate, for example providing commands to
     run the test directly. Tips have no impact on validation; the marker informs
     run the test directly. Tips have no impact on validation; the marker informs
     autoupdate that it can update or remove them as needed.
     autoupdate that it can update or remove them as needed.
+
+<!--
+{% endraw %}
+-->

+ 1 - 0
website/.ruby-version

@@ -0,0 +1 @@
+3.3

+ 12 - 0
website/404.md

@@ -0,0 +1,12 @@
+---
+title: 404
+nav_exclude: true
+---
+
+# 404: File not found
+
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->

+ 14 - 0
website/Gemfile

@@ -0,0 +1,14 @@
+# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+source 'https://rubygems.org'
+
+gem "jekyll", "4.3.3"
+gem "just-the-docs", "0.8.2"
+
+group :jekyll_plugins do
+  gem "jekyll-default-layout", "0.1.5"
+  gem "jekyll-readme-index", "0.3.0"
+  gem "jekyll-relative-links", "0.7.0"
+end

+ 100 - 0
website/Gemfile.lock

@@ -0,0 +1,100 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    addressable (2.8.7)
+      public_suffix (>= 2.0.2, < 7.0)
+    bigdecimal (3.1.8)
+    colorator (1.1.0)
+    concurrent-ruby (1.3.4)
+    em-websocket (0.5.3)
+      eventmachine (>= 0.12.9)
+      http_parser.rb (~> 0)
+    eventmachine (1.2.7)
+    ffi (1.17.0)
+    forwardable-extended (2.6.0)
+    google-protobuf (4.27.3-arm64-darwin)
+      bigdecimal
+      rake (>= 13)
+    google-protobuf (4.27.3-x86_64-linux)
+      bigdecimal
+      rake (>= 13)
+    http_parser.rb (0.8.0)
+    i18n (1.14.5)
+      concurrent-ruby (~> 1.0)
+    jekyll (4.3.3)
+      addressable (~> 2.4)
+      colorator (~> 1.0)
+      em-websocket (~> 0.5)
+      i18n (~> 1.0)
+      jekyll-sass-converter (>= 2.0, < 4.0)
+      jekyll-watch (~> 2.0)
+      kramdown (~> 2.3, >= 2.3.1)
+      kramdown-parser-gfm (~> 1.0)
+      liquid (~> 4.0)
+      mercenary (>= 0.3.6, < 0.5)
+      pathutil (~> 0.9)
+      rouge (>= 3.0, < 5.0)
+      safe_yaml (~> 1.0)
+      terminal-table (>= 1.8, < 4.0)
+      webrick (~> 1.7)
+    jekyll-default-layout (0.1.5)
+      jekyll (>= 3.0, < 5.0)
+    jekyll-include-cache (0.2.1)
+      jekyll (>= 3.7, < 5.0)
+    jekyll-readme-index (0.3.0)
+      jekyll (>= 3.0, < 5.0)
+    jekyll-relative-links (0.7.0)
+      jekyll (>= 3.3, < 5.0)
+    jekyll-sass-converter (3.0.0)
+      sass-embedded (~> 1.54)
+    jekyll-seo-tag (2.8.0)
+      jekyll (>= 3.8, < 5.0)
+    jekyll-watch (2.2.1)
+      listen (~> 3.0)
+    just-the-docs (0.8.2)
+      jekyll (>= 3.8.5)
+      jekyll-include-cache
+      jekyll-seo-tag (>= 2.0)
+      rake (>= 12.3.1)
+    kramdown (2.4.0)
+      rexml
+    kramdown-parser-gfm (1.1.0)
+      kramdown (~> 2.0)
+    liquid (4.0.4)
+    listen (3.9.0)
+      rb-fsevent (~> 0.10, >= 0.10.3)
+      rb-inotify (~> 0.9, >= 0.9.10)
+    mercenary (0.4.0)
+    pathutil (0.16.2)
+      forwardable-extended (~> 2.6)
+    public_suffix (6.0.1)
+    rake (13.2.1)
+    rb-fsevent (0.11.2)
+    rb-inotify (0.11.1)
+      ffi (~> 1.0)
+    rexml (3.3.5)
+      strscan
+    rouge (4.3.0)
+    safe_yaml (1.0.5)
+    sass-embedded (1.77.8)
+      google-protobuf (~> 4.26)
+      rake (>= 13)
+    strscan (3.1.0)
+    terminal-table (3.0.2)
+      unicode-display_width (>= 1.1.1, < 3)
+    unicode-display_width (2.5.0)
+    webrick (1.8.1)
+
+PLATFORMS
+  arm64-darwin
+  x86_64-linux
+
+DEPENDENCIES
+  jekyll (= 4.3.3)
+  jekyll-default-layout (= 0.1.5)
+  jekyll-readme-index (= 0.3.0)
+  jekyll-relative-links (= 0.7.0)
+  just-the-docs (= 0.8.2)
+
+BUNDLED WITH
+   2.5.17

+ 59 - 0
website/README.md

@@ -0,0 +1,59 @@
+# Documentation website
+
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+<!-- toc -->
+
+## Table of contents
+
+-   [Overview](#overview)
+-   [Build process](#build-process)
+-   [Development tips](#development-tips)
+-   [Domain configuration](#domain-configuration)
+
+<!-- tocstop -->
+
+## Overview
+
+Carbon's main website is the GitHub project page. Carbon remains too early and
+experimental to have a full-fledged website.
+
+This directories contains infrastructure for building the convenience
+documentation website, which should be at <https://docs.carbon-lang.dev>.
+
+## Build process
+
+Website generation is done by
+[gh_pages_deploy.yaml](/.github/workflows/gh_pages_deploy.yaml). It runs
+`prebuild.py`, which prepares files for website generation, then builds the
+website using Jekyll, configured through `_config.yml`.
+
+## Development tips
+
+[rbenv](https://github.com/rbenv/rbenv) can be used to set up Ruby and `bundle`.
+
+To run a server, run `bundle exec jekyll serve`. See
+[Jekyll docs](https://jekyllrb.com/docs/usage/) for more commands.
+
+To update the `Gemfile.lock` after `Gemfile` changes, run `bundle update`.
+
+## Domain configuration
+
+The
+[custom domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site)
+is configured in three places:
+
+-   GitHub organization
+    [verified domains](https://github.com/organizations/carbon-language/settings/pages)
+-   GitHub repository
+    [custom domain](https://github.com/carbon-language/carbon-lang/settings/pages)
+-   Google Cloud DNS
+    -   This is visible with `dig docs.carbon-lang.dev`
+
+Note all of these require admin permissions to modify. For sharing test pages, a
+GitHub user and repository can be used, pushing to `<username>.github.io` (or
+getting a custom DNS setup).

+ 25 - 0
website/_config.yml

@@ -0,0 +1,25 @@
+# Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+title: Carbon Language documentation
+description: An experimental successor to C++
+
+# https://just-the-docs.github.io/just-the-docs/
+theme: just-the-docs
+
+favicon_ico: '/favicon.png'
+
+aux_links:
+  Discord: https://discord.gg/ZjVdShJDAs
+  GitHub: https://github.com/carbon-language/carbon-lang
+
+mermaid:
+  # Version of mermaid library
+  # Pick an available version from https://cdn.jsdelivr.net/npm/mermaid/
+  version: '10.9.1'
+
+# We add frontmatter to files, but still want README.md -> index.md.
+readme_index:
+  remove_originals: true
+  with_frontmatter: true

+ 33 - 0
website/_includes/footer_custom.html

@@ -0,0 +1,33 @@
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+<!--
+Used from:
+https://github.com/just-the-docs/just-the-docs/blob/main/_includes/components/footer.html
+-->
+<footer>
+  <p><a href="#top" id="back-to-top">Back to top</a></p>
+
+  <p>
+    <a
+      href="https://github.com/carbon-language/carbon-lang/blob/trunk/{{ page.path }}"
+      id="edit-this-page"
+      >View on GitHub</a
+    >
+  </p>
+
+  <p class="text-small">
+    Part of the Carbon Language project, licensed under the
+    <a href="https://github.com/carbon-language/carbon-lang/blob/trunk/LICENSE"
+      >Apache License v2.0 with LLVM Exceptions</a
+    >.
+  </p>
+
+  <p class="text-small">
+    This site is hosted on GitHub Pages and uses
+    <a href="https://github.com/just-the-docs/just-the-docs">Just the Docs</a>.
+  </p>
+</footer>

+ 12 - 0
website/_includes/nav_footer_custom.html

@@ -0,0 +1,12 @@
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+<!--
+Used from:
+https://github.com/just-the-docs/just-the-docs/blob/main/_includes/components/sidebar.html
+
+This just removes the nav footer; we use `footer_custom.html` instead.
+-->

+ 47 - 0
website/_sass/custom/custom.scss

@@ -0,0 +1,47 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// This is modifying the builtin scss, particularly:
+// https://github.com/just-the-docs/just-the-docs/blob/main/_sass/layout.scss
+
+.site-nav {
+  // The navbar will typically overflow, so keep a scrollbar so that the zippies
+  // don't move on open/close.
+  overflow-y: scroll;
+}
+
+.side-bar {
+  @include mq(lg) {
+    // Allow left alignment instead of centering.
+    width: #{$nav-width};
+  }
+
+  & + .main {
+    @include mq(lg) {
+      // Allow left alignment instead of centering.
+      margin-left: #{$nav-width};
+    }
+  }
+}
+
+.site-footer {
+  color: #57606a;
+}
+
+// Comments in code.
+.highlight .c {
+  color: #57606a;
+}
+
+// Misc syntax in code.
+.highlight .nb,
+.highlight .nt,
+.highlight .nv {
+  color: #a36000;
+}
+
+// Strings in code.
+.highlight .s2 {
+  color: #3f7f3f;
+}

+ 14 - 0
website/_sass/custom/setup.scss

@@ -0,0 +1,14 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+// This is modifying the builtin scss, particularly:
+// https://github.com/just-the-docs/just-the-docs/blob/main/_sass/color_schemes/light.scss
+// https://github.com/just-the-docs/just-the-docs/blob/main/_sass/support/_variables.scss
+
+// Prefer wider content.
+$nav-width: 20rem;
+$content-width: 60rem;
+
+$code-background-color: #f6f8fa;
+$body-text-color: #1f2328;

BIN
website/favicon.png


+ 9 - 0
website/implementation.md

@@ -0,0 +1,9 @@
+# Implementation
+
+<!--
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+-->
+
+These documents cover components of Carbon's reference implementation.

+ 198 - 0
website/prebuild.py

@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+
+"""Updates files in preparation for a jekyll build.
+
+Used from .github/workflows/gh_pages.yaml. This updates the file and directory
+structure prior to the jekyll build.
+"""
+
+import dataclasses
+import os
+from pathlib import Path
+import re
+from typing import Optional
+
+__copyright__ = """
+Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+Exceptions. See /LICENSE for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""
+
+
+@dataclasses.dataclass
+class ChildDir:
+    """Tracks whether a child directory has grandchildren."""
+
+    title: str
+    has_grandchildren: bool = False
+
+
+def get_title(f: Path, content: str) -> str:
+    """Returns a file's title according to markdown.
+
+    Replacements are for YAML compatibility in `add_frontmatter`.
+    """
+    m = re.search("^# (.*)$", content, re.MULTILINE)
+    assert m, f
+    title = m[1]
+    title = title.replace("\\", "\\\\")
+    title = title.replace('"', '\\"')
+    title = re.sub("`([^`]+)`", r"<code>\1</code>", title)
+    return title
+
+
+def add_frontmatter(
+    f: Path,
+    orig_content: str,
+    titles: list[str],
+    nav_order: Optional[int],
+    has_children: bool,
+) -> None:
+    """Adds frontmatter to a file."""
+    content = "---\n"
+
+    assert len(titles) <= 3
+    content += f'title: "{titles[-1]}"\n'
+    if len(titles) >= 2:
+        content += f'parent: "{titles[-2]}"\n'
+    if len(titles) == 3:
+        content += f'grand_parent: "{titles[-3]}"\n'
+
+    if nav_order is not None:
+        content += f"nav_order: {nav_order}\n"
+    if has_children:
+        # has_toc is disabling the list of child files in a separate table of
+        # contents.
+        content += "has_children: true\nhas_toc: false\n"
+    content += "---\n\n" + orig_content
+    f.write_text(content)
+
+
+def label_subdir(
+    subdir_str: str,
+    top_nav_order: int,
+    parent_title: Optional[str] = None,
+    grandchild_dirs: bool = False,
+) -> None:
+    """Automatically adds child information to a subdirectory's markdown files.
+
+    This is in support of navigation.
+    """
+    assert not (parent_title and grandchild_dirs)
+
+    subdir = Path(subdir_str)
+    readme = subdir / "README.md"
+    readme_content = readme.read_text()
+
+    readme_title = get_title(readme, readme_content)
+    readme_titles = [readme_title]
+    if parent_title:
+        readme_titles.insert(0, parent_title)
+    children = [x for x in subdir.glob("**/*.md") if x != readme]
+    add_frontmatter(
+        readme, readme_content, readme_titles, top_nav_order, bool(children)
+    )
+
+    if grandchild_dirs:
+        # When adding grandchildren, we cluster files by child directory.
+        child_dirs = {}
+        for child in children:
+            if child.name == "README.md":
+                title = get_title(child, child.read_text())
+                child_dirs[child.parent] = ChildDir(title)
+        for child in children:
+            if child.name != "README.md" and child.parent in child_dirs:
+                child_dirs[child.parent].has_grandchildren = True
+
+    for child in children:
+        child_content = child.read_text()
+        child_title = get_title(child, child_content)
+        child_nav_order = None
+
+        if subdir_str == "proposals":
+            # Use proposal numbers as part of the title and ordering.
+            m = re.match(r"p(\d+).md", child.name)
+            # Skip files that aren't proposals.
+            if not m:
+                continue
+            child_title = f"#{m[1]}: {child_title}"
+            child_nav_order = int(m[1])
+
+        titles = [readme_title, child_title]
+        has_children = False
+        if parent_title:
+            titles.insert(0, parent_title)
+        elif grandchild_dirs and child.parent in child_dirs:
+            if child.name == "README.md":
+                has_children = child_dirs[child.parent].has_grandchildren
+            else:
+                dir_title = child_dirs[child.parent].title
+                titles.insert(1, dir_title)
+
+        add_frontmatter(
+            child, child_content, titles, child_nav_order, has_children
+        )
+
+
+def label_root_file(
+    name: str, title: str, top_nav_order: int, has_children: bool = False
+) -> None:
+    """Adds frontmatter to a root file, like CONTRIBUTING.md."""
+    f = Path(name)
+    add_frontmatter(f, f.read_text(), [title], top_nav_order, has_children)
+
+
+def main() -> None:
+    # Ensure this runs from the repo root.
+    os.chdir(Path(__file__).parent.parent)
+
+    # bazel-execroot interferes with jekyll because it's a broken symlink.
+    Path("bazel-execroot").unlink()
+
+    # The external symlink is created by scripts/create_compdb.py, and can
+    # interfere with local execution.
+    external = Path("external")
+    if external.exists():
+        external.unlink()
+
+    # Move files to the repo root.
+    for f in Path("website").iterdir():
+        if f.name == "README.md":
+            continue
+        f.rename(f.name)
+
+    # Use an object for a reference.
+    nav_order = [0]
+
+    # Returns an incrementing value for ordering.
+    def next(nav_order: list[int]) -> int:
+        nav_order[0] += 1
+        return nav_order[0]
+
+    label_root_file("README.md", "Home", next(nav_order))
+    label_root_file("CONTRIBUTING.md", "Contributing", next(nav_order))
+    label_subdir("docs/design", next(nav_order), grandchild_dirs=True)
+    label_subdir("docs/guides", next(nav_order))
+    label_subdir("docs/project", next(nav_order), grandchild_dirs=True)
+    label_subdir("docs/spec", next(nav_order))
+    # Provide a small file to cluster implementation-related directories.
+    label_root_file(
+        "implementation.md",
+        "Implementation",
+        next(nav_order),
+        has_children=True,
+    )
+    label_subdir("utils", next(nav_order))
+    label_subdir("proposals", next(nav_order))
+    label_root_file("CODE_OF_CONDUCT.md", "Code of conduct", next(nav_order))
+    label_root_file("SECURITY.md", "Security policy", next(nav_order))
+
+    # Reset the order for the implementation children.
+    nav_order[0] = 0
+    label_subdir("toolchain", next(nav_order), parent_title="Implementation")
+    label_subdir("explorer", next(nav_order), parent_title="Implementation")
+    label_subdir("testing", next(nav_order), parent_title="Implementation")
+
+
+if __name__ == "__main__":
+    main()