1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
#!/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']
exclude_patterns = ['_build']
# -- 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()
|