You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ci: check relative Markdown links and anchors - #575
scripts/check_markdown_links.py resolves every relative link and image target in tracked Markdown outside openvmm/, and every anchor against GitHub's heading slugs, ignoring fenced and inline code.
Runs in .github/actions/check-quality with its unit tests (scripts/test_check_markdown_links.py).
Fixes the one broken anchor it finds: aci_edge_sandboxes/README.md linked #native-proxy-and-forwarded-ports, a heading renamed to "Native proxy and host loopback".
Test
python scripts/check_markdown_links.py passes on this branch.
Parse Markdown destinations with titles and balanced parentheses
scripts/check_markdown_links.py:33
This expression misses or truncates valid Markdown destinations. [guide](missing.md 'Guide') and [guide](<missing guide.md>) produce no targets, so broken links pass CI. [guide](guide(v2).md) produces guide(v2, so an existing file is reported missing. Parse angle-delimited destinations, balanced/escaped parentheses, and Markdown title delimiters, and add regression cases for these forms.
Decode HTML entities before generating heading slugs
scripts/check_markdown_links.py:93
HTML entities are not decoded before generating heading slugs. For example, # Fish & Chips renders with anchor fish--chips, but this checker generates fish-amp-chips. It therefore rejects the valid anchor and accepts an incorrect one. Decode entities after processing heading markup but before restoring stashed code spans, so entity text inside code remains literal, and add regression cases for both forms.
Preserve inline-code-only Setext heading anchors
scripts/check_markdown_links.py:104
A Setext heading containing only inline code, such as a line containing Setup in backticks followed by =====, loses its anchor: masking turns the heading text into whitespace, so the blank-line check discards it. GitHub generates setup, but this checker rejects links to it. Use nonblank placeholders for code spans during heading detection, while still masking fences, and add a regression case.
Strip query strings before resolving link paths
scripts/check_markdown_links.py:165
Query strings are treated as part of the filesystem path. Valid links such as README.md?plain=1 or image.png?raw=true are reported missing even when the file exists, causing the quality job to reject valid documentation. Strip the query before resolving the path, while retaining the fragment for anchor validation.
Markdown link destinations with parentheses and titles are misparsed
scripts/check_markdown_links.py:35
The destination regex misparses valid Markdown: [guide](guide(v2).md) yields guide(v2, while [guide](missing.md 'Guide title') and [guide](<missing guide.md>) are missed entirely. This can reject existing files or silently accept broken links. Parse balanced and escaped parentheses, angle-delimited destinations, and Markdown title delimiters, and add regression cases for these forms.
Fenced code blocks inside Markdown containers are not ignored
scripts/check_markdown_links.py:61
Fence detection ignores Markdown containers. A block quote containing > ~~~, > [example](missing.md), and > ~~~ leaves the code example visible to link extraction, causing a false CI failure. List-item fences indented four spaces have the same problem; blank lines also prevent the code-span masker from hiding backtick examples. Track block-quote prefixes and list-relative indentation when opening and closing fences, and add regression cases for both containers.
HTML entities are not decoded when generating heading slugs
scripts/check_markdown_links.py:95
HTML entities are not decoded when generating heading slugs. The Markdown heading # Salt & Pepper renders as “Salt & Pepper”, whose anchor is salt--pepper, but this code generates salt-amp-pepper and rejects the valid link. Decode entities after removing markup and before restoring stashed code spans, so entities written inside code remain literal. Add regression tests for both cases.
Query strings are incorrectly treated as part of relative filenames
scripts/check_markdown_links.py:167
A relative URL such as README.md?plain=1 is checked as a literal filename containing ?plain=1, so it fails the quality gate even when README.md exists. Remove the query before resolving the path, but before percent-decoding so encoded question marks in filenames remain intact.
Nothing in CI checked the repository's relative links, so a renamed heading
broke its anchors silently. scripts/check_markdown_links.py resolves every
relative link and image target, and every anchor against GitHub's heading
slugs, ignoring code. Fix the one anchor it finds broken.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Fences close only on their own character and length. Setext headings, code
spans of any backtick length, multiline labels, and reference-style
definitions are handled. Slugs come from rendered heading text, and duplicate
suffixes skip slugs already taken. Each rule has a regression test.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
A backtick in one paragraph must not pair with one in another, since Markdown
ends a paragraph at a blank line. Add a regression case.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
Destinations keep balanced parentheses and drop titles and angle brackets.
Images nested in link labels are checked, and each target reports the line of
its opening bracket. Fences may be indented inside list items. Heading slugs
decode HTML entities outside code spans, and query strings are not part of
the path. Each form has a regression test.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23606 <4602056+jay23606@users.noreply.github.com>
Reference links with multiline or angle-bracket destinations are misparsed
scripts/check_markdown_links.py:40
Reference destinations can begin on the following line or use angle brackets around paths containing spaces. For [g]:\n missing.md, this regex finds no target, so a broken reference link passes CI. For [g]: <dir with space/g.md>, it checks the truncated target dir instead. Allow the optional line break and parse bracketed destinations separately from bare destinations.
Fenced code blocks inside blockquotes are not recognized
scripts/check_markdown_links.py:61
Fences inside blockquotes are not recognized because their lines start with >. A block containing > ~~~, > [example](missing.md), and > ~~~ therefore reports a broken link even though GitHub renders it as code. Track blockquote containers when opening and closing fences so quoted code examples do not fail the quality job.
Heading links with parentheses are normalized incorrectly
scripts/check_markdown_links.py:92
Heading normalization still uses a link regex that stops at the first closing parenthesis. For # [Guide](guide(v2).md), it produces guidemd instead of GitHub's guide, rejecting valid #guide links and accepting nonexistent #guidemd anchors. Extract heading labels with balanced destination parsing, including the title forms supported by the link checker.
Inline-code Setext headings are incorrectly treated as blank
scripts/check_markdown_links.py:127
Masking inline code makes a code-only Setext heading appear blank. A line containing nvx_tools in backticks followed by =========== defines #nvx_tools on GitHub, but anchors_in() returns no anchor and valid links fail CI. Use fence-masked text with inline code preserved when checking whether the heading paragraph contains content.
The code-span regex treats escaped opening backticks as delimiters. For example, \`[guide](missing.md)\` is masked completely, although the escaped backticks leave a live Markdown link. Broken links can therefore pass CI. Respect backslash parity when recognizing opening delimiters, without treating backslashes inside an actual code span as escapes, and add regression coverage.
Reference destinations fail with newlines or spaces
scripts/check_markdown_links.py:39
Reference destinations can start on the next line or contain spaces inside angle brackets. Here, [g]:\n missing.md produces no target, while [g]: <dir with space/g.md> produces dir. This lets broken reference links pass CI and can reject valid ones. Parse the complete destination in both forms and add regression tests alongside the inline-destination tests.
Fenced blocks with container prefixes are misdetected
scripts/check_markdown_links.py:62
Fence detection does not recognize blockquote or list-marker prefixes. A quoted fence such as > ~~~\n> [example](missing.md)\n> ~~~ exposes the example as a link. With `- ```\n example\n ````, the closing fence becomes an opener, so subsequent live links are also hidden through the end of the document. Track container prefixes and boundaries when masking fences, and test both forms so CI neither rejects code examples nor skips real links.
Headings in blockquotes and lists are missed
scripts/check_markdown_links.py:114
Headings inside blockquotes and list items are missed: both > ## Setup and - ## Setup produce an empty anchor set, although they render as headings with a setup anchor on GitHub. Valid links therefore fail CI, and a later duplicate heading receives the wrong suffix. Recognize container prefixes before extracting heading text, preserve code-block masking, and test standalone and duplicate headings inside containers.
A backslash escape before ASCII punctuation is part of the Markdown syntax,
so guide\(v2\).md names guide(v2).md in inline and reference links. HTML
attribute values keep their backslashes. Regression tests cover both.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Signed-off-by: jay23462 <4602056+jay23606@users.noreply.github.com>
I'm closing this pull request as superseded by #571, which fixed #563 as well and has merged into dev. It adds its checker to the same Lint, typing, and formatting job, fixes the same anchor in aci_edge_sandboxes/README.md, and also checks the doc/ paths that tracked files mention. On the current dev, this branch's checker finds no broken link that the merged one misses.
If you find a Markdown link that the merged checker gets wrong, please open an issue.
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
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.
Fixes #563
Summary
scripts/check_markdown_links.pyresolves every relative link and image target in tracked Markdown outsideopenvmm/, and every anchor against GitHub's heading slugs, ignoring fenced and inline code..github/actions/check-qualitywith its unit tests (scripts/test_check_markdown_links.py).aci_edge_sandboxes/README.mdlinked#native-proxy-and-forwarded-ports, a heading renamed to "Native proxy and host loopback".Test
python scripts/check_markdown_links.pypasses on this branch.python -m unittest discover -s scripts -p test_check_markdown_links.pypasses (6 tests).openvmm/are skipped: it is vendored and was not checked out in my clone, so two links into it could not be verified here.🤖 Generated with Claude Code