diff --git a/.github/pages/build.py b/.github/pages/build.py index b8074af..20dc645 100644 --- a/.github/pages/build.py +++ b/.github/pages/build.py @@ -224,7 +224,12 @@ def render(markdown): if heading: level = len(heading.group(1)) text = heading.group(2).strip() - out.append('%s' % (level, slug(text), inline(text), level)) + anchor = slug(text) + body = inline(text) + if anchor and "%s' % (level, anchor, body, level)) seen_prose = True i += 1 continue diff --git a/.github/pages/template.html b/.github/pages/template.html index 3e76614..8379663 100644 --- a/.github/pages/template.html +++ b/.github/pages/template.html @@ -103,15 +103,25 @@ /* ---- prose ---- */ h1, h2, h3 { - font-family: var(--mono); font-weight: 700; - letter-spacing: -0.035em; line-height: 1.25; text-wrap: balance; + font-family: var(--mono); font-weight: 500; + letter-spacing: -0.03em; line-height: 1.25; text-wrap: balance; } + + /* headings link to themselves, prefixed with their markdown level */ + .anchor { color: inherit; text-decoration: none; } + h2 .anchor::before, h3 .anchor::before { + color: var(--ink-3); font-weight: 400; margin-right: .5ch; + } + h2 .anchor::before { content: "##"; } + h3 .anchor::before { content: "###"; } + .anchor:hover { text-decoration: none; } + .anchor:hover::before { color: var(--accent-ink); } h1 { font-size: clamp(1.9rem, 4.6vw, 2.3rem); margin: 0 0 .3em; } h2 { font-size: 1.26rem; margin: 2.2em 0 1em; padding-bottom: .4em; border-bottom: 1px solid var(--rule); } - h3 { font-size: 1rem; font-weight: 500; letter-spacing: -0.025em; margin: 1.5em 0 .45em; } + h3 { font-size: 1rem; letter-spacing: -0.025em; margin: 1.5em 0 .45em; } /* the wordmark leads the page; the h1 stays for readers and search */ .sr-only { @@ -209,7 +219,11 @@ border: 1px solid var(--rule); border-radius: 8px; } .doc th, .doc td { text-align: left; padding: 15px 18px; vertical-align: top; } - .doc thead th { background: var(--surface-2); font-weight: 600; border-bottom: 1px solid var(--rule); } + .doc thead th { + background: var(--surface-2); border-bottom: 1px solid var(--rule); + font-family: var(--mono); font-weight: 500; font-size: .84rem; + letter-spacing: -0.02em; color: var(--ink-2); + } .doc tbody tr + tr td { border-top: 1px solid var(--rule-soft); } .doc td:first-child { font-weight: 600; } /* key/value rows: hold the value column to a readable measure */ diff --git a/CLAUDE.md b/CLAUDE.md index da9e256..7e2ac07 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ python3 install.py # --cli-only, --dev, --prefix, --verify, --uninstal # Pipe a single command (non-interactive; cli.py detects a non-tty stdin) echo '/search "q2 plan"' | python3 -m glean_code -# Run the full test suite (1,103 tests, stdlib unittest โ€” works with or without pytest) +# Run the full test suite (1,108 tests, stdlib unittest โ€” works with or without pytest) python3 -m pytest tests/ python3 -m unittest discover tests/ diff --git a/README.md b/README.md index f8767ee..44f3f28 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ The full Glean Code REPL โ€” slash commands, status bar, mock/live switching, se | ๐Ÿ” **[SSO / OAuth](docs/SSO_OAUTH.md)** ยท **[Secure tokens](docs/SECURE_TOKENS.md)** | Browser sign-in, secure refs, the masking matrix | | ๐Ÿ”Œ **[MCP server](docs/MCP.md)** | Glean as native tools in Claude Code, Claude Desktop, Cursor | | ๐Ÿ›๏ธ **[Architecture](docs/ARCHITECTURE.md)** ยท **[REST paths](docs/REST_PATHS.md)** | Module map, request flow, endpoints, how to add a command | -| โœ… **[Testing](docs/TESTING.md)** | Running the 1,103-test suite and what it covers | +| โœ… **[Testing](docs/TESTING.md)** | Running the 1,108-test suite and what it covers | | ๐Ÿ›Ÿ **[Support](SUPPORT.md)** ยท **[Changelog](CHANGELOG.md)** | How to report a bug ยท release history | > [!NOTE] diff --git a/docs/TESTING.md b/docs/TESTING.md index fac4262..39c71c0 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -21,13 +21,13 @@ files, and they outrank the `Glean Code.app` launcher in `Cmd+Space`: export PYTHONPYCACHEPREFIX="$HOME/.cache/python" ``` -1,103 tests covering the client and every mock response, commands and dispatch, config, UI, auth, completion, help docs, the mock corpus, indexing-walk, scaffold, the installer, the MCP server, the flow mapper, the Pages site builder, and Glean Personal (text extraction, the index, the content graph, ranking explanations, local mode, and the local MCP tools). +1,108 tests covering the client and every mock response, commands and dispatch, config, UI, auth, completion, help docs, the mock corpus, indexing-walk, scaffold, the installer, the MCP server, the flow mapper, the Pages site builder, and Glean Personal (text extraction, the index, the content graph, ranking explanations, local mode, and the local MCP tools). ## Development notes Notes on the test suite added during development of glean-code-cli. -All 1,103 tests pass. Here's what was added across the development passes: +All 1,108 tests pass. Here's what was added across the development passes: `tests/test_commands_extended.py` (155 new tests) โ€” covers all previously untested commands: diff --git a/tests/test_pages_build.py b/tests/test_pages_build.py index c023855..b978e93 100644 --- a/tests/test_pages_build.py +++ b/tests/test_pages_build.py @@ -88,6 +88,21 @@ class TestBlocks(unittest.TestCase): def test_heading_gets_an_anchor_id(self): self.assertIn('

', build.render("## Quickstart")) + def test_heading_links_to_itself(self): + out = build.render("## Quickstart") + self.assertEqual( + out, '

Quickstart

') + + def test_every_heading_level_gets_an_anchor(self): + out = build.render("# One\n\n## Two\n\n### Three") + self.assertEqual(out.count('class="anchor"'), 3) + self.assertIn('href="#three"', out) + + def test_a_heading_that_already_holds_a_link_is_not_double_wrapped(self): + out = build.render("## See [the docs](docs/COMMANDS.md)") + self.assertNotIn('class="anchor"', out) + self.assertEqual(out.count("' && x\n```") self.assertIn('
', out)
@@ -136,7 +151,8 @@ def test_note_alert(self):
     def test_raw_html_passes_through_untouched(self):
         out = build.render('
\n\n# Glean Code\n\n
') self.assertIn('
', out) - self.assertIn('

Glean Code

', out) + self.assertIn( + '

Glean Code

', out) self.assertIn("
", out) def test_horizontal_rule(self): @@ -283,6 +299,15 @@ def test_tagline_is_set_in_the_mono_face(self): rule = self.css[self.css.index('[align="center"] .banner + p {'):] self.assertIn("font-family: var(--mono)", rule[:rule.index("}")]) + def test_headings_carry_their_markdown_level(self): + self.assertIn('h2 .anchor::before { content: "##"; }', self.css) + self.assertIn('h3 .anchor::before { content: "###"; }', self.css) + + def test_headings_and_table_headers_use_the_mono_face(self): + for selector in ("h1, h2, h3 {", ".doc thead th {"): + rule = self.css[self.css.index(selector):] + self.assertIn("font-family: var(--mono)", rule[:rule.index("}")], selector) + def test_image_rows_do_not_stretch_their_items(self): row = self.css[self.css.index("p.imgrow {"):] self.assertIn("align-items: center", row[:row.index("}")])