fix(headings): keep an image's alt text in the heading anchor - #333
Open
harsh-thakkar7 wants to merge 1 commit into
Open
harsh-thakkar7 wants to merge 1 commit into
harsh-thakkar7 wants to merge 1 commit into
Conversation
`extract_heading_text`'s docstring promises that images keep their visible text, but its regex had no capture group on the image branch, so `` was deleted whole. A heading that is only an image then produced an empty slug, and with it: - the document outline listed the section as a blank row with an empty anchor, - the preview rendered the heading with no `id` at all, so it could not be linked to at all, - the AI table of contents silently omitted the section entirely. `## ` became an unanchorable heading. The image branch now captures its alt text, and the substitution keeps both branches' groups (`\1\2`), so an image contributes its label exactly the way a link already did. That change alone would have desynchronised the anchors: the preview slug is built from the rendered heading's text nodes, and an `<img>` has none, so `_HeadingIdAssigner` would have gone on dropping the alt text while the outline and table of contents started using it. `_HeadingIdAssigner` now counts an image's `alt` while inside a heading, in both the start-tag and self-closing paths, which restores the agreement `assign_heading_ids` is documented to keep. Adds 12 tests. The central one asserts the invariant directly — the anchors produced by the outline, the AI table of contents and the rendered preview are equal for a document covering images, bold, code, links, strikethrough, Unicode and duplicates — because a disagreement between the three is what makes a table-of-contents link silently stop resolving. 8 of 8 mutants killed; 8 of the new tests fail against the previous implementation. Nested markup such as `[](url)` is mis-parsed by the link branch, identically before and after this change, and is left alone here.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A heading containing an image had no anchor at all, was blank in the document outline, and was silently omitted from the AI table of contents.
Root cause
backend/heading_anchor.pydocuments whatextract_heading_textis supposed to do:The regex had a capture group on the link branch but not on the image branch:
The replacement is
sub(r"\1", …), and the image branch matches no group, sowas deleted whole — alt text included.Effect
For
## , onmain:{text: '', anchor: ''}<h2>with noidattributeSo the section existed in the document, showed up in the outline as an empty row, could not be linked to from anywhere, and vanished from the generated TOC.
Headings that merely contained an image were also wrong, just less obviously:
##  Resultsbecame#results, silently discarding "Architecture diagram" even though that text is visible in the preview.The fix has two halves, and both are required
1.
backend/heading_anchor.py— keep the alt textThe image branch captures its label and the substitution keeps both branches' groups, so an image contributes its text exactly the way a link already did:
2.
backend/renderer.py— and this part is not optionalThe preview slug is built from the rendered heading's text nodes, and an
<img>has none. Fixing only part 1 would have produced#architecture-diagram-resultsin the outline and TOC while the preview still renderedid="results"— a silent regression that breaks exactly the links this module exists to keep working.assign_heading_idsis documented to emit ids that "exactly match the anchors produced bybackend.heading_anchor", so_HeadingIdAssignernow counts an image'saltwhile a heading is open. It is wired into bothhandle_starttagandhandle_startendtag, because markdown2 emits<img … />(self-closing) and only the self-closing handler would otherwise ever run.html.parserhands thealtattribute over already decoded, so it is used as-is — that is precisely what the preview displays.Result
## ,##  Resultsand## Results now all agree across the three anchor producers:id## architecture-diagramarchitecture-diagramarchitecture-diagram##  Resultsarchitecture-diagram-resultsarchitecture-diagram-resultsarchitecture-diagram-results## Results results-chartresults-chartresults-chartDuplicate image headings still disambiguate GitHub-style (
diagram,diagram-1,diagram-2) in all three.Testing
12 new tests in
tests/test_markdown_outline.py.The one that matters most asserts the invariant directly rather than case by case:
over a document covering image-only headings, trailing images, bold/italic, inline code, links, a strikethrough inside a link label, Unicode, duplicates and nesting levels. A disagreement between the outline, the TOC and the preview is precisely what makes a table-of-contents link silently stop resolving, so the property is what is pinned.
The rest: alt text preserved as an outline label, image-only heading anchored in all three, image heading listed in the TOC, alt text participating in a mixed heading, several images joined with the rest of the text, an
&in alt text, duplicate image headings disambiguated, and headings without images unchanged.Three feed
assign_heading_idsHTML directly to cover what markdown2 cannot produce: an<img>written without the self-closing slash (otherwise thehandle_starttagpath is dead code as far as the suite is concerned), a non-<img>element carrying analtattribute proving only images are consulted, and four non-image headings confirming nothing else moved.8 of 8 mutants killed: the image alt dropped again, the renderer not counting alt at all, alt prepended instead of appended, heading text cleared after reading alt, only the self-closing path handled, alt read from any tag, the link label dropped as collateral, and the replacement reduced to a single group.
8 of the new tests fail against the previous implementation.
I did not assert one case on purpose:
## [](url). The link branch matches[Report ![coveras the label, so the nested form is mis-parsed — and it is mis-parsed identically before and after this change, because the defect is in the link branch's non-greedy[^\]]*, not in the image branch. Fixing it means recursing over nested inline markup, which is a larger and riskier change than this one; flagging it rather than quietly widening the diff.Verification
pytest tests/test_markdown_outline.py tests/test_markdown_toc.py— 35 passedpytest tests— unchanged failures, +12 passingruff check .— cleanruff format --check— cleanTested against the installed
python-docx/markdown2versions via the repo's own locked environment.