Repository navigation
fix(context): measure visible link text when skipping tables of contents - #190
JayOfTheKeyboard wants to merge 4 commits into
Conversation
🦋 Changeset detectedLatest commit: 151e483 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Changes requested — This fix restores useful prose, but Sphinx's generated Python index pages now enter search too. Please keep those pages out of the index before merging.
Changes behavior: yes — searches gain recovered prose and generated navigation catalogs.
Must fix
- Keep Sphinx index pages out of search. (
packages/context/src/build.ts:68)
This codebase is run by Human0.
| // Check link ratio - TOC sections are mostly links. Measure the text a reader | ||
| // sees: counting each link's URL would make prose with a few long URLs look | ||
| // like a TOC and drop it. | ||
| const linkPattern = /\[([^\]]*)\]\([^)]+\)/g; |
There was a problem hiding this comment.
Must fix: Keep generated Sphinx indexes out of search.
This change lets Sphinx's genindex and py-modindex pages through because their generated navigation and footer text lowers the link ratio. These alphabetical link catalogs can surface instead of useful documentation in search; exclude the affected pages or remove that generated text before applying the ratio.
|
Thanks, good catch. With visible link text, I tried your second suggestion first. Removing Sphinx's navigation bar and footer before measuring is not enough on its own: So 888119a leaves the pages out instead. Python 3.14 docs, same
The difference is exactly those six pages. Every other page keeps the sections this PR recovered, and no page has fewer sections than on The new tests in
|
There was a problem hiding this comment.
Changes requested — The link-ratio fix restores useful prose and excludes Sphinx index catalogs, but the new filter can also hide real HTML documentation pages.
Changes behavior: yes — searches now include prose with long links and omit generated Sphinx indexes.
Must fix
- Only skip actual Sphinx index pages.
This codebase is run by Human0.
| * Markdown parser for section extraction and chunking. | ||
| */ | ||
| export function parseHtml(source: string, filePath: string): ParsedDoc { | ||
| if (SPHINX_INDEX.test(source)) { |
There was a problem hiding this comment.
Must fix: Only skip actual Sphinx index pages.
This checks for a bare indextable class anywhere in the raw HTML and returns no sections before HTML cleanup. A real guide can contain that class on its own table, or show Sphinx markup in a code sample or comment; when parsed, the whole page disappears from search without an error. Match the actual Sphinx index template or gate the exclusion on index filenames, rather than matching this class anywhere in the source.
There was a problem hiding this comment.
Done in a65a2f9. A page is now skipped only when it has both a Sphinx index file name (genindex*.html, or a domain index such as py-modindex.html) and an actual <table class="indextable"> or <div class="genindex-jumpbox"> tag. Escaped markup in a code sample doesn't match the tag pattern. A guide with its own indextable table, a guide showing the markup in a code sample and a comment, and a genindex.html without Sphinx's markup are all still indexed, with tests for each. The Python 3.14 docs give the same output as the previous commit: 538 pages, 5,331 sections, no page differs.
There was a problem hiding this comment.
The ordinary-guide issue is improved but not fully settled: [a-z]+-[a-z]*index also matches names like api-index.html, so a guide with that name and an indextable can still disappear from search. I also found a separate performance issue in the raw-HTML check; the current review asks to narrow the filename match and bound the check.
This codebase is run by Human0.
There was a problem hiding this comment.
Approved — Prose with long URLs now remains searchable without admitting generated Sphinx index catalogs; both earlier review concerns are resolved at this head.
Changes behavior: no — This corrects what counts as visible link text and preserves the existing exclusion of generated index catalogs; it adds no new user-facing capability or contract.
This codebase is run by Human0.
There was a problem hiding this comment.
Changes requested — The link-text fix restores useful prose, but the new HTML filter can still hide ordinary pages and take excessive time on crafted input.
Changes behavior: yes — search now keeps prose with long URLs and excludes generated Sphinx index pages.
Must fix
- Restrict the filter to actual Sphinx index pages.
- Keep the raw HTML check bounded.
This codebase is run by Human0.
| // them by the file name Sphinx gives them and a tag its index templates emit; a guide | ||
| // with only one of the two is still indexed. | ||
| const SPHINX_INDEX_FILE = | ||
| /(?:^|\/)(?:genindex(?:-[^/]+)?|[a-z]+-[a-z]*index)\.html?$/; |
There was a problem hiding this comment.
Must fix: Restrict the filter to actual Sphinx index pages.
The [a-z]+-[a-z]*index pattern also matches ordinary names such as api-index.html because * allows no characters before index. If that guide contains its own indextable table, the filter returns no sections and silently removes its content from search; a literal matching tag in a comment can also satisfy the raw-source check. Match Sphinx's actual names, such as genindex*.html and *-modindex.html, and add a test that a non-Sphinx api-index.html page remains indexed.
| // with only one of the two is still indexed. | ||
| const SPHINX_INDEX_FILE = | ||
| /(?:^|\/)(?:genindex(?:-[^/]+)?|[a-z]+-[a-z]*index)\.html?$/; | ||
| const SPHINX_INDEX_MARKUP = |
There was a problem hiding this comment.
Must fix: Keep the raw HTML check bounded.
A truncated page with repeated <table or <div tokens makes this pattern repeatedly scan and backtrack to the end of the input; a 448 KB input took about 30 seconds in testing. Since the filter runs on raw HTML before parsing, indexing one crafted page can stall the whole build. Bound the scan or check tag heads in a linear pass.
|
Thanks for the fix. The reviewer found two remaining issues on This codebase is run by Human0. |
There was a problem hiding this comment.
Approved — The index filter now preserves ordinary guides while excluding Sphinx catalogs, and the full Python 3.14 docs output matches the previous commit (538 pages, 5,331 sections).
Changes behavior: no — This corrects the promised visible-link measurement and keeps generated indexes out of search without adding a new user-facing capability or contract.
This codebase is run by Human0.
|
Thanks for the careful fix. Before this can merge, please address two remaining issues:
This codebase is run by Human0. |
isTableOfContents counted each link's whole Markdown, URL included, as link text, so a prose paragraph with a few long links read as "mostly links" and its section was dropped from the index. Compare the link text a reader sees with the rest of the visible text instead. A list of links is still skipped.
Measuring visible link text lets Sphinx's genindex pages and py-modindex.html through the table-of-contents check: their list markup, plain-text entries, module synopses and page chrome keep them under the 50% link ratio. Stripping Sphinx's navigation bar and footer does not change that for genindex-E, -O, -Symbols, -all or py-modindex.html, so recognise the pages by the classes Sphinx's index templates emit (indextable, genindex-jumpbox) and return no sections for them.
The previous check dropped any HTML page whose source contained an indextable or genindex-jumpbox class, including a guide that uses the class on its own table or shows Sphinx's markup in a code sample. Require both a Sphinx index file name (genindex*.html or a domain index such as py-modindex.html) and an actual <table class="indextable"> or <div class="genindex-jumpbox"> tag. The Python 3.14 docs give the same sections as before (538 pages, 5,331 sections, no page differs).
api-index.html matched the domain-index pattern because the part before "index" could be empty, so a guide with an indextable was dropped. Domain indexes are <domain>-<index>.html and the only built-in one is py-modindex, so match that. The markup pattern's [^>]* ran to the end of the page from every unclosed <table, which took about 11 seconds for 40,000 of them. It now stops at the next < or >.
a65a2f9 to
151e483
Compare
|
Both fixed in 151e483, and the branch is rebased onto current
All 360 tests in |
|
Thanks for the update and for adding regression coverage for both cases. The reviewer will assess the new head before we decide on merging. This codebase is run by Human0. |
There was a problem hiding this comment.
Changes requested — Readers can now find prose sections that were previously dropped as link-heavy, while generated Sphinx index pages remain excluded.
Changes behavior: no — This corrects the parser's handling of sections intended to be searchable; the explicit Sphinx-index exclusion preserves pages that were already omitted.
Must fix
- Use a random changeset filename (
.changeset/toc-link-text-ratio.md:1)
1 note, no action needed
- Suggestion: Keep Sphinx index handling easy to find Sphinx-file filtering also exists in
packages/registry/src/zip.ts, with a narrower filename-only rule. These run at different stages, but a comment linking the two policies could help future changes stay consistent. (packages/context/src/html.ts:96)
This codebase is run by Human0.
| @@ -0,0 +1,5 @@ | |||
| --- | |||
There was a problem hiding this comment.
Must fix: Use a random changeset filename
The repository's CLAUDE.md asks for a random changeset filename, but this one uses the feature name. Please rename it to a random name; its contents can remain descriptive.
| * Markdown parser for section extraction and chunking. | ||
| */ | ||
| export function parseHtml(source: string, filePath: string): ParsedDoc { | ||
| if (SPHINX_INDEX_FILE.test(filePath) && SPHINX_INDEX_MARKUP.test(source)) { |
There was a problem hiding this comment.
Question: Could an HTML example trigger the Sphinx index skip?
A genuine page named genindex*.html that includes raw <table class="indextable"> markup in a code sample would be skipped too. Is that exclusion intentional?
Problem
isTableOfContentsskips a section when more than half of its content is links. It counted each link's whole Markdown, URL included, as link text:A prose paragraph with two or three long URLs therefore reads as "mostly links" and is left out of the index. tRPC's AWS Lambda adapter page is one example. Its section saying which API Gateway versions are supported has 5 links. A reader sees 59 characters of link text in 322 (18%), but counted with their URLs the links are 436 of 699 characters (62%), so the section is dropped. It opens:
HTML is hit hardest, because turndown writes Sphinx's cross-references with relative paths and title attributes (
[`PyObject`](structures.html#c.PyObject "PyObject")) and every definition has a[¶](#c.PyDict_Check "Link to this definition")permalink. Most Python C API pages are one long section, so the whole page goes.I parsed every file with
parseDocumentonmainand with this change. The Python run uses the registry definition'sexclude_paths:Python pages that produce no sections on
maininclude the C API pages fordict(10,403 tokens),list,set,tuple,datetime,sequenceandmonitoring, pluslibrary/errno.html,library/token.html,library/asyncio-exceptions.htmlandlibrary/keyword.html. The 51 still empty with the fix are the chapter contents pages (library/index.html,contents.html,library/datatypes.html, ...) and Sphinx's generated index pages (genindex*.html,py-modindex.html).In the Markdown sets, the sections this change brings back are prose: tRPC's AWS Lambda and edge-runtime introductions and its
useUtilshelper table, the Drizzle release notes' "Fixes" lists, and Kysely's merge examples. I read all of them.End to end, building a package from
c-api/dict.html,library/errno.htmland tRPC'saws-lambda.mdwithbuildPackageand then callingsearch:PyDict_SetDefaultdict.html > Introduction (part 5)ECONNREFUSEDerrno.html > Introduction (part 5)API Gateway HTTP API v2AWS Lambda + API Gateway Adapter > AWS Lambda adapterThe package has 3 sections (1,493 tokens) on
mainand 25 sections (16,943 tokens) with the fix.Fix
Measure what a reader sees. Each link is replaced by its text, the link text is summed along the way, and the ratio is link text over the resulting visible text. The 50% threshold is unchanged. Because only the URL and bracket characters are removed, the new ratio is never higher than the old one: this change can only keep a section that was dropped, never drop one that was kept.
Sphinx's generated index pages (
genindex*.html,py-modindex.html) are alphabetical link catalogs, and their list markup and plain-text entries hold them under the ratio once URLs no longer count.parseHtmlnow returns no sections for a page that has both a Sphinx index file name (genindex*.html, or a domain index such aspy-modindex.html) and a tag from Sphinx's index templates (<table class="indextable">,<div class="genindex-jumpbox">). A page with only one of the two is still indexed. No content page in the Python docs has either class, and no page has fewer sections than onmain.Not changed
table of contents,toc,contents,index).counts its alt text as link text). Treating them separately changed no section in the Markdown sets and 11 of 5,437 in Python, so I left it out.Test
html.test.ts: a genindex landing page, a letter page andpy-modindex.htmlproduce no sections, and a normal page with the same Sphinx chrome is still indexed. On the previous commit the three index cases fail. Three more pages stay indexed: a guide with its ownindextabletable, a guide that shows Sphinx's index markup in a code sample and a comment, and a page namedgenindex.htmlwithout Sphinx's markup. Dropping either half of the check fails one of them.build.test.ts: a section of prose with two long AWS links is kept, and a section that is a bulleted list of three links is still skipped.On the parent commit the test fails: the prose section is dropped (
[]instead of["AWS Lambda adapter"]). With the fix it passes.I broke each part of the fix in turn, and each time the test went red:
Validation
vitest runinpackages/context: 18 files, 351 tests pass.biome ci --error-on-warnings: clean.tsc --noEmit -p tsconfig.build.json: clean..changeset/toc-link-text-ratio.md(patch).