from __future__ import annotations
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class Failure:
page: str
message: str
@dataclass
class Report:
failures: list[Failure] = field(default_factory=list)
def fail(self, page: str, message: str) -> None:
self.failures.append(Failure(page, message))
HOME_REQUIRED_MARKERS = (
'class="rm-home"',
'class="rm-home-nav"',
'>Documentation<',
'id="rm-hero-title"',
'class="rm-terminal-shot"',
'class="rm-terminal-shot__window"',
'src="images/homepage-terminal.png"',
'class="rm-install rm-install--primary"',
'class="rm-hero__alternatives"',
'class="rm-next__primary"',
'class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" hidden',
">Quickstart</a>",
"rm-performance",
'href="https://rumdl.dev/getting-started/installation/"',
'href="https://rumdl.dev/playground/"',
'href="https://rumdl.dev/markdownlint-comparison/"',
'class="rm-benchmark__featured"',
'data-rm-event="cta_select"',
'data-rm-command="uvx_check"',
)
HOME_FORBIDDEN_MARKERS = (
'class="rm-proof"',
'class="rm-terminal"',
"0.15s",
"0.02s",
"5.2s",
">Try on your repository<",
)
REQUIRED_ASSETS = (
"stylesheets/rumdl.css",
"javascripts/rumdl.js",
"javascripts/playground-editor.js",
"javascripts/playground-editor.licenses.txt",
"javascripts/rumdl_lib_bg.wasm",
"images/homepage-terminal.png",
"images/social-preview.jpg",
)
REQUIRED_ROUTES = ("playground/index.html",)
PLAYGROUND_REQUIRED_MARKERS = (
'for="pg-example"',
'id="pg-announcer"',
'aria-live="polite"',
'class="pg-workbench"',
'aria-labelledby="pg-input-heading"',
'id="pg-editor"',
'id="pg-problems-resize"',
'id="pg-focus-btn"',
'id="pg-active-config"',
'id="pg-file-input"',
'class="pg-drop-overlay"',
'data-issue-filter="manual"',
"Auto-fixable",
'id="pg-undo-btn"',
'id="pg-config-form"',
'id="pg-reflow"',
'id="pg-share-btn"',
'data-warning-fix=',
"show in editor",
"loadRumdl",
"destroyPlayground",
"DRAFT_KEY",
"MAX_FILE_SIZE",
"SHARE_PREFIX",
"MD013: { reflow: config.reflow }",
)
PLAYGROUND_FORBIDDEN_MARKERS = (
'class="pg-fix-badge"',
"cdn.jsdelivr.net/npm/rumdl-wasm",
)
CODE_FENCE_MARKERS = ("rm-hero__aside", "rm-terminal-shot", "rm-section")
SOCIAL_PREVIEW_MARKERS = (
'<meta property="og:image" content="https://rumdl.dev/images/social-preview.jpg">',
'<meta property="og:image:type" content="image/jpeg">',
'<meta property="og:image:width" content="1200">',
'<meta property="og:image:height" content="630">',
(
'<meta property="og:image:alt" content="rumdl documentation, with document '
'lines accelerating through a coral heading marker">'
),
'<meta name="twitter:card" content="summary_large_image">',
'<meta name="twitter:image" content="https://rumdl.dev/images/social-preview.jpg">',
(
'<meta name="twitter:image:alt" content="rumdl documentation, with document '
'lines accelerating through a coral heading marker">'
),
)
LANGUAGE_TEXT_CODE = re.compile(
r'<code[^>]*class="[^"]*\blanguage-text\b[^"]*"[^>]*>([^<]*)</code>',
re.IGNORECASE,
)
def read(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="replace")
def check_page(path: Path, report: Report) -> None:
rel = path.relative_to(path.parent).as_posix()
html = read(path)
for marker in HOME_REQUIRED_MARKERS:
if marker not in html:
report.fail(rel, f"missing required homepage marker {marker!r}")
for marker in HOME_FORBIDDEN_MARKERS:
if marker in html:
report.fail(rel, f"contains superseded benchmark value {marker!r}")
for marker in SOCIAL_PREVIEW_MARKERS:
if marker not in html:
report.fail(rel, f"missing social-preview metadata {marker!r}")
for marker in CODE_FENCE_MARKERS:
for code_match in LANGUAGE_TEXT_CODE.finditer(html):
if marker in code_match.group(1):
report.fail(
rel,
f"homepage component {marker!r} rendered inside a text code fence",
)
break
def check_playground(path: Path, report: Report) -> None:
html = read(path)
for marker in PLAYGROUND_REQUIRED_MARKERS:
if marker not in html:
report.fail("playground/index.html", f"missing playground marker {marker!r}")
for marker in PLAYGROUND_FORBIDDEN_MARKERS:
if marker in html:
report.fail("playground/index.html", f"found forbidden playground marker {marker!r}")
def main(argv: list[str]) -> int:
if len(argv) != 2:
print("usage: docs_smoke_test.py <site_dir>", file=sys.stderr)
return 2
site = Path(argv[1])
if not site.is_dir():
print(f"error: {site!s} is not a directory", file=sys.stderr)
return 2
report = Report()
homepage = site / "index.html"
if not homepage.is_file():
report.fail("index.html", f"page not found at {homepage!s}")
else:
check_page(homepage, report)
playground = site / "playground" / "index.html"
if playground.is_file():
check_playground(playground, report)
for rel in (*REQUIRED_ASSETS, *REQUIRED_ROUTES):
path = site / rel
if not path.is_file():
report.fail(rel, f"required launch surface not found at {path!s}")
if report.failures:
print("docs smoke test FAILED:", file=sys.stderr)
for failure in report.failures:
print(f" [{failure.page}] {failure.message}", file=sys.stderr)
return 1
print("docs smoke test passed")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))