Skip to content

fix(context): keep formatted and linked text in section titles - #175

Open
JayOfTheKeyboard wants to merge 1 commit into
neuledge:mainfrom
JayOfTheKeyboard:fix/heading-formatted-text
Open

JayOfTheKeyboard wants to merge 1 commit into
neuledge:mainfrom
JayOfTheKeyboard:fix/heading-formatted-text

Conversation

@JayOfTheKeyboard

Copy link
Copy Markdown
Contributor

Problem

getHeadingText builds a section's title from the heading's direct text and inlineCode children only. Text inside a link, **strong** or *emphasis* is one level deeper, so it is dropped. When the whole heading is a link or bold, the title comes out empty, and the empty string falls through to the "content before the first h2" branch, so the section is filed as "Introduction".

sectionTitle has the highest weight in search (bm25(chunks_fts, 5.0, 10.0, 1.0)), so the lost words are the ones that count most.

HTML docs are hit hardest, because turndown turns Sphinx's cross-references into links inside headings. Python 3.14 (python-3.14-docs-html, the registry's source), before and after:

Heading On main With this PR
Numeric Types — int, float, complex Numeric Types — , , Numeric Types — int, float, complex
Text Sequence Type — str Text Sequence Type — Text Sequence Type — str
Constants added by the site module Constants added by the module Constants added by the site module
OrderedDict objects objects OrderedDict objects
Why are Python strings immutable? (FAQ) Introduction Why are Python strings immutable?

The FAQ and several guides wrap every heading in a back-link to the page's contents (<a class="toc-backref">), so the whole title is link text.

Counts from parsing every page:

Docs Sections Sections whose title loses words on main Filed as "Introduction" on main, but have a heading
Python 3.14 HTML (579 pages) 6,475 300, in 30 pages 152
NestJS, tRPC, Kysely, Drizzle (Markdown) 3,374 3 1

The Markdown cases: tRPC's ## Using [superjson](...) and ## Using [devalue](...) are both titled "Using ", and Drizzle's FAQ ## **Should I use generateorpush?** is filed as "Introduction".

Fix

  1. getHeadingText reads inline text recursively (getInlineText): text and inlineCode values, and the children of any node that has them. Nodes without text (html, image, break) are still skipped, as before.
  2. That alone would put permalink symbols into HTML titles. Sphinx and systemd's man pages put <a class="headerlink" href="#...">¶</a> in every heading, rustdoc uses §, VuePress # and Docusaurus a zero-width space. The old code only dropped them by accident, as link text. A turndown rule now drops an <a href="#..."> inside an <h2> whose text is only one of those symbols. The <h2> is the heading that becomes the section title. The rule matches on text, not class names, so it covers generators outside the registry too. It is a rule rather than turndown.remove(), because turndown's link rule matches <a> before remove() is consulted.

The rule is limited to <h2> on purpose. The permalinks in <h3> and below are part of section content, and they count toward isTableOfContents's link ratio. Dropping them everywhere let the intro section of 35 Python pages, Sphinx's navigation bar included, pass that filter for the first time. That is a separate question, so this PR leaves the content alone: every section body is identical to main, apart from one line in using/cmdline.html where an ## ...¶ heading already sat inside a body and now reads without the anchor.

Not changed

  • Section content, section count and chunking: the same 6,475 Python sections and 3,374 Markdown sections, with identical bodies and token counts (except the one line above).
  • Permalink anchors in <h3> and below stay in content, as before. Dropping them would change which sections the table-of-contents filter keeps, as described above. Happy to follow up if you want that.
  • While measuring I noticed that in using/cmdline.html, "1.2. Environment variables" gets no section of its own. Its <h2> ends up inside the last part of "1.1. Command line". I have not looked into why. It is separate from this change.

Test

Two tests. Both fail on fc503b7 and pass with the fix:

  • build.test.ts: a link, a fully bold heading with inline code, and emphasis give Using superjson, Should I use generate or push? and The strict option. On main: Using , Introduction, The option.
  • html.test.ts: Sphinx markup (a toc-backref question, a <code> cross-reference, permalinks), a rustdoc § anchor and a Docusaurus zero-width anchor nested in a <span> give Why are Python strings immutable?, Constants added by the site module, Traits and Setup. An <h3> permalink stays in the content. On main the first two are Introduction and Constants added by the module.

I broke each part of the fix in turn, and each time a test went red:

Mutation Test that fails
no recursion into children Markdown
inlineCode not read Markdown
no permalink rule HTML (titles end in "¶")
rule applies to every heading HTML (the <h3> permalink disappears from content)
direct <h2> parent only, instead of closest("h2") HTML (Setup keeps its zero-width space)
§ not treated as a permalink HTML (Traits§)
zero-width space not stripped HTML (Setup keeps its zero-width space)

Validation

  • context: 269 tests pass (267 before). registry: 103 pass.
  • tsc -p tsconfig.build.json and biome ci --error-on-warnings are clean.
  • Real docs: the before/after comparison above, over the full Python 3.14 HTML archive and the four Markdown doc sets.
  • Changeset: .changeset/keep-heading-formatting.md (patch).

getHeadingText read only a heading's direct text and inlineCode children, so text inside emphasis, strong and links was dropped. A heading that was entirely a link or bold got an empty title and fell through to "Introduction". Read inline text recursively, and drop permalink anchors (a # link whose text is only ¶, §, # or a zero-width space) inside <h2>, which would otherwise now end up in the title.
@changeset-bot

changeset-bot Bot commented Sep 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 496088a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@neuledge/context Patch
@neuledge/registry Patch

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

JayOfTheKeyboard added a commit to JayOfTheKeyboard/JayOfTheKeyboard that referenced this pull request Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant