| #!/usr/bin/env python3 | |
| # Configuration file for the Sphinx documentation builder. | |
| # | |
| # This file only contains a selection of the most common options. For a full | |
| # list see the documentation: | |
| # https://www.sphinx-doc.org/en/master/usage/configuration.html | |
| import re | |
| import shutil | |
| from pathlib import Path | |
| # -- Path setup -------------------------------------------------------------- | |
| # -- Project information ----------------------------------------------------- | |
| project = 'TinyUSB' | |
| copyright = '2025, Ha Thach' | |
| author = 'Ha Thach' | |
| # -- General configuration --------------------------------------------------- | |
| extensions = [ | |
| 'sphinx.ext.autodoc', | |
| 'sphinx.ext.intersphinx', | |
| 'sphinx.ext.todo', | |
| 'sphinx_autodoc_typehints', | |
| 'myst_parser', # Markdown (.md) support alongside reStructuredText | |
| ] | |
| templates_path = ['_templates'] | |
| # 'superpowers' holds internal plans/specs/handoffs (see CLAUDE.md), not published docs. | |
| # 'reference/hil_boards.md' is a generated partial that hardware-in-the-loop.md includes. | |
| exclude_patterns = ['_build', 'superpowers', 'reference/hil_boards.md'] | |
| # -- Options for HTML output ------------------------------------------------- | |
| html_theme = 'furo' | |
| html_title = 'TinyUSB' | |
| html_logo = 'assets/logo.svg' | |
| html_favicon = 'assets/logo.svg' | |
| html_theme_options = { | |
| 'sidebar_hide_name': True, | |
| } | |
| html_static_path = ['_static'] | |
| html_css_files = ['custom.css'] | |
| todo_include_todos = True | |
| # pre-process path in README.rst | |
| def preprocess_readme(): | |
| """Modify figure paths in README.rst for Sphinx builds.""" | |
| src = Path(__file__).parent.parent / "README.rst" | |
| tgt = Path(__file__).parent.parent / "README_processed.rst" | |
| if src.exists(): | |
| content = src.read_text(encoding='utf-8') | |
| # if the matching is inside a table, keep the table cell width by adding the same number of spaces in the end of the line | |
| # match pattern: | ... `... <docs/...>`_ ... | | |
| # change into: | ... `... <...>`_ ... | | |
| def _rewrite_table_line(line): | |
| if not (line.startswith('|') and line.rstrip().endswith('|')): | |
| return line | |
| rewritten = re.sub(r"<docs/([^>]+)>", r"<\1>", line) | |
| delta = len(line) - len(rewritten) - 1 # -1 for rst->html | |
| if delta > 0: | |
| last_pipe = rewritten.rfind('|') | |
| if last_pipe >= 0: | |
| rewritten = rewritten[:last_pipe] + (' ' * delta) + rewritten[last_pipe:] | |
| return rewritten | |
| content = ''.join(_rewrite_table_line(line) for line in content.splitlines(keepends=True)) | |
| content = re.sub(r"docs/", r"", content) | |
| content = re.sub(r"\.rst\b", r".html", content) | |
| if not content.endswith("\n"): | |
| content += "\n" | |
| tgt.write_text(content, encoding='utf-8') | |
| preprocess_readme() | |
| # scan example READMEs into docs/examples/ and generate a per-group index page | |
| EXAMPLE_GROUPS = ('device', 'host', 'dual') | |
| _HEADING_RE = re.compile(r'^(#{1,6})(\s.*)$') | |
| _FENCE_RE = re.compile(r'^\s*(```|~~~)') | |
| def _normalize_headings(text): | |
| """Make each page a single Sphinx section: promote so the first heading is | |
| H1 and demote any later same-or-higher heading to at least H2. Without this, | |
| a README that uses flat #### headings (no H1) becomes several top-level | |
| sections and each leaks into the sidebar as a separate entry.""" | |
| lines = text.splitlines(keepends=True) | |
| headings, in_fence = [], False | |
| for i, line in enumerate(lines): | |
| if _FENCE_RE.match(line): | |
| in_fence = not in_fence | |
| elif not in_fence and _HEADING_RE.match(line): | |
| headings.append(i) | |
| if not headings: | |
| return text | |
| delta = 1 - len(_HEADING_RE.match(lines[headings[0]]).group(1)) | |
| for n, i in enumerate(headings): | |
| m = _HEADING_RE.match(lines[i]) | |
| level = max(1, min(6, len(m.group(1)) + delta)) | |
| if n > 0: | |
| level = max(level, 2) | |
| lines[i] = '#' * level + m.group(2) + ('\n' if lines[i].endswith('\n') else '') | |
| return ''.join(lines) | |
| def _with_location(text, rel): | |
| """Insert a source-location note right after the first H1 so each rendered | |
| example page shows which example directory it came from.""" | |
| note = f"> **Example source:** `{rel}`\n" | |
| lines = text.splitlines(keepends=True) | |
| for i, line in enumerate(lines): | |
| if line.lstrip().startswith("# "): | |
| return "".join(lines[:i + 1]) + "\n" + note + "\n" + "".join(lines[i + 1:]) | |
| return note + "\n" + text # no H1: prepend | |
| def generate_examples_docs(): | |
| """Copy every examples/{device,host,dual}/*/README.md into | |
| docs/examples/<group>/<name>.md (noting its source location) and write a | |
| docs/examples/<group>/index.rst landing page per group. index.rst points at | |
| those group pages, giving a 3-level sidebar: Examples > Device/Host/Dual > | |
| example. Output is rebuilt each run (git-ignored).""" | |
| docs_dir = Path(__file__).parent | |
| examples_root = docs_dir.parent / "examples" | |
| out_dir = docs_dir / "examples" | |
| # start clean so deleted/renamed examples don't leave stale pages | |
| if out_dir.exists(): | |
| shutil.rmtree(out_dir) | |
| (docs_dir / "examples.rst").unlink(missing_ok=True) # remove legacy single-file output | |
| for group in EXAMPLE_GROUPS: | |
| group_out = out_dir / group | |
| group_out.mkdir(parents=True, exist_ok=True) | |
| names = [] | |
| for readme in sorted((examples_root / group).glob("*/README.md")): | |
| name = readme.parent.name | |
| rel = f"examples/{group}/{name}" | |
| content = _normalize_headings(readme.read_text(encoding='utf-8')) | |
| (group_out / f"{name}.md").write_text(_with_location(content, rel), encoding='utf-8') | |
| names.append(name) | |
| # group landing page (Device / Host / Dual) with a toctree of its examples | |
| heading = group.capitalize() | |
| page = [f"{'*' * len(heading)}\n{heading}\n{'*' * len(heading)}\n"] | |
| if names: | |
| page.append(".. toctree::\n :maxdepth: 1\n") | |
| page.extend(f" {name}" for name in names) | |
| else: | |
| page.append("No documented examples yet.") | |
| (group_out / "index.rst").write_text("\n".join(page) + "\n", encoding='utf-8') | |
| generate_examples_docs() |