From 034293df14c558a0fff4467fb28099148f9834a5 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:46:24 +0800 Subject: [PATCH] fix(docs): resolve link roots before containment checks Normalize repository and generated-site roots at the owning boundary so symlink aliases do not reject valid local links. Preserve missing-fragment and escaping-path rejection. Refs #4264 --- scripts/test-website.py | 41 +++++++++++++++++++++++++++++++++++++++ scripts/verify-website.py | 2 ++ 2 files changed, 43 insertions(+) diff --git a/scripts/test-website.py b/scripts/test-website.py index 4d2ba29cb..b317ac345 100644 --- a/scripts/test-website.py +++ b/scripts/test-website.py @@ -322,6 +322,47 @@ class WebsiteMaintenanceChecks(unittest.TestCase): self.assertTrue(any("missing Markdown fragment" in error for error in errors)) self.assertTrue(any("missing repository target" in error for error in errors)) + def test_repository_links_accept_aliased_roots_and_reject_escaping_links(self): + with tempfile.TemporaryDirectory() as directory: + root = (Path(directory) / "repository").resolve() + (root / "website/public").mkdir(parents=True) + (root / "docs").mkdir() + (root / "docs/guide.md").write_text("# Guide\n") + page = root / "website/README.md" + page.write_text("[Guide](../docs/guide.md#guide)\n") + alias = Path(directory) / "repository-alias" + alias.symlink_to(root, target_is_directory=True) + self.assertEqual(VERIFY.check_repository_links(root), []) + self.assertEqual(VERIFY.check_repository_links(alias), []) + (root.parent / "outside.md").write_text("# Outside\n") + page.write_text("[Outside](../../outside.md)\n") + for boundary in (root, alias): + with self.subTest(boundary=boundary): + self.assertEqual(VERIFY.check_repository_links(boundary), [ + "website/README.md: missing repository target: ../../outside.md" + ]) + + def test_site_links_preserve_containment_through_aliased_roots(self): + with tempfile.TemporaryDirectory() as directory: + site = (Path(directory) / "site").resolve() + site.mkdir() + page = site / "index.html" + page.write_text('Fixture

Fixture

Guide') + (site / "guide.html").write_text('Guide

Answer

') + (site / "sitemap.xml").write_text('') + (site / "robots.txt").write_text("User-agent: *\nAllow: /\nSitemap: https://rea.tools/sitemap.xml\n") + alias = Path(directory) / "site-alias" + alias.symlink_to(site, target_is_directory=True) + # The tiny link fixture intentionally omits the social preview. + expected = ["Social preview PNG is missing; run scripts/prepare-website.py"] + self.assertEqual(VERIFY.check_site(site)[3], expected) + self.assertEqual(VERIFY.check_site(alias)[3], expected) + (site.parent / "outside.html").write_text("

Outside

") + page.write_text(page.read_text().replace("guide.html#answer", "../outside.html")) + for boundary in (site, alias): + with self.subTest(boundary=boundary): + self.assertIn("index.html: reference leaves the website: ../outside.html", VERIFY.check_site(boundary)[3]) + def test_preview_preserves_pages_prefix_for_assets_and_directory_redirects(self): with tempfile.TemporaryDirectory() as directory: site = Path(directory) diff --git a/scripts/verify-website.py b/scripts/verify-website.py index e60536e4c..10af305dc 100644 --- a/scripts/verify-website.py +++ b/scripts/verify-website.py @@ -44,6 +44,7 @@ def markdown_ids(path): def check_repository_links(root): """Check local links in website Markdown and current-repository source links.""" + root = root.resolve() errors = [] sources = {} for path in authored_markdown(root): @@ -318,6 +319,7 @@ def check_search_metadata(site, pages): def check_site(site): """Check assets, HTML fragment destinations and SVG syntax.""" + site = site.resolve() pages = {} errors = [] references = 0