Repository navigation
feat(parser): index HTML pages, their scripts and anchored sections - #1159
2 commits merged into
Conversation
A .html file gave nothing at all. Inline scripts now go to the JS/TS parsers with the page's own line numbers, and classic scripts on a page share one scope, so one block can call what another defines. A script src becomes an import of the local file, and an inline handler that calls a function on the page becomes a call. A div, section or article with an id becomes a DocSection node, connected to its file by CONTAINS, as #521's review asked. It holds the anchor and first heading, so that is what search can find; the body text is not indexed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
code-review-graph reviewOverall risk: 0.65 (MEDIUM) — 72 changed function(s)/class(es), 53 affected flow(s), 15 test gap(s) (13 with no tested caller found, 2 reached only through a caller) Risk-scored changes
Affected execution flows
Test gaps
Reached only through a callerNo test names these directly, but a tested caller reaches them along the call path shown. That is a path in the call graph, not a record of execution -- the caller's tests may never take this branch. Treat it as where to look, not as coverage: these count as gaps and are scored as untested.
Token savings: this graph-backed report used ~115,715 fewer tokens (~27%) than reading every changed file in full (estimated, chars/4 approximation). Powered by code-review-graph — local-first analysis; no code leaves the CI runner. |
There was a problem hiding this comment.
🟡 Changes recommended
Critical scope and call-resolution errors merge distinct module definitions, expose module-private functions, misresolve imported bindings, and create false or missing call edges.
7 open issues
Parse handler call expressions instead of matching raw text · New Let module imports shadow classic script definitions · New Preserve module identity and scope during extraction · New Resolve calls to member-assigned functions in HTML · New Include HTML entities when extracting heading text · New Generate test mirrors for extracted test nodes · New Canonicalize and exclude root-relative script paths · New
What changed in this PR
Adds HTML pages to the code-review graph, making inline code and anchored document sections discoverable through existing search and graph queries.
Changes:
- Extracts inline JavaScript/TypeScript, local script imports, handler calls, and anchored
DocSectionnodes. - Integrates HTML with cross-language caller queries, CLI filtering, and graph visualization.
- Adds parser and integration tests, a representative fixture, and documentation.
| File | Description |
|---|---|
tests/test_visualization.py |
Checks DocSection visualization support. |
tests/test_html_parser.py |
Covers HTML extraction, limits, search, and graph queries. |
tests/fixtures/sample_html.html |
Supplies scripts, handlers, and anchored sections for tests. |
README.md |
Adds HTML to supported formats. |
docs/USAGE.md |
Documents HTML support and DocSection nodes. |
docs/schema.md |
Defines DocSection metadata and relationships. |
docs/COMMANDS.md |
Documents DocSection query filters. |
docs/architecture.md |
Lists HTML among targeted parser formats. |
code_review_graph/visualization.py |
Adds DocSection styling and filtering. |
code_review_graph/parser.py |
Implements HTML detection and extraction. |
code_review_graph/main.py |
Updates tool documentation for DocSection filters. |
code_review_graph/graph.py |
Includes HTML in the JavaScript language family. |
code_review_graph/docs/LLM-OPTIMIZED-REFERENCE.md |
Adds HTML to the served language reference. |
code_review_graph/cli.py |
Accepts DocSection as a search kind. |
CHANGELOG.md |
Records HTML support, limits, and rebuild guidance. |
Review effort: Balanced
| } | ||
| for event, value, line in handlers: | ||
| seen: set[str] = set() | ||
| for called in _HTML_HANDLER_CALL_RE.findall(value): |
There was a problem hiding this comment.
Fixed in 2610e84: handlers are now read with the JavaScript grammar and only a call of a plain name counts, so strings, comments and obj . known() no longer link.
| self._extract_from_tree( | ||
| root, text, script_lang, file_path, nodes, edges, | ||
| import_map={**shared_imports, **import_map}, | ||
| defined_names=shared_names | defined_names, |
There was a problem hiding this comment.
Fixed in 2610e84: a name a module imports is taken out of the page globals for that module, so its import wins.
| self._extract_from_tree( | ||
| root, text, script_lang, file_path, nodes, edges, | ||
| import_map={**shared_imports, **import_map}, | ||
| defined_names=shared_names | defined_names, | ||
| ) |
There was a problem hiding this comment.
Partly fixed in 2610e84: inline handlers now reach only classic-script functions, as they run in global scope. Giving each module block its own node identity changes how page nodes are named, so I have left that out of this PR.
| all_nodes.extend(nodes) | ||
| all_edges.extend(edges) |
There was a problem hiding this comment.
Fixed in 2610e84: script calls now go through _resolve_call_targets over what each block can see, so app.handle() reaches page.html::app.handle. Test added.
| if part.type == "text": | ||
| words.append(part.text.decode("utf-8", errors="replace")) | ||
| else: | ||
| parts.extend(reversed(part.children)) |
There was a problem hiding this comment.
Fixed in 2610e84: entity leaves are kept and joined with no gap where the page has none, so Café reads Café and & alone gives &.
| extra={"html_event": event}, | ||
| )) | ||
|
|
||
| if test_file: |
There was a problem hiding this comment.
Fixed in 2610e84: TESTED_BY edges are now made whenever the page has test nodes, as in the normal path, not only on test pages.
| candidate = self._repo_root / relative.lstrip("/") | ||
| if candidate.is_file(): | ||
| resolved = normalize_file_path(candidate) |
There was a problem hiding this comment.
Fixed in 2610e84: a root-relative src is now made canonical (.. stops at the root, as in a browser) and a forgotten file stays bare.
… a browser does - Inline handlers are read with the JavaScript grammar, so a name in a string, a comment or after a dot is no longer a call, and they reach only the page's classic functions, not a module's. - A name a module imports shadows a page global of that name. - Script calls go through the same-file resolver, so app.handle() reaches the app.handle the page assigns. - Headings keep their entities, so Café reads Café. - A test_ function on any page links what it calls, as in a .js file. - A root-relative src is made canonical, and a forgotten file stays bare. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


What was wrong
A
.htmlor.htmfile gave nothing: no file node, no functions, no way to find a section of a long page. Functions in inline scripts could not be found, and a script file could not say which pages load it.What changed
This follows the review on #521.
type="module"script keeps its names to itself.application/json,text/x-templateand so on) are not read as code.DocSectionis a new node kind. Adiv,sectionorarticlewith an id becomes a node named after its anchor (#cover), with aCONTAINSedge from the file.<script src>becomes anIMPORTS_FROMedge to the local file. URLs and template expressions add no edge. A local path that does not resolve keeps its raw text, as JS imports do.onclick="openSearch(1)"becomes a call, but only for functions the page defines. Otherwise those functions would look dead.htmljoins the JavaScript language family, socallers_ofcan cross from a page to a script file.Style blocks and heading anchors stay out, as the review asked.
Limits
html_truncated). They are most likely generated.html_truncated.html_id_occurrences. Browsers do the same, and two nodes with one name would collide.{{ item.id }}) is not an anchor.An existing graph needs
buildto pick up pages, becauseupdateonly re-reads changed files.Left out
<a href>links between pages or to anchors.srcpaths resolved through a web root other than the repo root. These keep their raw text.Real numbers
Fresh builds from staging (115dccf) and from this branch, on copies of each repo.
pallets/flaskh5bp/html5-boilerplatemdn/learning-areaBuild times on runs this short are mostly noise.
learning-areagrows most because 154 of its 212 indexed files are HTML. It gains 42DocSectionnodes and 51 import edges from pages to scripts.Code symbols in the top 10 search hits, before → after:
session10 → 10,blueprint10 → 10,render_template6 → 6,config8 → 8,login7 → 7fetch5 → 5,canvas0 → 0,button0 → 3,draw4 → 4,game0 → 6analytics,index,webpack,manifest,main: all 0 → 0No term got worse. The html5-boilerplate zeros mean nothing either way, since that repo gains only two file nodes (
src/index.htmlandsrc/404.html) and one script import, and has almost no code.Spot checks
I read the claimed source line for a random sample on
learning-area: 5DocSection, 5Function, 5CALLS, 5IMPORTS_FROMand 5CONTAINS, plus 3 each ofFunction,CALLSandCONTAINSon flask. All matched. Examples:css/web-fonts/fonts/cicle_fina-demo.html:286#glyphs, heading "Language Support", on<div id="glyphs">accessibility/css/mouse-and-keyboard-events.html:30showImgonfunction showImg() {javascript/apis/third-party-apis/nytimes/finished/index.html→.../finished/script.json<script src="script.js" defer></script>Tests
tests/test_html_parser.py(57 tests) andtests/fixtures/sample_html.html. They cover what the review listed: multiple and nested scripts, external and empty scripts, malformed HTML, duplicate ids, unquoted and case-varied attributes,.htm, line offsets, import and call edges, and full-build graph queries..code-review-graphmarker. Both pass from the normal temp folder. I then tidied type hints only, and re-ran the closest 718 tests, which pass. CI runs them all.Fixes #521
🤖 Generated with Claude Code