From 94769c118560aaf817d0fe92930410bf78f9b4ac Mon Sep 17 00:00:00 2001 From: GeiserX <9169332+GeiserX@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:30:56 +0100 Subject: [PATCH 1/2] fix(docs): check raw HTML links and #fragments in the site check The old site check read links with a regex that only saw double-quoted attributes, skipped full URLs pointing back into the site and never looked at #fragments, so a dead anchor or a single-quoted raw HTML link could ship without a failure. This brings in the shared check that parses the HTML and verifies every in-site link and fragment, and corrects the concurrency comment, which overstated the push-order guarantee. --- .github/scripts/site-check.py | 216 +++++++++++++++++++++++++--------- .github/workflows/docs.yml | 5 +- 2 files changed, 162 insertions(+), 59 deletions(-) diff --git a/.github/scripts/site-check.py b/.github/scripts/site-check.py index 31a8df5..03b449f 100644 --- a/.github/scripts/site-check.py +++ b/.github/scripts/site-check.py @@ -10,14 +10,19 @@ Exit 1 on any of these, each printed as one ERROR line: - a link or image whose target is not in the built site: a nav entry whose page does not - exist, a missing image, any other dangling href or src (Zensical validates Markdown page - links and anchors; it does not validate images, nav entries or raw HTML links). A redirect - stub's own links (canonical and body) are checked the same way; + exist, a missing image, any other dangling href or src, in any quoting and with HTML + entities decoded (Zensical validates Markdown page links; it does not validate images, + nav entries or raw HTML links). A full URL into the site itself + (https://geiserx.github.io//...) is checked the same way. A redirect stub's own + links (canonical and body) are checked too; + - a link whose #fragment is not an id on the target page, including links that come in + through a snippet include (Zensical 0.1.0 does not check those; MkDocs strict did); - an href ending in .md: a correct page link is rewritten to a directory URL, so a .md left in the HTML is a raw-HTML or unprocessed link, and it opens the llmstxt Markdown copy; - a root-absolute link (/getting-started/) outside the site's own path prefix: it breaks under https://geiserx.github.io// (MkDocs' `absolute_links: warn`, which Zensical - ignores); + ignores). The prefix comes from site_url, else the home page's canonical link; with + neither, that is an error rather than a check that cannot fire; - a page that no navigation entry links to: a stray file under docs/ published at a public URL that nothing reaches (MkDocs' `nav.omitted_files: warn`, which Zensical ignores); - a .py, .pyc or __pycache__ entry in the site: the awesome lists keep their Markdown @@ -25,12 +30,12 @@ - an awesome list's count token left in a page (``, `n:categorias`, `n:retirados`): the Markdown extension that fills them in did not run. Only those three names: CashPilot writes other `` comments of its own in its pages; - - a raw , <textarea>, <xmp>, <plaintext>, <noembed> or <noframes> tag inside a page's - article: a browser's HTML parser treats everything after it as text, so the rest of the - page disappears (Markdown passes `<title>` written in prose through as a real tag; - Telegram-Archive shipped that way); - - a "Copy as Markdown" button (data-md-copy-url) whose Markdown copy does not exist: the - llmstxt plugin writes the copies, and only for pages its `sections` globs match. + - a raw <title>, or an unclosed <textarea>, <xmp>, <noembed>, <noframes>, or any + <plaintext>, in a page's article outside inline SVG and MathML: a browser's HTML parser + treats everything after it as text, so the rest of the page disappears (Markdown passes + `<title>` written in prose through as a real tag; Telegram-Archive shipped that way); + - a "Copy as Markdown" button (data-md-copy-url) whose Markdown copy does not exist in the + site: the llmstxt plugin writes the copies, and only for pages its `sections` globs match. And, from zensical.toml (Zensical drops what it does not know without a word; under MkDocs strict an unknown plugin aborted the build): - a plugin name Zensical 0.1.0 does not know (`redirect` for `redirects`, `llmstxtt`): the @@ -40,23 +45,25 @@ - `theme.variant` other than "modern" or "classic" (anything else is classic, silently), and a "modern" config whose index.html does not load the modern stylesheet; - a `redirect_maps` entry whose stub was not written; - - a `[project.plugins.llmstxt]` table with no site/llms.txt, or `content.action.copy` in - the features with a page that carries no copy button. + - an enabled llmstxt plugin with no site/llms.txt, or `content.action.copy` in the + features with a page that carries no copy button. +Plugin keys are read the way Zensical reads them (`material/redirects` is `redirects`), and +a plugin with `enabled = false` is treated as off. With directory URLs off (the offline +plugin turns them off) every .html file is a page. Stdlib only (tomllib needs Python 3.11+), so it runs under `python -I` on any runner. """ import re import sys import tomllib +from html.parser import HTMLParser from pathlib import Path from urllib.parse import unquote, urlparse SWALLOW = re.compile(r"<(title|textarea|xmp|plaintext|noembed|noframes)\b", re.IGNORECASE) +FOREIGN = re.compile(r"<(svg|math)\b.*?</\1\s*>", re.IGNORECASE | re.DOTALL) TOKEN = re.compile(r"<!--\s*n:(?:proyectos|categorias|retirados)\s*-->") ARTICLE = re.compile(r"<article\b.*?</article>", re.DOTALL) -ATTR = re.compile(r'\s(?:href|src)="([^"]*)"') -COPY = re.compile(r'\sdata-md-copy-url="([^"]*)"') -NAV = re.compile(r'<a\s+href="([^"]*)"\s+class="md-(?:nav|tabs)__link') STUB = "<title>Redirecting..." # Keys of _PLUGIN_UNSUPPORTED_OPTIONS in zensical/config.py (0.1.0, same set as 0.0.74): every other name is @@ -85,16 +92,86 @@ } -def body_urls(body: str) -> set[str]: - return set(ATTR.findall(body)) + + +class Page(HTMLParser): + """The parts of one built HTML file the checks read, from a real HTML parser: any + quoting, entities decoded, nothing taken from