diff --git a/.github/scripts/site-check.py b/.github/scripts/site-check.py index 31a8df5..cdcb3a5 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 @@ -38,25 +43,26 @@ - a theme feature name the Zensical UI 0.0.41 does not know (the list below is what its templates and bundle test for; `search.suggest` and `search.share` are not in it); - `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; + and a modern config (the default) 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. +Plugins are read in every form Zensical accepts (a table, or a list of names and one-entry +tables; `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) +SWALLOW = {"title", "textarea", "xmp", "plaintext", "noembed", "noframes"} 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 +91,156 @@ } -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