Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[help wanted] HTML as a graph: sections, anchored links, and markup→code edges (sibling of #409 / #414)

Open
#417 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
25/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
cpp
Domain
cli, tooling

Research direction

Start with slice A: read the Markdown grammar setup in CMakeLists.txt, the language table in src/ingest_crawl.h, and the document handling in src/docparse.h and src/ingest_docs.h. The maintainers are taking A; slice B depends on A and the changes from #414. For a chosen slice, follow its listed files and red-first checks; done means its acceptance gate passes with the specified HTML behavior.

Written by the indexing model from the issue text.

Description

help wanted

Claim a slice in this issue first. @muayyad-alsadi, after your work on #414, slice B (anchored links) is a natural next step if you'd like it. It reuses the exact (file, anchor) bind you built for markdown. Size: each slice S–M, independent after slice A. Pattern to copy: PR #414 (markdown caller/callee).

The problem, in user terms

#409 / #414 turn Markdown into a graph: headings are sections, links are edges, and a doc link is marked via="link", so it never passes for a code caller. HTML gets none of this today. A tiny site (index.html loads static/app.js, has <button onclick="saveDoc()">, links to docs/guide.html#config, and has an inline <script>) gives:

ripwire . --json          files=5 symbols=12 edges=0 unindexed={"css":1}
                          index.html → one sec "index"; docs/guide.html → one sec "guide"
ripwire . --callers=saveDoc      count="0"   (the onclick in index.html is invisible)
ripwire . --grep=inlineHelper    2 hits in index.html, but it is no symbol: inline <script> is not indexed
ripwire . --recall="config file" serves the whole of guide.html, nav text included, as one unit

Why, on main:

  • src/docparse.h:56 puts .html/.htm in the EXTRACTOR tier (DocKind::Html). extractHtml (:411) strips tags and drops <script>/<style> bodies (:421).
  • src/ingest_docpass.h:213-214 adds ONE whole-file Section (lang=Markdown, named by the file stem). There are no headings, no anchors, and no links.
  • No HTML grammar is vendored (CMakeLists.txt has the markdown block grammar, and no tree-sitter-html). Inline JS is dropped, yet the file is not counted in unindexed=, so the miss is silent.
  • Markdown→HTML links drop as well. emitLinkTarget (src/ingest_docs.h:316) keeps only .md/.markdown targets, so [guide](docs/guide.html#config) makes no edge.

Proposal, ranked by value / cost

Each item lists the edge, how it binds, and how it stays honest. The rule across all of them: markup is never a code caller. Every new edge kind carries its own via= and stays out of callers=/count=/impact_reaches=, the way #414 handles links. Any scan that might miss something says so in a *_floor / unindexed= field.

  1. Sections + anchors (shared with markdown; cheap, high value). Vendor tree-sitter-html (MIT, the same pin style as the other grammars). h1–h6 become Section defs. Each section's span runs to the next heading at the same or a higher level, and its scope is the nearest shallower heading (the markdown rule). Every id= (and legacy <a name=>) is an anchor target attached to its enclosing section. Text inside <nav>, <header>, <footer> and <aside> is left out of a section's body, so --recall ranks the main content (the readability/trafilatura idea, made deterministic by using landmark tags only). If the page has <main>/<article>, prefer it. A file without headings stays one whole-file section, and --recall says section-granular only when that is true.
  2. Anchored doc links (copy #414 directly). <a href="file#id"> binds exactly to (file, anchor). In-file href="#id" binds to the section that holds that id. Markdown↔HTML links work in both directions (lift the .md-only filter). Rows are via="link". A missing anchor falls back to the file and is marked anchor="missing", and it also shows as a --doc-drift row. http(s):, mailto: and links that leave the repo are counted (external_links=), never bound. Payoff: "what links here" = --callers on a page or section, and the nav graph = --callees.
  3. Inline <script> indexed as JS + <script src> / <link href> as include edges (medium). Parse each inline script's byte range with the existing JavaScript grammar, at its real offsets so p=index.html:16 is right (inlineHelper becomes a symbol and its loadConfig() call becomes a real call edge). src/href become file→file via="include" edges, which are dependencies, not calls. <style> and .css stay unindexed (see "not now").
  4. Event-handler attributes → callback REGISTRATION sites (medium; ties into ripwire's callback/indirection work). onclick="saveDoc()" makes the section that holds the element point to saveDoc with via="handler". That is the place the callback is registered, not a call. Bind only against functions the page can see: its <script src> files plus its inline scripts. Otherwise the row is hedged (bind="name"). --callers=saveDoc then lists it under its own handlers= count, and count= stays 0.
  5. Literal string binds between markup and JS (medium, all exact-literal-only). customElements.define('my-widget', MyWidget) ↔ <my-widget> uses. getElementById('x') / querySelector('#x') / querySelector('.c') ↔ the element that has id="x" / class="c". Rows are via="selector" / via="element". A non-literal argument goes to selector_unresolved=N, never to a guess. Compound CSS selectors bind only on a single simple selector, and every other selector counts in the floor.
  6. Page metadata as section text (cheap). <title>, <meta name=description>, OpenGraph og:* and JSON-LD "@type"/"name" feed the file section's name and recall text (the extruct idea). Tables and lists (<table>, <ul>/<ol>) are served whole by --expand and never split mid-row (docling/unstructured treat them as units).
  7. <form action> / literal fetch('/api/x') ↔ server routes (higher cost; maybe its own issue). Bind only to a literal route decorator or registration (@app.route("/api/save"), app.post('/api/save', …)), with via="route". This needs a route table per framework, so it starts with Flask and Express and discloses route_unbound=.

Separate issues (please don't fold them in here): template languages (Jinja/Django {% include/extends %}, Handlebars partials, ERB/EJS), because .html alone cannot tell them apart and they need their own include-graph rules; and Vue/Svelte SFCs, whose <script> is component code and belongs with the TS/JS language work.

Not doing, and why: the full DOM or the CSS cascade (specificity and inheritance are runtime semantics, and any answer would be a guess); markup created at runtime (innerHTML, JSX output, server rendering), which cannot be seen statically, so disclose it, don't infer it; fetching external URLs; and LLM entity extraction or embeddings (the GraphRAG/LightRAG approach). ripwire answers are deterministic and byte-reproducible, and an LLM-built edge cannot be gated or re-checked.

Slices for contributors (copy #414's shape: a fixture dir, a test/*check.sh gate, red-first arms)

  • A. Sections + anchors (needed by B–E). The maintainers are taking this slice, so B–E have a base to build on. Comment here if you want to pair on it. Touch: CMakeLists.txt (add_ts_grammar(html …)), src/ingest_crawl.h (kLangTable row), a new src/ingest_html.h beside src/ingest_docs.h, and src/docparse.h (move .html from the extractor tier to the grammar tier, keeping the sibling-completeness guard). Red-first: --outline=docs/guide.html lists Guide, Config file (sc=Guide) and Advanced; --recall="config file" serves only the Config file section, and the nav text from index.html is in no section body. Near miss: a page with no headings is one section and is not called section-granular.
  • B. Anchored links (after A, plus #414 merged). Touch: the link emitter in ingest_html.h, the (file, anchor) bind in src/graph.h that #414 adds, and src/docdrift.h. Red-first: --callers='Config file' lists index.html::Installation and README.md::Links, both via="link"; href="guide.html#removed" gives anchor="missing" and one --doc-drift row; --safe-delete/--impact rows carry via="link" (the #414 review lesson).
  • C. Inline scripts + includes. Touch: ingest_html.h (hand each script range to the JS parse at its offset) and the RawRef cache (src/ingest_cache.h, keep it cache-stable like #414). Red-first: --callers=loadConfig shows inlineHelper (p=index.html:16); --callees=index.html shows static/app.js with via="include"; and count= of loadConfig does not include the include edge.
  • D. Handlers + custom elements + selectors. Touch: ingest_html.h (attribute scan), src/graph.h (new via kinds), and the legends src/compactlegend.h / src/graphlegend.h. Red-first: --callers=saveDoc → count="0" handlers="1" and a via="handler" row from index.html::Usage; --callers=MyWidget → a via="element" row. Near miss: getElementById(name) gives selector_unresolved="1" and no row.
  • E. Metadata + table/list units. Touch: ingest_html.h and the --expand span rule. Red-first: a JSON-LD "name" is found by --recall, and --expand of a section that holds a table returns the whole <table>.

Acceptance (every slice)

  • bash test/langidentitycheck.sh BASE HEAD: non-HTML languages are SAME. HTML DIFFs are waived by --intended rows that name the slice's gate.
  • No existing callers=/count=/impact_reaches= value changes for code-only repos. --quality-delta gates 0, and the CONTRIBUTING definition of done applies.

Prior art (HTML→structure only; we copy the deterministic parts)


If nobody has claimed a slice by the time our next integration train starts, we may pick it up ourselves. Claiming one only takes a comment here.

Dominant language
C++
Stars
2.5k
Forks
158
Avg merge
2d 9h
Merged PRs (30d)
55

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from redhat-et/ripwire

All issues in redhat-et/ripwire

Similar issues

More C++ issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.