diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ff2edff7..df87a7af 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,5 @@ -# Dependency updates (docs/CI-CD.md CI-13): once a week per ecosystem, minor and patch updates in -# one pull request, each major in a pull request of its own. Every ignore says why on the line +# Dependency updates (docs/CI-CD.md CI-13): once a week per ecosystem (the docs pin once a month), +# minor and patch updates in one pull request, each major in a pull request of its own. Every ignore says why on the line # above it; tests/dependabot.test.ts fails on an ignore without a reason. # # Merge these with an explicit subject and body (`gh pr merge --squash --subject ... --body ...`): @@ -46,3 +46,12 @@ updates: groups: actions-minor-and-patch: update-types: [minor, patch] + + # The docs site's one pin (zensical). Monthly, not weekly: Zensical releases every week or two, + # and the strict build and site check in the Docs workflow prove each bump. + - package-ecosystem: "pip" + directory: "/docs" + schedule: + interval: "monthly" + commit-message: + prefix: "build(docs)" diff --git a/.github/scripts/site-check.py b/.github/scripts/site-check.py new file mode 100755 index 00000000..03b449f4 --- /dev/null +++ b/.github/scripts/site-check.py @@ -0,0 +1,335 @@ +# Copied from the new-repo templates into every docs repo. Repos run different ruff +# settings, so their formatter leaves this file as it is: lint it, do not reformat it. +# fmt: off +"""Post-build check of a Zensical site: the rules `zensical build --strict` does not enforce. + +usage: python3 -I .github/scripts/site-check.py [zensical.toml] + +Run from the repository root (the workflow does): the config is read from ./zensical.toml +unless its path is given. No config file is an error, never a skipped pass. + +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, 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). 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 + extension under docs/hooks/, and Zensical publishes every file not in exclude_docs; + - 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 , 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 + plugin's whole output silently vanishes otherwise; + - 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; + - a `redirect_maps` entry whose stub was not written; + - 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) +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 +# dropped by `_convert_plugins` without a message. `material/` prefixes and `social/`, +# `privacy/`, `optimize/` instance names canonicalise the same way Zensical does. +KNOWN_PLUGINS = { + "api-autonav", "autorefs", "awesome-nav", "blog", "callouts", "exclude", "gh-admonitions", + "glightbox", "include-markdown", "literate-nav", "llmstxt", "macros", "markdown-exec", + "markdownextradata", "meta", "mike", "minify", "mkdocs-audio", "mkdocs-autoapi", + "mkdocs-nav-weight", "mkdocs-video", "mkdocstrings", "offline", "optimize", "privacy", + "redirects", "rss", "search", "social", "table-reader", "tags", +} +# Feature names the shipped UI tests for (zensical_ui 0.0.41, read from its templates and +# bundle.*.min.js). Regenerate after a bump with: +# grep -rhoE '"[a-z]+(\.[a-z]+)+" in features' /zensical_ui/templates | sort -u +# grep -oE '"(navigation|content|search|toc|header|announce)\.[a-z.]+"' .../bundle.*.min.js | sort -u +KNOWN_FEATURES = { + "announce.dismiss", "content.action.copy", "content.action.edit", "content.action.view", + "content.code.annotate", "content.code.copy", "content.code.select", + "content.footnote.tooltips", "content.lazy", "content.tabs.link", "content.tooltips", + "header.autohide", "navigation.expand", "navigation.footer", "navigation.indexes", + "navigation.instant", "navigation.instant.prefetch", "navigation.instant.preview", + "navigation.instant.progress", "navigation.path", "navigation.prune", "navigation.sections", + "navigation.tabs", "navigation.tabs.sticky", "navigation.top", "navigation.tracking", + "search.highlight", "toc.follow", "toc.integrate", +} + + + + +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