#!/usr/bin/env python3 """Build the static OpenCAD Studio HTML help system from help/manual.md.""" from __future__ import annotations import argparse import html import json import re import subprocess import sys from dataclasses import dataclass from html.parser import HTMLParser from pathlib import Path HELP = Path(__file__).resolve().parents[1] SOURCE = HELP / "manual.md" @dataclass(frozen=True) class Page: filename: str title: str summary: str PAGES = ( Page("getting-started.html", "Getting Started", "Begin a drawing safely and learn how to use this manual."), Page("files-drawings.html", "Files and Drawings", "Open, save, recover, and move drawings with their dependencies."), Page("workspace-navigation.html", "Workspace and Navigation", "Work with spaces, views, selection, and object snaps."), Page("create-edit-geometry.html", "Create and Edit Geometry", "Draw, modify, grip-edit, erase, and restore geometry."), Page("layers-properties.html", "Layers and Properties", "Organize objects and control their inherited appearance."), Page("blocks-references.html", "Blocks and References", "Create reusable content, arrays, attributes, and file references."), Page("annotation-dimensions.html", "Annotation and Dimensions", "Add readable text, dimensions, leaders, and scaled annotation."), Page("layouts-plotting.html", "Layouts and Plotting", "Prepare paper-space sheets and verify plotted output."), Page("3d-modeling.html", "3D Modeling and Visualization", "Create and inspect solids, surfaces, materials, and visual styles."), Page("parametric-constraints.html", "Parametric Constraints", "Capture geometric intent and diagnose solve or history issues."), Page("commands-shortcuts.html", "Commands and Shortcuts", "Use prompts, autocomplete, aliases, and keyboard conventions."), Page("automation-plugins.html", "Automation and Plugins", "Control drawings programmatically and extend the native app."), Page("troubleshooting-recovery.html", "Troubleshooting and Recovery", "Diagnose file, geometry, rendering, and history problems."), ) def slug(text: str) -> str: value = re.sub(r"[^\w\s-]", "", text.lower(), flags=re.UNICODE) value = re.sub(r"[\s_]+", "-", value) return value.strip("-") def split_chapters(markdown: str) -> dict[str, str]: chapters: dict[str, list[str]] = {} current: list[str] | None = None for line in markdown.splitlines(): match = re.match(r"^## (.+)$", line) if match: title = match.group(1).strip() if title in chapters: raise RuntimeError(f"Duplicate chapter: {title}") current = [line] chapters[title] = current elif current is not None: current.append(line) return {title: "\n".join(lines) for title, lines in chapters.items()} def pandoc(markdown: str) -> str: process = subprocess.run( ["pandoc", "--from=gfm", "--to=html5", "--wrap=none"], input=markdown, text=True, capture_output=True, check=True, ) return process.stdout.strip() def headings(markdown: str) -> list[tuple[str, str]]: output: list[tuple[str, str]] = [] for line in markdown.splitlines(): match = re.match(r"^### (.+)$", line) if match: title = match.group(1).strip() output.append((slug(title), title)) return output def sidebar(current: Page | None, chapters: dict[str, str]) -> str: parts = ['") return "\n".join(parts) def overview() -> str: cards = "".join( f'{html.escape(page.title)}' f'{html.escape(page.summary)}' for page in PAGES ) return f"""

OpenCAD Studio User Manual

Find the workflow, command, or concept you need

Learn how to create and edit drawings, exchange DWG and DXF files, prepare layouts, model in 3D, use constraints, automate work, and recover from common problems.

Create a first drawing Recover a drawing Report a problem

Manual chapters

{cards}
""".strip() def shell(page: Page | None, title: str, body: str, chapters: dict[str, str]) -> str: previous_link = "" next_link = "" if page is not None: index = PAGES.index(page) previous = PAGES[index - 1] if index else None following = PAGES[index + 1] if index + 1 < len(PAGES) else None previous_link = ( f'' if previous else '' ) next_link = ( f'' if following else "" ) elif PAGES: next_link = f'' return f""" {html.escape(title)} — OpenCAD Studio Help
OpenCAD Studio Help
{sidebar(page, chapters)}

OpenCAD Studio User Manual

{html.escape(title)}

{body}
""" class TextExtractor(HTMLParser): def __init__(self) -> None: super().__init__() self.parts: list[str] = [] def handle_data(self, data: str) -> None: if data.strip(): self.parts.append(data.strip()) def search_entries(chapters: dict[str, str]) -> list[dict[str, str]]: entries: list[dict[str, str]] = [] for page in PAGES: chapter = chapters[page.title] sections = re.split(r"(?=^### )", chapter, flags=re.MULTILINE) for section in sections: match = re.match(r"^### (.+)$", section.splitlines()[0] if section.splitlines() else "") if not match: continue title = match.group(1).strip() extractor = TextExtractor() extractor.feed(pandoc(section)) entries.append({ "title": title, "chapter": page.title, "url": f"{page.filename}#{slug(title)}", "text": " ".join(extractor.parts), }) return entries def generated_outputs() -> dict[str, str]: markdown = SOURCE.read_text(encoding="utf-8") chapters = split_chapters(markdown) expected = {page.title for page in PAGES} if set(chapters) != expected: raise RuntimeError( f"Manual chapter mapping mismatch. Missing: {sorted(expected - set(chapters))}; " f"extra: {sorted(set(chapters) - expected)}" ) output = {"index.html": shell(None, "Help overview", overview(), chapters)} for page in PAGES: chapter_body = "\n".join(chapters[page.title].splitlines()[1:]).lstrip() output[page.filename] = shell(page, page.title, pandoc(chapter_body), chapters) index_json = json.dumps(search_entries(chapters), ensure_ascii=False, separators=(",", ":")) output["search-index.js"] = f"window.OCS_HELP_INDEX = {index_json};\n" return output def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="Fail if generated output is stale.") args = parser.parse_args() output = generated_outputs() if args.check: stale = [name for name, content in output.items() if not (HELP / name).is_file() or (HELP / name).read_text(encoding="utf-8") != content] if stale: print("Stale generated help: " + ", ".join(stale), file=sys.stderr) return 1 print(f"Generated help is current ({len(output) - 1} pages plus search index).") return 0 for name, content in output.items(): (HELP / name).write_text(content, encoding="utf-8") print(f"Generated {len(output) - 1} pages and search-index.js in {HELP}") return 0 if __name__ == "__main__": raise SystemExit(main())