mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-07-25 01:31:48 -05:00
Allow intentional cross-section anchors in lego_focal_verify, skip algorithm LaTeX false positives in audit_html, and add appendix_inference to HTML builds. Document the chapter-anchor exception in LEGO_CELLS.md; ledger now 45/45 pass.
111 lines
2.9 KiB
Python
111 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
audit_html.py
|
|
=============
|
|
|
|
Scan rendered Quarto HTML for spurious ``*.0`` in narrative prose (final check).
|
|
|
|
Strips code cells and Quarto chrome, then flags numbers like ``739,726.0`` or
|
|
``153.0 percent`` that should have been formatted as integers. Uses the same
|
|
false-positive filters as ``audit_prose.py`` (via ``spurious_zero``).
|
|
|
|
Usage::
|
|
|
|
python3 book/tools/audit/fmt/audit_html.py path/to/chapter.html
|
|
|
|
Exit code 0 prints ``CLEAN``; exit code 1 lists findings.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from book.cli.checks.currency_style import audit_rendered_file
|
|
from spurious_zero import find_spurious_zeros
|
|
|
|
try:
|
|
from bs4 import BeautifulSoup
|
|
except ImportError as exc: # pragma: no cover
|
|
raise SystemExit(
|
|
"audit_html.py requires beautifulsoup4 (pip install beautifulsoup4)"
|
|
) from exc
|
|
|
|
|
|
_ALGO_LATEX_MARKERS = (
|
|
"begin{algorithm}",
|
|
"end{algorithm}",
|
|
"begin{algorithmic}",
|
|
"end{algorithmic}",
|
|
"\\endfor",
|
|
"\\state",
|
|
"\\comment{",
|
|
"\\for{",
|
|
"\\ensure",
|
|
"\\return",
|
|
)
|
|
|
|
|
|
def _skip_rendered_violation(issue) -> bool:
|
|
"""Algorithm blocks often appear as raw LaTeX in HTML text extraction — not LEGO bugs."""
|
|
if issue.code != "rendered_raw_latex":
|
|
return False
|
|
ctx = issue.context.lower()
|
|
return any(marker in ctx for marker in _ALGO_LATEX_MARKERS)
|
|
|
|
|
|
def audit_html(file_path: Path) -> list[dict[str, str]]:
|
|
issues = [
|
|
{
|
|
"value": issue.code,
|
|
"context": issue.context,
|
|
}
|
|
for issue in audit_rendered_file(file_path)
|
|
if not _skip_rendered_violation(issue)
|
|
]
|
|
|
|
soup = BeautifulSoup(file_path.read_text(encoding="utf-8"), "html.parser")
|
|
content = soup.find("main") or soup.body
|
|
if not content:
|
|
return issues
|
|
|
|
for tag in content(["script", "style", "pre", "code"]):
|
|
tag.decompose()
|
|
for tag in content.find_all(class_=re.compile(r"cell-code|quarto-appendix-contents|quarto-figure")):
|
|
tag.decompose()
|
|
|
|
text = content.get_text(separator=" ")
|
|
issues.extend(
|
|
{"value": value, "context": context}
|
|
for value, context in find_spurious_zeros(text)
|
|
)
|
|
return issues
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("html", type=Path, help="Rendered chapter .html file")
|
|
args = parser.parse_args()
|
|
|
|
if not args.html.is_file():
|
|
print(f"File not found: {args.html}", file=sys.stderr)
|
|
return 1
|
|
|
|
issues = audit_html(args.html)
|
|
if not issues:
|
|
print("CLEAN")
|
|
return 0
|
|
|
|
for issue in issues:
|
|
print(f"FOUND: {issue['value']} | Context: ...{issue['context']}...")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|