cpdocs 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cpdocs/__init__.py +5 -0
- cpdocs/__main__.py +5 -0
- cpdocs/build.py +645 -0
- cpdocs/clang_backend.py +1254 -0
- cpdocs/cli.py +78 -0
- cpdocs/config.py +160 -0
- cpdocs/contract.py +407 -0
- cpdocs/html_renderer.py +1803 -0
- cpdocs/model.py +228 -0
- cpdocs/properdocs_core.py +158 -0
- cpdocs/properdocs_plugin.py +236 -0
- cpdocs/python_backend.py +469 -0
- cpdocs/python_manifest.py +84 -0
- cpdocs/render.py +838 -0
- cpdocs/rust_backend.py +234 -0
- cpdocs/share/cmake/cpdocs.cmake +464 -0
- cpdocs/versioning.py +201 -0
- cpdocs-0.1.0.dist-info/METADATA +48 -0
- cpdocs-0.1.0.dist-info/RECORD +21 -0
- cpdocs-0.1.0.dist-info/WHEEL +4 -0
- cpdocs-0.1.0.dist-info/entry_points.txt +5 -0
cpdocs/html_renderer.py
ADDED
|
@@ -0,0 +1,1803 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 cpdocs developers
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Standalone HTML renderer for cpdocs API references.
|
|
5
|
+
|
|
6
|
+
The project documentation is rendered by ProperDocs, but API references have different navigation
|
|
7
|
+
and versioning semantics. This renderer therefore consumes the language-neutral :class:`ApiGraph`
|
|
8
|
+
directly and emits a self-contained code browser without routing the reference through Markdown,
|
|
9
|
+
MkDocs, Sphinx, Doxygen, Breathe, or Exhale.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import dataclasses
|
|
15
|
+
import functools
|
|
16
|
+
import hashlib
|
|
17
|
+
import html
|
|
18
|
+
import json
|
|
19
|
+
import posixpath
|
|
20
|
+
import re
|
|
21
|
+
import shutil
|
|
22
|
+
from pathlib import Path, PurePosixPath
|
|
23
|
+
|
|
24
|
+
from markdown_it import MarkdownIt
|
|
25
|
+
|
|
26
|
+
from .model import ApiEntity, ApiGraph, ApiSignature, entity_sort_key
|
|
27
|
+
from .render import (
|
|
28
|
+
_CLASS_LIKE_KINDS,
|
|
29
|
+
_documentation_blocks,
|
|
30
|
+
_embedded_member_parent,
|
|
31
|
+
_entity_declaration,
|
|
32
|
+
_highlight_declaration,
|
|
33
|
+
_highlight_source_lines,
|
|
34
|
+
_kind_marker,
|
|
35
|
+
_member_anchor,
|
|
36
|
+
_member_summary_spelling,
|
|
37
|
+
_relative_link,
|
|
38
|
+
_signature_spelling,
|
|
39
|
+
_source_document,
|
|
40
|
+
_source_language,
|
|
41
|
+
_variant_anchor,
|
|
42
|
+
_variant_display_name,
|
|
43
|
+
assign_documents,
|
|
44
|
+
entity_document,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_CONTAINER_KINDS = {"namespace", "module", "package"}
|
|
48
|
+
_FUNCTION_KINDS = {"function", "method", "constructor", "deduction_guide"}
|
|
49
|
+
_TYPE_KINDS = {"class", "struct", "union", "enum", "trait", "protocol", "type_alias", "concept"}
|
|
50
|
+
_MEMBER_VALUE_KINDS = {"property", "attribute", "variable", "constant"}
|
|
51
|
+
|
|
52
|
+
_INLINE_DOCUMENTATION = re.compile(
|
|
53
|
+
r"`(?P<code>[^`]+)`"
|
|
54
|
+
r"|\*\*(?P<strong>[^*\n]+?)\*\*"
|
|
55
|
+
r"|\[(?P<label>[^\]]+)\]\((?P<href>[^)]+)\)"
|
|
56
|
+
r"|@projectdocs(?:\{(?P<project_path>[^}]+)\})?"
|
|
57
|
+
)
|
|
58
|
+
_PROJECTDOCS_HREF = re.compile(r'href=(?P<quote>["\'])projectdocs:(?P<path>[^"\']*)(?P=quote)')
|
|
59
|
+
|
|
60
|
+
_MARKDOWN = MarkdownIt("commonmark", {"html": False, "linkify": False, "typographer": False})
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Paths and links
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _output_directory(document: Path) -> Path:
|
|
69
|
+
if document.name == "index.md":
|
|
70
|
+
return document.parent
|
|
71
|
+
return document.with_suffix("")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _output_file(document: Path) -> Path:
|
|
75
|
+
return _output_directory(document) / "index.html"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _root_relative(document: Path, target: str) -> str:
|
|
79
|
+
directory = _output_directory(document)
|
|
80
|
+
value = posixpath.relpath(target, directory.as_posix() or ".")
|
|
81
|
+
if target.endswith("/") and not value.endswith("/"):
|
|
82
|
+
value += "/"
|
|
83
|
+
return value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _page_href(source: Path, target: Path, anchor: str = "") -> str:
|
|
87
|
+
value = _relative_link(source, target)
|
|
88
|
+
return value + (f"#{anchor}" if anchor else "")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _project_docs_target(document: Path, project_docs_url: str, target: str = "") -> str:
|
|
92
|
+
"""Resolve one semantic ``projectdocs:`` target for the current API page.
|
|
93
|
+
|
|
94
|
+
``target`` is always interpreted relative to the root of the non-versioned project site. This
|
|
95
|
+
is deliberately different from a normal relative hyperlink: every historical API version must
|
|
96
|
+
continue to point at the single current project website.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
base = _project_docs_href(document, project_docs_url)
|
|
100
|
+
target = target.strip()
|
|
101
|
+
if not target:
|
|
102
|
+
return base
|
|
103
|
+
if target.startswith("#"):
|
|
104
|
+
return base + target
|
|
105
|
+
|
|
106
|
+
path, separator, fragment = target.partition("#")
|
|
107
|
+
value = base.rstrip("/") + "/" + path.lstrip("/")
|
|
108
|
+
if path.endswith("/") and not value.endswith("/"):
|
|
109
|
+
value += "/"
|
|
110
|
+
if separator:
|
|
111
|
+
value += "#" + fragment
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _resolve_projectdocs_links(fragment: str, document: Path, project_docs_url: str) -> str:
|
|
116
|
+
"""Resolve semantic project-site links after a page has been placed in the API tree."""
|
|
117
|
+
|
|
118
|
+
def replace(match: re.Match[str]) -> str:
|
|
119
|
+
target = _project_docs_target(
|
|
120
|
+
document, project_docs_url, html.unescape(match.group("path"))
|
|
121
|
+
)
|
|
122
|
+
quote = match.group("quote")
|
|
123
|
+
return f"href={quote}{html.escape(target, quote=True)}{quote}"
|
|
124
|
+
|
|
125
|
+
return _PROJECTDOCS_HREF.sub(replace, fragment)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _entity_target(entity: ApiEntity, graph: ApiGraph) -> tuple[Path, str]:
|
|
129
|
+
parent = _embedded_member_parent(entity, graph)
|
|
130
|
+
if parent is not None:
|
|
131
|
+
return entity_document(parent), _member_anchor(entity)
|
|
132
|
+
return entity_document(entity), ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _entity_href(source: Path, entity: ApiEntity, graph: ApiGraph) -> str:
|
|
136
|
+
target, anchor = _entity_target(entity, graph)
|
|
137
|
+
return _page_href(source, target, anchor)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _display_source_path(source_path: str, graph: ApiGraph) -> str:
|
|
141
|
+
"""Return the source path shown to readers while preserving the real repository path for links."""
|
|
142
|
+
source_root = str(graph.metadata.get("source_root") or "").strip("/")
|
|
143
|
+
logical = PurePosixPath(source_path)
|
|
144
|
+
if not source_root:
|
|
145
|
+
return logical.as_posix()
|
|
146
|
+
root = PurePosixPath(source_root)
|
|
147
|
+
try:
|
|
148
|
+
return logical.relative_to(root).as_posix()
|
|
149
|
+
except ValueError:
|
|
150
|
+
return logical.as_posix()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _source_href(source_document: Path, entity: ApiEntity, graph: ApiGraph) -> str | None:
|
|
154
|
+
if entity.source is None or entity.source.path not in graph.sources:
|
|
155
|
+
return None
|
|
156
|
+
value = _page_href(source_document, _source_document(entity.source.path))
|
|
157
|
+
if entity.source.line:
|
|
158
|
+
value += f"#L{entity.source.line}"
|
|
159
|
+
return value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Shared fragments
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _kind_label(kind: str) -> str:
|
|
168
|
+
return {
|
|
169
|
+
"namespace": "namespace",
|
|
170
|
+
"module": "module",
|
|
171
|
+
"package": "package",
|
|
172
|
+
"class": "class",
|
|
173
|
+
"struct": "struct",
|
|
174
|
+
"union": "union",
|
|
175
|
+
"enum": "enum",
|
|
176
|
+
"trait": "trait",
|
|
177
|
+
"protocol": "protocol",
|
|
178
|
+
"type_alias": "type alias",
|
|
179
|
+
"concept": "concept",
|
|
180
|
+
"function": "function",
|
|
181
|
+
"method": "method",
|
|
182
|
+
"constructor": "constructor",
|
|
183
|
+
"property": "property",
|
|
184
|
+
"attribute": "attribute",
|
|
185
|
+
"variable": "variable",
|
|
186
|
+
"constant": "constant",
|
|
187
|
+
"macro": "macro",
|
|
188
|
+
"deduction_guide": "deduction guide",
|
|
189
|
+
}.get(kind, kind.replace("_", " "))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _kind_badge(kind: str) -> str:
|
|
193
|
+
marker = html.escape(_kind_marker(kind))
|
|
194
|
+
return f'<span class="api-kind" title="{html.escape(_kind_label(kind))}">{marker}</span>'
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _legend(graph: ApiGraph) -> str:
|
|
198
|
+
present = {entity.kind for entity in graph.entities.values()}
|
|
199
|
+
order = [
|
|
200
|
+
"namespace",
|
|
201
|
+
"module",
|
|
202
|
+
"package",
|
|
203
|
+
"class",
|
|
204
|
+
"struct",
|
|
205
|
+
"union",
|
|
206
|
+
"enum",
|
|
207
|
+
"trait",
|
|
208
|
+
"protocol",
|
|
209
|
+
"concept",
|
|
210
|
+
"function",
|
|
211
|
+
"macro",
|
|
212
|
+
]
|
|
213
|
+
entries = []
|
|
214
|
+
for kind in order:
|
|
215
|
+
if kind not in present:
|
|
216
|
+
continue
|
|
217
|
+
entries.append(
|
|
218
|
+
f'<span class="api-legend-entry">{_kind_badge(kind)} {_kind_label(kind)}</span>'
|
|
219
|
+
)
|
|
220
|
+
return (
|
|
221
|
+
'<div class="api-legend">'
|
|
222
|
+
+ '<span class="api-legend-separator">·</span>'.join(entries)
|
|
223
|
+
+ "</div>"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _visible_outline_children(entity: ApiEntity, graph: ApiGraph) -> list[ApiEntity]:
|
|
228
|
+
# Public members of records live on the record page and should not duplicate themselves in the
|
|
229
|
+
# global API outline. Namespaces/modules/packages retain their recursive API tree.
|
|
230
|
+
if entity.kind in _CLASS_LIKE_KINDS or entity.kind == "enum":
|
|
231
|
+
return []
|
|
232
|
+
values = [
|
|
233
|
+
graph.entities[child_id]
|
|
234
|
+
for child_id in entity.children
|
|
235
|
+
if child_id in graph.entities and graph.entities[child_id].kind != "macro"
|
|
236
|
+
]
|
|
237
|
+
return sorted(values, key=entity_sort_key)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _is_ancestor(candidate: ApiEntity, current: ApiEntity | None, graph: ApiGraph) -> bool:
|
|
241
|
+
entity = current
|
|
242
|
+
while entity is not None:
|
|
243
|
+
if entity.id == candidate.id:
|
|
244
|
+
return True
|
|
245
|
+
if not entity.parent or entity.parent not in graph.entities:
|
|
246
|
+
break
|
|
247
|
+
entity = graph.entities[entity.parent]
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _outline_entity(
|
|
252
|
+
entity: ApiEntity, graph: ApiGraph, document: Path, current: ApiEntity | None
|
|
253
|
+
) -> str:
|
|
254
|
+
children = _visible_outline_children(entity, graph)
|
|
255
|
+
expanded = bool(children and _is_ancestor(entity, current, graph))
|
|
256
|
+
active = current is not None and entity.id == current.id
|
|
257
|
+
href = _entity_href(document, entity, graph)
|
|
258
|
+
row = [
|
|
259
|
+
'<div class="api-outline-row' + (" is-current" if active else "") + '">',
|
|
260
|
+
_kind_badge(entity.kind),
|
|
261
|
+
f'<a class="api-outline-link" href="{html.escape(href)}">{html.escape(entity.member_label)}</a>',
|
|
262
|
+
_variant_tag(entity, graph),
|
|
263
|
+
]
|
|
264
|
+
if children:
|
|
265
|
+
state = "true" if expanded else "false"
|
|
266
|
+
row.append(
|
|
267
|
+
f'<button class="api-outline-toggle" type="button" aria-expanded="{state}" '
|
|
268
|
+
f'aria-label="{"Collapse" if expanded else "Expand"} {html.escape(entity.qualified_name)}"></button>'
|
|
269
|
+
)
|
|
270
|
+
row.append("</div>")
|
|
271
|
+
child_html = ""
|
|
272
|
+
if children:
|
|
273
|
+
hidden = "" if expanded else " hidden"
|
|
274
|
+
child_html = (
|
|
275
|
+
'<ul class="api-outline-children"'
|
|
276
|
+
+ hidden
|
|
277
|
+
+ ">"
|
|
278
|
+
+ "".join(
|
|
279
|
+
"<li>" + _outline_entity(child, graph, document, current) + "</li>"
|
|
280
|
+
for child in children
|
|
281
|
+
)
|
|
282
|
+
+ "</ul>"
|
|
283
|
+
)
|
|
284
|
+
return "".join(row) + child_html
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _outline(
|
|
288
|
+
graph: ApiGraph, document: Path, current: ApiEntity | None, *, current_special: str = ""
|
|
289
|
+
) -> str:
|
|
290
|
+
roots = [entity for entity in graph.roots() if entity.kind != "macro"]
|
|
291
|
+
macros = sorted(
|
|
292
|
+
(entity for entity in graph.entities.values() if entity.kind == "macro"),
|
|
293
|
+
key=entity_sort_key,
|
|
294
|
+
)
|
|
295
|
+
current_label = (
|
|
296
|
+
current.name
|
|
297
|
+
if current is not None
|
|
298
|
+
else {
|
|
299
|
+
"home": "Home",
|
|
300
|
+
"variants": "API Variants",
|
|
301
|
+
"macros": "Macros",
|
|
302
|
+
"source": "Source",
|
|
303
|
+
}.get(current_special, "")
|
|
304
|
+
)
|
|
305
|
+
home_current = current is None and current_special == "home"
|
|
306
|
+
variants_current = current_special == "variants"
|
|
307
|
+
parts = [
|
|
308
|
+
'<nav class="api-outline" aria-label="API outline">',
|
|
309
|
+
"<h2>Outline</h2>",
|
|
310
|
+
# On small screens the outline collapses behind this button, which names the current page.
|
|
311
|
+
'<button class="api-outline-panel-toggle" type="button" aria-expanded="false"'
|
|
312
|
+
' aria-controls="api-outline-tree"><span>Outline</span>'
|
|
313
|
+
f'<span class="api-outline-current">{html.escape(current_label)}</span></button>',
|
|
314
|
+
'<ul class="api-outline-root" id="api-outline-tree">',
|
|
315
|
+
'<li><div class="api-outline-row' + (" is-current" if home_current else "") + '">',
|
|
316
|
+
'<span class="api-kind api-kind-home">H</span>',
|
|
317
|
+
f'<a class="api-outline-link" href="{html.escape(_page_href(document, Path("index.md")))}">Home</a>',
|
|
318
|
+
"</div></li>",
|
|
319
|
+
]
|
|
320
|
+
if graph.variants:
|
|
321
|
+
parts.extend(
|
|
322
|
+
[
|
|
323
|
+
'<li><div class="api-outline-row'
|
|
324
|
+
+ (" is-current" if variants_current else "")
|
|
325
|
+
+ '">',
|
|
326
|
+
'<span class="api-kind api-kind-empty"></span>',
|
|
327
|
+
f'<a class="api-outline-link" href="{html.escape(_page_href(document, Path("api-variants.md")))}">API Variants</a>',
|
|
328
|
+
"</div></li>",
|
|
329
|
+
]
|
|
330
|
+
)
|
|
331
|
+
for entity in roots:
|
|
332
|
+
parts.append("<li>" + _outline_entity(entity, graph, document, current) + "</li>")
|
|
333
|
+
if macros:
|
|
334
|
+
macro_open = current is not None and current.kind == "macro"
|
|
335
|
+
parts.extend(
|
|
336
|
+
[
|
|
337
|
+
"<li>",
|
|
338
|
+
'<div class="api-outline-row">',
|
|
339
|
+
'<span class="api-kind api-kind-empty"></span>',
|
|
340
|
+
'<a class="api-outline-link" href="'
|
|
341
|
+
+ html.escape(_page_href(document, Path("macros.md")))
|
|
342
|
+
+ '">Macros</a>',
|
|
343
|
+
f'<button class="api-outline-toggle" type="button" aria-expanded="{"true" if macro_open else "false"}" aria-label="{"Collapse" if macro_open else "Expand"} Macros"></button>',
|
|
344
|
+
"</div>",
|
|
345
|
+
'<ul class="api-outline-children"' + ("" if macro_open else " hidden") + ">",
|
|
346
|
+
]
|
|
347
|
+
)
|
|
348
|
+
for macro in macros:
|
|
349
|
+
active = current is not None and macro.id == current.id
|
|
350
|
+
parts.append(
|
|
351
|
+
'<li><div class="api-outline-row'
|
|
352
|
+
+ (" is-current" if active else "")
|
|
353
|
+
+ '">'
|
|
354
|
+
+ _kind_badge("macro")
|
|
355
|
+
+ f'<a class="api-outline-link" href="{html.escape(_entity_href(document, macro, graph))}">{html.escape(macro.name)}</a>'
|
|
356
|
+
+ "</div></li>"
|
|
357
|
+
)
|
|
358
|
+
parts.extend(["</ul>", "</li>"])
|
|
359
|
+
parts.extend(["</ul>", "</nav>"])
|
|
360
|
+
return "".join(parts)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _toc(items: list[tuple[str, str, int]]) -> str:
|
|
364
|
+
if not items:
|
|
365
|
+
return '<aside class="api-toc"><h2>On this page</h2></aside>'
|
|
366
|
+
parts = ['<aside class="api-toc"><h2>On this page</h2><ul>']
|
|
367
|
+
for label, anchor, level in items:
|
|
368
|
+
parts.append(
|
|
369
|
+
f'<li class="toc-level-{level}"><a href="#{html.escape(anchor)}">{html.escape(label)}</a></li>'
|
|
370
|
+
)
|
|
371
|
+
parts.append("</ul></aside>")
|
|
372
|
+
return "".join(parts)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _inline_documentation(text: str) -> str:
|
|
376
|
+
"""Render the small Markdown-like inline subset accepted in extracted API prose.
|
|
377
|
+
|
|
378
|
+
API comments are intentionally not routed through a full Markdown engine. We only need stable,
|
|
379
|
+
predictable inline constructs here: code spans, Markdown links, and cpdocs's ``@projectdocs``
|
|
380
|
+
shorthand. A Markdown link may use ``projectdocs:`` as its destination and is resolved later,
|
|
381
|
+
once the renderer knows the final depth of the generated API page.
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
result: list[str] = []
|
|
385
|
+
position = 0
|
|
386
|
+
for match in _INLINE_DOCUMENTATION.finditer(text):
|
|
387
|
+
result.append(html.escape(text[position : match.start()]))
|
|
388
|
+
if match.group("code") is not None:
|
|
389
|
+
result.append(f"<code>{html.escape(match.group('code'))}</code>")
|
|
390
|
+
elif match.group("strong") is not None:
|
|
391
|
+
result.append(f"<strong>{html.escape(match.group('strong'))}</strong>")
|
|
392
|
+
elif match.group("label") is not None:
|
|
393
|
+
label = html.escape(match.group("label"))
|
|
394
|
+
href = match.group("href").strip()
|
|
395
|
+
# Reject executable URL schemes in comments. Ordinary relative links, anchors,
|
|
396
|
+
# HTTP(S), mailto and projectdocs semantic links remain available.
|
|
397
|
+
if re.match(r"(?i)^(?:javascript|data|vbscript):", href):
|
|
398
|
+
result.append(label)
|
|
399
|
+
else:
|
|
400
|
+
result.append(f'<a href="{html.escape(href, quote=True)}">{label}</a>')
|
|
401
|
+
else:
|
|
402
|
+
target = (match.group("project_path") or "").strip()
|
|
403
|
+
label = target.rstrip("/") or "project documentation"
|
|
404
|
+
result.append(
|
|
405
|
+
f'<a href="projectdocs:{html.escape(target, quote=True)}">{html.escape(label)}</a>'
|
|
406
|
+
)
|
|
407
|
+
position = match.end()
|
|
408
|
+
result.append(html.escape(text[position:]))
|
|
409
|
+
return "".join(result)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
_DOCUMENTATION_FIELD = re.compile(
|
|
413
|
+
r"@(?P<command>tparam|param|returns?|throws?)\b\s*(?P<rest>.*)", re.DOTALL
|
|
414
|
+
)
|
|
415
|
+
_FIELD_TITLES = {
|
|
416
|
+
"tparam": "Template parameters",
|
|
417
|
+
"param": "Parameters",
|
|
418
|
+
"return": "Returns",
|
|
419
|
+
"throws": "Throws",
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _documentation_description(text: str) -> str:
|
|
424
|
+
"""Render documentation prose, followed by its parameter, return and exception fields.
|
|
425
|
+
|
|
426
|
+
Paragraphs of the form ``@tparam NAME text``, ``@param NAME text``, ``@return text`` and
|
|
427
|
+
``@throws text`` are collected, wherever they appear, into titled lists after the prose.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
paragraphs: list[str] = []
|
|
431
|
+
fields: dict[str, list[tuple[str, str]]] = {}
|
|
432
|
+
for block in re.split(r"\n\s*\n", text.strip()):
|
|
433
|
+
paragraph = block.strip()
|
|
434
|
+
if not paragraph or paragraph.startswith("CPDOCS-API-RELATES-TO:"):
|
|
435
|
+
continue
|
|
436
|
+
# Doc-comment line wrapping is not semantically significant. Keep explicit Markdown-like
|
|
437
|
+
# inline syntax intact while presenting the prose as a normal paragraph.
|
|
438
|
+
normalized = " ".join(line.strip() for line in paragraph.splitlines())
|
|
439
|
+
field = _DOCUMENTATION_FIELD.fullmatch(normalized)
|
|
440
|
+
if field is None:
|
|
441
|
+
paragraphs.append(f"<p>{_inline_documentation(normalized)}</p>")
|
|
442
|
+
continue
|
|
443
|
+
command = {"returns": "return", "throw": "throws"}.get(
|
|
444
|
+
field.group("command"), field.group("command")
|
|
445
|
+
)
|
|
446
|
+
rest = field.group("rest").strip()
|
|
447
|
+
if command in {"tparam", "param"}:
|
|
448
|
+
name, _, description = rest.partition(" ")
|
|
449
|
+
fields.setdefault(command, []).append((name, description.strip()))
|
|
450
|
+
else:
|
|
451
|
+
fields.setdefault(command, []).append(("", rest))
|
|
452
|
+
for command in ("tparam", "param", "return", "throws"):
|
|
453
|
+
entries = fields.get(command)
|
|
454
|
+
if not entries:
|
|
455
|
+
continue
|
|
456
|
+
paragraphs.append(
|
|
457
|
+
f'<div class="api-doc-field"><p class="api-doc-field-title">{_FIELD_TITLES[command]}</p>'
|
|
458
|
+
)
|
|
459
|
+
if command in {"tparam", "param"}:
|
|
460
|
+
paragraphs.append(
|
|
461
|
+
"<dl>"
|
|
462
|
+
+ "".join(
|
|
463
|
+
f"<dt><code>{html.escape(name)}</code></dt><dd>{_inline_documentation(description)}</dd>"
|
|
464
|
+
for name, description in entries
|
|
465
|
+
)
|
|
466
|
+
+ "</dl>"
|
|
467
|
+
)
|
|
468
|
+
else:
|
|
469
|
+
paragraphs.extend(f"<p>{_inline_documentation(value)}</p>" for _name, value in entries)
|
|
470
|
+
paragraphs.append("</div>")
|
|
471
|
+
return "".join(paragraphs)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _documentation(entity: ApiEntity, graph: ApiGraph, document: Path) -> str:
|
|
475
|
+
_, related = _documentation_blocks(entity, graph)
|
|
476
|
+
description = _documentation_description(entity.documentation)
|
|
477
|
+
if not description and not related:
|
|
478
|
+
description = '<p class="api-undocumented">No API documentation has been provided.</p>'
|
|
479
|
+
if not related:
|
|
480
|
+
return description
|
|
481
|
+
links: list[str] = []
|
|
482
|
+
for text, target in related:
|
|
483
|
+
if target is None:
|
|
484
|
+
links.append(f"<code>{html.escape(text)}</code>")
|
|
485
|
+
else:
|
|
486
|
+
links.append(
|
|
487
|
+
f'<a href="{html.escape(_entity_href(document, target, graph))}"><code>{html.escape(target.name)}</code></a>'
|
|
488
|
+
)
|
|
489
|
+
return (
|
|
490
|
+
description
|
|
491
|
+
+ '<p class="api-related"><strong>Related:</strong> '
|
|
492
|
+
+ " · ".join(links)
|
|
493
|
+
+ "</p>"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _availability(entity: ApiEntity, document: Path, variants: list[str] | None = None) -> str:
|
|
498
|
+
variants = variants if variants else entity.variants
|
|
499
|
+
if not variants:
|
|
500
|
+
return ""
|
|
501
|
+
links = []
|
|
502
|
+
for variant in variants:
|
|
503
|
+
href = _page_href(document, Path("api-variants.md"), _variant_anchor(variant))
|
|
504
|
+
links.append(
|
|
505
|
+
f'<a href="{html.escape(href)}">{html.escape(_variant_display_name(variant))}</a>'
|
|
506
|
+
)
|
|
507
|
+
links.append(
|
|
508
|
+
f'<a href="{html.escape(_page_href(document, Path("api-variants.md")))}">About API variants</a>'
|
|
509
|
+
)
|
|
510
|
+
return (
|
|
511
|
+
'<div class="api-availability"><strong>API variants</strong><span>'
|
|
512
|
+
+ " · ".join(links)
|
|
513
|
+
+ "</span></div>"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _defined_in(entity: ApiEntity, graph: ApiGraph, document: Path) -> str:
|
|
518
|
+
if entity.kind in _CONTAINER_KINDS or entity.source is None:
|
|
519
|
+
return ""
|
|
520
|
+
href = _source_href(document, entity, graph)
|
|
521
|
+
label = Path(entity.source.path).name
|
|
522
|
+
if href:
|
|
523
|
+
return f'<p class="api-defined">Defined in <a href="{html.escape(href)}">{html.escape(label)}</a></p>'
|
|
524
|
+
return f'<p class="api-defined">Defined in <code>{html.escape(label)}</code></p>'
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _definition_row(entity: ApiEntity, graph: ApiGraph, document: Path) -> str:
|
|
528
|
+
if entity.source is None:
|
|
529
|
+
return ""
|
|
530
|
+
href = _source_href(document, entity, graph)
|
|
531
|
+
location = _display_source_path(entity.source.path, graph) + (
|
|
532
|
+
f":{entity.source.line}" if entity.source.line else ""
|
|
533
|
+
)
|
|
534
|
+
value = (
|
|
535
|
+
f'<a href="{html.escape(href)}"><code>{html.escape(location)}</code></a>'
|
|
536
|
+
if href
|
|
537
|
+
else f"<code>{html.escape(location)}</code>"
|
|
538
|
+
)
|
|
539
|
+
return '<div class="api-meta-row"><strong>Definition</strong><span>' + value + "</span></div>"
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _signature_card(
|
|
543
|
+
entity: ApiEntity,
|
|
544
|
+
signature: ApiSignature,
|
|
545
|
+
graph: ApiGraph,
|
|
546
|
+
document: Path,
|
|
547
|
+
*,
|
|
548
|
+
anchor: str,
|
|
549
|
+
documentation: bool = True,
|
|
550
|
+
) -> str:
|
|
551
|
+
if signature.documentation or signature.source is not None:
|
|
552
|
+
entity = dataclasses.replace(
|
|
553
|
+
entity,
|
|
554
|
+
documentation=signature.documentation or entity.documentation,
|
|
555
|
+
source=signature.source or entity.source,
|
|
556
|
+
)
|
|
557
|
+
spelling = _signature_spelling(entity, signature)
|
|
558
|
+
declaration = _highlight_declaration(
|
|
559
|
+
spelling, entity.language, entity=entity, graph=graph, document_path=document
|
|
560
|
+
)
|
|
561
|
+
properties = list(dict.fromkeys([*entity.properties, *signature.qualifiers]))
|
|
562
|
+
rows = []
|
|
563
|
+
if properties:
|
|
564
|
+
rows.append(
|
|
565
|
+
'<div class="api-meta-row"><strong>Properties</strong><span class="api-badges">'
|
|
566
|
+
+ "".join(f"<span>{html.escape(value)}</span>" for value in properties)
|
|
567
|
+
+ "</span></div>"
|
|
568
|
+
)
|
|
569
|
+
if entity.source is not None:
|
|
570
|
+
rows.append(_definition_row(entity, graph, document))
|
|
571
|
+
body = _documentation(entity, graph, document) if documentation else ""
|
|
572
|
+
return (
|
|
573
|
+
f'<article class="api-signature-card" id="{html.escape(anchor)}">'
|
|
574
|
+
f'<pre class="api-signature"><code class="language-{html.escape(entity.language)}">{declaration}</code></pre>'
|
|
575
|
+
+ "".join(rows)
|
|
576
|
+
+ (f'<div class="api-description">{body}</div>' if body else "")
|
|
577
|
+
+ _availability(entity, document, _signature_variants(entity, signature))
|
|
578
|
+
+ "</article>"
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _signature_variants(entity: ApiEntity, signature: ApiSignature) -> list[str]:
|
|
583
|
+
if signature not in entity.signatures:
|
|
584
|
+
return []
|
|
585
|
+
index = entity.signatures.index(signature)
|
|
586
|
+
if index < len(entity.signature_variants):
|
|
587
|
+
return entity.signature_variants[index]
|
|
588
|
+
return []
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _variant_tag(entity: ApiEntity, graph: ApiGraph) -> str:
|
|
592
|
+
"""Compact marker for entities that exist only in some variants."""
|
|
593
|
+
|
|
594
|
+
if not entity.variants or len(graph.variants) < 2:
|
|
595
|
+
return ""
|
|
596
|
+
if set(graph.variants) <= set(entity.variants):
|
|
597
|
+
return ""
|
|
598
|
+
names = " · ".join(_variant_display_name(value) for value in entity.variants)
|
|
599
|
+
return f'<span class="api-variant-tag" title="Only in: {html.escape(names)}">{html.escape(names)}</span>'
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _compact_signature_label(entity: ApiEntity, signature: ApiSignature) -> str:
|
|
603
|
+
if entity.kind == "macro":
|
|
604
|
+
return entity.name
|
|
605
|
+
values = [parameter.type or parameter.name for parameter in signature.parameters]
|
|
606
|
+
return f"{entity.name}({', '.join(values)})"
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _member_anchor_for_signature(entity: ApiEntity, index: int) -> str:
|
|
610
|
+
base = _member_anchor(entity)
|
|
611
|
+
return base if index == 0 else f"{base}-{index + 1}"
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
# ---------------------------------------------------------------------------
|
|
615
|
+
# Page bodies
|
|
616
|
+
# ---------------------------------------------------------------------------
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _namespace_page(
|
|
620
|
+
entity: ApiEntity, graph: ApiGraph, document: Path
|
|
621
|
+
) -> tuple[str, list[tuple[str, str, int]]]:
|
|
622
|
+
title = f"{_kind_label(entity.kind).title()} {entity.qualified_name}"
|
|
623
|
+
body = [f"<h1>{html.escape(title)}</h1>"]
|
|
624
|
+
if entity.documentation.strip():
|
|
625
|
+
body.append(
|
|
626
|
+
'<div class="api-namespace-doc">' + _documentation(entity, graph, document) + "</div>"
|
|
627
|
+
)
|
|
628
|
+
else:
|
|
629
|
+
body.append(
|
|
630
|
+
'<div class="api-namespace-doc api-namespace-doc-generated">'
|
|
631
|
+
f"<p>The <code>{html.escape(entity.qualified_name)}</code> namespace groups the public API "
|
|
632
|
+
"declared in this scope. The hierarchy below expands nested namespaces through their "
|
|
633
|
+
"public types and function families; members of classes and structs remain documented "
|
|
634
|
+
"on their corresponding type pages.</p>"
|
|
635
|
+
"</div>"
|
|
636
|
+
)
|
|
637
|
+
body.extend(
|
|
638
|
+
[
|
|
639
|
+
'<section id="members" class="api-section">',
|
|
640
|
+
"<h2>Members</h2>",
|
|
641
|
+
_legend(graph),
|
|
642
|
+
'<ul class="api-namespace-tree">',
|
|
643
|
+
]
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
def render_namespace_children(container: ApiEntity) -> str:
|
|
647
|
+
children = [
|
|
648
|
+
graph.entities[value]
|
|
649
|
+
for value in container.children
|
|
650
|
+
if value in graph.entities and graph.entities[value].kind != "macro"
|
|
651
|
+
]
|
|
652
|
+
children.sort(key=entity_sort_key)
|
|
653
|
+
parts: list[str] = []
|
|
654
|
+
for child in children:
|
|
655
|
+
parts.append('<li><div class="api-namespace-tree-row">')
|
|
656
|
+
parts.append(_kind_badge(child.kind))
|
|
657
|
+
parts.append(
|
|
658
|
+
f'<a href="{html.escape(_entity_href(document, child, graph))}">{html.escape(child.member_label)}</a>'
|
|
659
|
+
)
|
|
660
|
+
parts.append("</div>")
|
|
661
|
+
if child.kind in _CONTAINER_KINDS:
|
|
662
|
+
nested = render_namespace_children(child)
|
|
663
|
+
if nested:
|
|
664
|
+
parts.append("<ul>" + nested + "</ul>")
|
|
665
|
+
parts.append("</li>")
|
|
666
|
+
return "".join(parts)
|
|
667
|
+
|
|
668
|
+
body.append(render_namespace_children(entity))
|
|
669
|
+
body.extend(["</ul>", _availability(entity, document), "</section>"])
|
|
670
|
+
return "".join(body), [("Members", "members", 1)]
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _record_page(
|
|
674
|
+
entity: ApiEntity, graph: ApiGraph, document: Path
|
|
675
|
+
) -> tuple[str, list[tuple[str, str, int]]]:
|
|
676
|
+
children = [graph.entities[value] for value in entity.children if value in graph.entities]
|
|
677
|
+
children.sort(key=entity_sort_key)
|
|
678
|
+
body = [f"<h1>{html.escape(entity.name)}</h1>", _defined_in(entity, graph, document)]
|
|
679
|
+
if entity.language in {"c", "cpp"} and entity.kind in {"class", "struct", "union", "enum"}:
|
|
680
|
+
scoped = " class" if entity.kind == "enum" and "scoped" in entity.properties else ""
|
|
681
|
+
bases = f" : {', '.join(entity.bases)}" if entity.bases else ""
|
|
682
|
+
declaration_text = f"{entity.kind}{scoped} {entity.name}{bases}"
|
|
683
|
+
if entity.template:
|
|
684
|
+
declaration_text = f"{entity.template}\n{declaration_text}"
|
|
685
|
+
elif entity.language == "python" and entity.kind in {"class", "protocol"}:
|
|
686
|
+
bases = f"({', '.join(entity.bases)})" if entity.bases else ""
|
|
687
|
+
declaration_text = f"class {entity.name}{bases}"
|
|
688
|
+
elif entity.language == "rust" and entity.kind in {"struct", "enum", "trait"}:
|
|
689
|
+
prefix = "pub " if "pub" in entity.properties else ""
|
|
690
|
+
declaration_text = f"{prefix}{entity.kind} {entity.name}"
|
|
691
|
+
else:
|
|
692
|
+
declaration_text = _entity_declaration(entity)
|
|
693
|
+
declaration = _highlight_declaration(
|
|
694
|
+
declaration_text, entity.language, entity=entity, graph=graph, document_path=document
|
|
695
|
+
)
|
|
696
|
+
body.append(
|
|
697
|
+
'<article class="api-entity-card">'
|
|
698
|
+
f'<pre class="api-signature"><code class="language-{html.escape(entity.language)}">{declaration}</code></pre>'
|
|
699
|
+
+ _definition_row(entity, graph, document)
|
|
700
|
+
+ '<div class="api-description">'
|
|
701
|
+
+ _documentation(entity, graph, document)
|
|
702
|
+
+ "</div>"
|
|
703
|
+
+ _availability(entity, document)
|
|
704
|
+
+ "</article>"
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
toc: list[tuple[str, str, int]] = []
|
|
708
|
+
specializations = _specializations(entity, graph)
|
|
709
|
+
if specializations:
|
|
710
|
+
body.append(
|
|
711
|
+
'<section id="specializations" class="api-section"><h2>Specializations</h2>'
|
|
712
|
+
'<div class="api-record-members">'
|
|
713
|
+
)
|
|
714
|
+
toc.append(("Specializations", "specializations", 1))
|
|
715
|
+
for other in specializations:
|
|
716
|
+
# Only the specialization's name is a link, as in every other member row; the
|
|
717
|
+
# highlighted prefix must not contain links of its own, since links cannot nest.
|
|
718
|
+
prefix = _highlight_declaration(
|
|
719
|
+
f"{other.template} {other.kind}".strip(),
|
|
720
|
+
other.language,
|
|
721
|
+
entity=other,
|
|
722
|
+
graph=graph,
|
|
723
|
+
document_path=document,
|
|
724
|
+
)
|
|
725
|
+
href = html.escape(_entity_href(document, other, graph))
|
|
726
|
+
body.append(
|
|
727
|
+
'<div class="api-member-row">'
|
|
728
|
+
+ _kind_badge(other.kind)
|
|
729
|
+
+ f'<code>{prefix} <a href="{href}">{html.escape(other.name)}</a></code>'
|
|
730
|
+
+ (
|
|
731
|
+
'<div class="api-member-doc">'
|
|
732
|
+
+ _documentation(other, graph, document)
|
|
733
|
+
+ "</div>"
|
|
734
|
+
if other.documentation.strip()
|
|
735
|
+
else ""
|
|
736
|
+
)
|
|
737
|
+
+ "</div>"
|
|
738
|
+
)
|
|
739
|
+
body.append("</div></section>")
|
|
740
|
+
|
|
741
|
+
grouped = {
|
|
742
|
+
"Public Types": [
|
|
743
|
+
child for child in children if child.kind in _TYPE_KINDS or child.kind in {"type_alias"}
|
|
744
|
+
],
|
|
745
|
+
"Public Functions": [
|
|
746
|
+
child
|
|
747
|
+
for child in children
|
|
748
|
+
if child.kind in _FUNCTION_KINDS and child.kind != "deduction_guide"
|
|
749
|
+
],
|
|
750
|
+
"Deduction Guides": [child for child in children if child.kind == "deduction_guide"],
|
|
751
|
+
"Public Members": [child for child in children if child.kind in _MEMBER_VALUE_KINDS],
|
|
752
|
+
}
|
|
753
|
+
for heading, values in grouped.items():
|
|
754
|
+
if not values:
|
|
755
|
+
continue
|
|
756
|
+
section_anchor = re.sub(r"[^a-z0-9]+", "-", heading.lower()).strip("-")
|
|
757
|
+
body.append(
|
|
758
|
+
f'<section id="{section_anchor}" class="api-section"><h2>{html.escape(heading)}</h2>'
|
|
759
|
+
)
|
|
760
|
+
toc.append((heading, section_anchor, 1))
|
|
761
|
+
body.append('<div class="api-record-members">')
|
|
762
|
+
for child in values:
|
|
763
|
+
if child.kind in _FUNCTION_KINDS and child.signatures:
|
|
764
|
+
for index, signature in enumerate(child.signatures):
|
|
765
|
+
anchor = _member_anchor_for_signature(child, index)
|
|
766
|
+
label = _compact_signature_label(child, signature)
|
|
767
|
+
toc.append((label, anchor, 2))
|
|
768
|
+
body.append(_signature_card(child, signature, graph, document, anchor=anchor))
|
|
769
|
+
elif child.kind in _MEMBER_VALUE_KINDS:
|
|
770
|
+
anchor = _member_anchor(child)
|
|
771
|
+
toc.append((child.name, anchor, 2))
|
|
772
|
+
summary = _member_summary_spelling(child)
|
|
773
|
+
highlighted = _highlight_declaration(
|
|
774
|
+
summary, child.language, entity=child, graph=graph, document_path=document
|
|
775
|
+
)
|
|
776
|
+
body.append(
|
|
777
|
+
f'<div class="api-member-row" id="{html.escape(anchor)}">'
|
|
778
|
+
+ _kind_badge(child.kind)
|
|
779
|
+
+ f"<code>{highlighted}</code>"
|
|
780
|
+
+ (
|
|
781
|
+
'<div class="api-member-doc">'
|
|
782
|
+
+ _documentation(child, graph, document)
|
|
783
|
+
+ "</div>"
|
|
784
|
+
if child.documentation.strip()
|
|
785
|
+
else ""
|
|
786
|
+
)
|
|
787
|
+
+ "</div>"
|
|
788
|
+
)
|
|
789
|
+
else:
|
|
790
|
+
anchor = _member_anchor(child)
|
|
791
|
+
toc.append((child.name, anchor, 2))
|
|
792
|
+
body.append(
|
|
793
|
+
f'<div class="api-member-row" id="{html.escape(anchor)}">'
|
|
794
|
+
+ _kind_badge(child.kind)
|
|
795
|
+
+ f'<a href="{html.escape(_entity_href(document, child, graph))}">{html.escape(child.name)}</a></div>'
|
|
796
|
+
)
|
|
797
|
+
body.extend(["</div>", "</section>"])
|
|
798
|
+
return "".join(body), toc
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _specializations(entity: ApiEntity, graph: ApiGraph) -> list[ApiEntity]:
|
|
802
|
+
"""Written specializations of a primary class template, which are its siblings."""
|
|
803
|
+
|
|
804
|
+
if "<" in entity.name or not entity.template:
|
|
805
|
+
return []
|
|
806
|
+
values = [
|
|
807
|
+
other
|
|
808
|
+
for other in graph.entities.values()
|
|
809
|
+
if other.parent == entity.parent
|
|
810
|
+
and other.language == entity.language
|
|
811
|
+
and other.name.startswith(entity.name + "<")
|
|
812
|
+
]
|
|
813
|
+
return sorted(values, key=lambda other: other.name)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _function_page(
|
|
817
|
+
entity: ApiEntity, graph: ApiGraph, document: Path
|
|
818
|
+
) -> tuple[str, list[tuple[str, str, int]]]:
|
|
819
|
+
body = [f"<h1>{html.escape(entity.name)}</h1>", _defined_in(entity, graph, document)]
|
|
820
|
+
toc: list[tuple[str, str, int]] = []
|
|
821
|
+
signatures = entity.signatures or [ApiSignature()]
|
|
822
|
+
for index, signature in enumerate(signatures):
|
|
823
|
+
anchor = f"overload-{index + 1}"
|
|
824
|
+
label = _compact_signature_label(entity, signature)
|
|
825
|
+
toc.append((label, anchor, 1))
|
|
826
|
+
body.append(_signature_card(entity, signature, graph, document, anchor=anchor))
|
|
827
|
+
return "".join(body), toc
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _enum_page(
|
|
831
|
+
entity: ApiEntity, graph: ApiGraph, document: Path
|
|
832
|
+
) -> tuple[str, list[tuple[str, str, int]]]:
|
|
833
|
+
return _record_page(entity, graph, document)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _simple_entity_page(
|
|
837
|
+
entity: ApiEntity, graph: ApiGraph, document: Path
|
|
838
|
+
) -> tuple[str, list[tuple[str, str, int]]]:
|
|
839
|
+
body = [f"<h1>{html.escape(entity.name)}</h1>", _defined_in(entity, graph, document)]
|
|
840
|
+
if entity.signatures:
|
|
841
|
+
for index, signature in enumerate(entity.signatures):
|
|
842
|
+
body.append(
|
|
843
|
+
_signature_card(entity, signature, graph, document, anchor=f"signature-{index + 1}")
|
|
844
|
+
)
|
|
845
|
+
else:
|
|
846
|
+
declaration = _highlight_declaration(
|
|
847
|
+
_entity_declaration(entity),
|
|
848
|
+
entity.language,
|
|
849
|
+
entity=entity,
|
|
850
|
+
graph=graph,
|
|
851
|
+
document_path=document,
|
|
852
|
+
)
|
|
853
|
+
body.append(
|
|
854
|
+
'<article class="api-entity-card">'
|
|
855
|
+
f'<pre class="api-signature"><code class="language-{html.escape(entity.language)}">{declaration}</code></pre>'
|
|
856
|
+
+ _definition_row(entity, graph, document)
|
|
857
|
+
+ '<div class="api-description">'
|
|
858
|
+
+ _documentation(entity, graph, document)
|
|
859
|
+
+ "</div>"
|
|
860
|
+
+ _availability(entity, document)
|
|
861
|
+
+ "</article>"
|
|
862
|
+
)
|
|
863
|
+
return "".join(body), []
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _entity_page(
|
|
867
|
+
entity: ApiEntity, graph: ApiGraph, document: Path
|
|
868
|
+
) -> tuple[str, list[tuple[str, str, int]]]:
|
|
869
|
+
if entity.kind in _CONTAINER_KINDS:
|
|
870
|
+
return _namespace_page(entity, graph, document)
|
|
871
|
+
if entity.kind in _CLASS_LIKE_KINDS or entity.kind == "enum":
|
|
872
|
+
return _record_page(entity, graph, document)
|
|
873
|
+
if entity.kind in _FUNCTION_KINDS:
|
|
874
|
+
return _function_page(entity, graph, document)
|
|
875
|
+
return _simple_entity_page(entity, graph, document)
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _hierarchy_entity(entity: ApiEntity, graph: ApiGraph, document: Path) -> str:
|
|
879
|
+
children = _visible_outline_children(entity, graph)
|
|
880
|
+
parts = [
|
|
881
|
+
"<li>",
|
|
882
|
+
_kind_badge(entity.kind),
|
|
883
|
+
f'<a href="{html.escape(_entity_href(document, entity, graph))}">{html.escape(entity.member_label)}</a>',
|
|
884
|
+
_variant_tag(entity, graph),
|
|
885
|
+
]
|
|
886
|
+
if children:
|
|
887
|
+
parts.append("<ul>")
|
|
888
|
+
parts.extend(_hierarchy_entity(child, graph, document) for child in children)
|
|
889
|
+
parts.append("</ul>")
|
|
890
|
+
parts.append("</li>")
|
|
891
|
+
return "".join(parts)
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def _file_tree(graph: ApiGraph) -> dict[str, object]:
|
|
895
|
+
root: dict[str, object] = {}
|
|
896
|
+
for source in sorted(graph.sources):
|
|
897
|
+
node = root
|
|
898
|
+
display_source = _display_source_path(source, graph)
|
|
899
|
+
parts = PurePosixPath(display_source).parts
|
|
900
|
+
for part in parts[:-1]:
|
|
901
|
+
node = node.setdefault(part, {}) # type: ignore[assignment]
|
|
902
|
+
node.setdefault("__files__", []).append((parts[-1], source)) # type: ignore[union-attr]
|
|
903
|
+
return root
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def _file_tree_html(node: dict[str, object], document: Path) -> str:
|
|
907
|
+
parts = ['<ul class="api-file-tree">']
|
|
908
|
+
files = node.get("__files__", [])
|
|
909
|
+
if isinstance(files, list):
|
|
910
|
+
for filename, source_path in sorted(files, key=lambda item: str(item[0]).casefold()):
|
|
911
|
+
parts.append(
|
|
912
|
+
'<li><span class="api-kind file-kind" title="file">F</span>'
|
|
913
|
+
f'<a href="{html.escape(_page_href(document, _source_document(str(source_path))))}">{html.escape(str(filename))}</a></li>'
|
|
914
|
+
)
|
|
915
|
+
for name, child in sorted(node.items()):
|
|
916
|
+
if name == "__files__" or not isinstance(child, dict):
|
|
917
|
+
continue
|
|
918
|
+
parts.append(
|
|
919
|
+
f'<li><span class="api-kind file-kind" title="directory">D</span>'
|
|
920
|
+
f"<span>{html.escape(name)}</span>"
|
|
921
|
+
)
|
|
922
|
+
parts.append(_file_tree_html(child, document))
|
|
923
|
+
parts.append("</li>")
|
|
924
|
+
parts.append("</ul>")
|
|
925
|
+
return "".join(parts)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
_HTML_COMMENT = re.compile(r"<!--.*?-->[ \t]*\n?", re.DOTALL)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _render_markdown(text: str, graph: ApiGraph) -> str:
|
|
932
|
+
"""Render human-authored Markdown used by the standalone API site.
|
|
933
|
+
|
|
934
|
+
Raw HTML is not rendered, so HTML comments such as the SPDX header of the introduction file
|
|
935
|
+
are removed instead of being shown as text.
|
|
936
|
+
"""
|
|
937
|
+
|
|
938
|
+
substituted = (
|
|
939
|
+
_HTML_COMMENT.sub("", text)
|
|
940
|
+
.replace("{{ project }}", graph.project)
|
|
941
|
+
.replace("{{ version }}", graph.version)
|
|
942
|
+
.replace("{{ language }}", graph.language)
|
|
943
|
+
)
|
|
944
|
+
return _MARKDOWN.render(substituted)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def _home_page(
|
|
948
|
+
graph: ApiGraph,
|
|
949
|
+
document: Path,
|
|
950
|
+
project_docs_url: str,
|
|
951
|
+
*,
|
|
952
|
+
introduction: str | None = None,
|
|
953
|
+
) -> tuple[str, list[tuple[str, str, int]]]:
|
|
954
|
+
del project_docs_url
|
|
955
|
+
roots = [entity for entity in graph.roots() if entity.kind != "macro"]
|
|
956
|
+
macros = sorted(
|
|
957
|
+
(entity for entity in graph.entities.values() if entity.kind == "macro"),
|
|
958
|
+
key=entity_sort_key,
|
|
959
|
+
)
|
|
960
|
+
if introduction is None:
|
|
961
|
+
introduction = (
|
|
962
|
+
"# API Documentation Home\n\n"
|
|
963
|
+
"This site is the generated API reference for this version of `{{ project }}`. "
|
|
964
|
+
"For tutorials, how-to guides, explanations, and general project information, see the "
|
|
965
|
+
"[main project documentation](projectdocs:).\n\n"
|
|
966
|
+
"cpdocs extracts semantic declarations into a language-neutral API graph and renders this "
|
|
967
|
+
"versioned reference directly as a code-oriented site.\n\n"
|
|
968
|
+
"The [API variants and features](api-variants/) page explains how configured features select "
|
|
969
|
+
"entity variants, which inputs participate, and which parser predefinitions are used.\n"
|
|
970
|
+
)
|
|
971
|
+
body = [
|
|
972
|
+
_render_markdown(introduction, graph),
|
|
973
|
+
'<section id="api-hierarchy" class="api-section"><h2>API hierarchy</h2>',
|
|
974
|
+
_legend(graph),
|
|
975
|
+
'<ul class="api-hierarchy">',
|
|
976
|
+
]
|
|
977
|
+
body.extend(_hierarchy_entity(entity, graph, document) for entity in roots)
|
|
978
|
+
if macros:
|
|
979
|
+
body.append("<li><strong>Macros</strong><ul>")
|
|
980
|
+
for macro in macros:
|
|
981
|
+
body.append(
|
|
982
|
+
"<li>"
|
|
983
|
+
+ _kind_badge("macro")
|
|
984
|
+
+ f'<a href="{html.escape(_entity_href(document, macro, graph))}">{html.escape(macro.name)}</a></li>'
|
|
985
|
+
)
|
|
986
|
+
body.append("</ul></li>")
|
|
987
|
+
body.extend(
|
|
988
|
+
[
|
|
989
|
+
"</ul></section>",
|
|
990
|
+
'<section id="file-hierarchy" class="api-section"><h2>File Hierarchy</h2>',
|
|
991
|
+
_file_tree_html(_file_tree(graph), document),
|
|
992
|
+
"</section>",
|
|
993
|
+
]
|
|
994
|
+
)
|
|
995
|
+
return "".join(body), [
|
|
996
|
+
("API hierarchy", "api-hierarchy", 1),
|
|
997
|
+
("File Hierarchy", "file-hierarchy", 1),
|
|
998
|
+
]
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _variant_page(graph: ApiGraph, document: Path) -> tuple[str, list[tuple[str, str, int]]]:
|
|
1002
|
+
del document
|
|
1003
|
+
axes = graph.metadata.get("features", {})
|
|
1004
|
+
if not isinstance(axes, dict):
|
|
1005
|
+
axes = {}
|
|
1006
|
+
units = graph.metadata.get("units", [])
|
|
1007
|
+
if not isinstance(units, list):
|
|
1008
|
+
units = []
|
|
1009
|
+
|
|
1010
|
+
def chips(values: object) -> str:
|
|
1011
|
+
if not isinstance(values, list) or not values:
|
|
1012
|
+
return '<span class="api-none">none</span>'
|
|
1013
|
+
return " ".join(
|
|
1014
|
+
f'<code class="api-chip">{html.escape(str(value))}</code>' for value in values
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
def selected(name: str, axis: str) -> list[str]:
|
|
1018
|
+
variant = graph.variants.get(name, {})
|
|
1019
|
+
features = variant.get("features", {}) if isinstance(variant, dict) else {}
|
|
1020
|
+
values = features.get(axis, []) if isinstance(features, dict) else []
|
|
1021
|
+
return [str(value) for value in values] if isinstance(values, list) else []
|
|
1022
|
+
|
|
1023
|
+
primary = next(
|
|
1024
|
+
(
|
|
1025
|
+
name
|
|
1026
|
+
for name, value in graph.variants.items()
|
|
1027
|
+
if isinstance(value, dict) and value.get("primary")
|
|
1028
|
+
),
|
|
1029
|
+
None,
|
|
1030
|
+
)
|
|
1031
|
+
unit_text = " · ".join(
|
|
1032
|
+
f"<code>{html.escape(str(unit.get('id')))}</code> ({html.escape(str(unit.get('language')))})"
|
|
1033
|
+
for unit in units
|
|
1034
|
+
if isinstance(unit, dict)
|
|
1035
|
+
)
|
|
1036
|
+
body = [
|
|
1037
|
+
"<h1>API Variants</h1>",
|
|
1038
|
+
"<p>The project's build system declares feature axes and the variants worth documenting. "
|
|
1039
|
+
"cpdocs extracts the API once per variant and merges the results, so every entity records "
|
|
1040
|
+
"the variants in which it exists.</p>",
|
|
1041
|
+
'<section id="overview" class="api-section"><h2>Overview</h2>',
|
|
1042
|
+
"<table><tbody>",
|
|
1043
|
+
f"<tr><th>Project</th><td><code>{html.escape(graph.project)}</code></td></tr>",
|
|
1044
|
+
"<tr><th>Variants</th><td>"
|
|
1045
|
+
+ " · ".join(
|
|
1046
|
+
html.escape(_variant_display_name(name)) + (" (primary)" if name == primary else "")
|
|
1047
|
+
for name in graph.variants
|
|
1048
|
+
)
|
|
1049
|
+
+ "</td></tr>",
|
|
1050
|
+
]
|
|
1051
|
+
if unit_text:
|
|
1052
|
+
body.append(f"<tr><th>Units</th><td>{unit_text}</td></tr>")
|
|
1053
|
+
generator = graph.metadata.get("generator")
|
|
1054
|
+
if generator:
|
|
1055
|
+
body.append(f"<tr><th>Described by</th><td>{html.escape(str(generator))}</td></tr>")
|
|
1056
|
+
body.append("</tbody></table></section>")
|
|
1057
|
+
toc: list[tuple[str, str, int]] = [("Overview", "overview", 1)]
|
|
1058
|
+
|
|
1059
|
+
if axes:
|
|
1060
|
+
names = list(graph.variants)
|
|
1061
|
+
# With one axis its name heads the value column and each row is just the value. With
|
|
1062
|
+
# several axes, each axis gets a group row and its values are listed below it.
|
|
1063
|
+
single = len(axes) == 1
|
|
1064
|
+
axis_name = str(next(iter(axes)))
|
|
1065
|
+
intro = (
|
|
1066
|
+
f"Which values of <code>{html.escape(axis_name)}</code> each variant selects."
|
|
1067
|
+
if single
|
|
1068
|
+
else "Which feature values each variant selects."
|
|
1069
|
+
)
|
|
1070
|
+
first_header = f"<code>{html.escape(axis_name)}</code>" if single else "Feature"
|
|
1071
|
+
body.extend(
|
|
1072
|
+
[
|
|
1073
|
+
'<section id="features" class="api-section"><h2>Features</h2>',
|
|
1074
|
+
f'<p class="api-section-intro">{intro}</p>',
|
|
1075
|
+
f'<div class="table-scroll"><table><thead><tr><th scope="col">{first_header}</th>'
|
|
1076
|
+
+ "".join(
|
|
1077
|
+
f'<th scope="col">{html.escape(_variant_display_name(name))}</th>'
|
|
1078
|
+
for name in names
|
|
1079
|
+
)
|
|
1080
|
+
+ "</tr></thead><tbody>",
|
|
1081
|
+
]
|
|
1082
|
+
)
|
|
1083
|
+
for axis, values in axes.items():
|
|
1084
|
+
if not single:
|
|
1085
|
+
body.append(
|
|
1086
|
+
f'<tr class="api-feature-axis"><th scope="rowgroup" colspan="{len(names) + 1}">'
|
|
1087
|
+
f"<code>{html.escape(str(axis))}</code></th></tr>"
|
|
1088
|
+
)
|
|
1089
|
+
for value in values if isinstance(values, list) else []:
|
|
1090
|
+
row = [f'<th scope="row"><code>{html.escape(str(value))}</code></th>']
|
|
1091
|
+
for name in names:
|
|
1092
|
+
row.append(
|
|
1093
|
+
"<td>yes</td>" if str(value) in selected(name, str(axis)) else "<td>—</td>"
|
|
1094
|
+
)
|
|
1095
|
+
body.append("<tr>" + "".join(row) + "</tr>")
|
|
1096
|
+
body.append("</tbody></table></div></section>")
|
|
1097
|
+
toc.append(("Features", "features", 1))
|
|
1098
|
+
|
|
1099
|
+
body.extend(
|
|
1100
|
+
[
|
|
1101
|
+
'<section id="api-variants" class="api-section"><h2>Variants</h2>',
|
|
1102
|
+
'<div class="api-variant-grid">',
|
|
1103
|
+
]
|
|
1104
|
+
)
|
|
1105
|
+
toc.append(("Variants", "api-variants", 1))
|
|
1106
|
+
for name in graph.variants:
|
|
1107
|
+
anchor = _variant_anchor(name)
|
|
1108
|
+
display = _variant_display_name(name)
|
|
1109
|
+
label = "Primary variant" if name == primary else "Variant"
|
|
1110
|
+
body.extend(
|
|
1111
|
+
[
|
|
1112
|
+
f'<article id="{html.escape(anchor)}" class="api-variant-card">',
|
|
1113
|
+
f'<header><h3>{html.escape(display)}</h3><span class="api-variant-card-label">{label}</span></header>',
|
|
1114
|
+
"<dl>",
|
|
1115
|
+
]
|
|
1116
|
+
)
|
|
1117
|
+
for axis in axes:
|
|
1118
|
+
body.append(
|
|
1119
|
+
f"<div><dt>{html.escape(str(axis))}</dt><dd>"
|
|
1120
|
+
+ chips(selected(name, str(axis)))
|
|
1121
|
+
+ "</dd></div>"
|
|
1122
|
+
)
|
|
1123
|
+
if not axes:
|
|
1124
|
+
body.append(
|
|
1125
|
+
'<div><dt>Features</dt><dd><span class="api-none">none declared</span></dd></div>'
|
|
1126
|
+
)
|
|
1127
|
+
body.append("</dl></article>")
|
|
1128
|
+
toc.append((display, anchor, 2))
|
|
1129
|
+
body.append("</div></section>")
|
|
1130
|
+
return "".join(body), toc
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def _macros_page(graph: ApiGraph, document: Path) -> tuple[str, list[tuple[str, str, int]]]:
|
|
1134
|
+
macros = sorted(
|
|
1135
|
+
(entity for entity in graph.entities.values() if entity.kind == "macro"),
|
|
1136
|
+
key=entity_sort_key,
|
|
1137
|
+
)
|
|
1138
|
+
body = ['<h1>Macros</h1><ul class="api-member-list">']
|
|
1139
|
+
for macro in macros:
|
|
1140
|
+
body.append(
|
|
1141
|
+
"<li>"
|
|
1142
|
+
+ _kind_badge("macro")
|
|
1143
|
+
+ f'<a href="{html.escape(_entity_href(document, macro, graph))}">{html.escape(macro.name)}</a></li>'
|
|
1144
|
+
)
|
|
1145
|
+
body.append("</ul>")
|
|
1146
|
+
return "".join(body), []
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
def _source_page(
|
|
1150
|
+
source_path: str, content: str, document: Path, graph: ApiGraph
|
|
1151
|
+
) -> tuple[str, list[tuple[str, str, int]]]:
|
|
1152
|
+
language = _source_language(source_path)
|
|
1153
|
+
if language == "cpp":
|
|
1154
|
+
# Some generated headers align macro-continuation backslashes with large runs of spaces.
|
|
1155
|
+
# That is useful in raw source but produces visually detached backslashes in the browser.
|
|
1156
|
+
# Preserve the source semantics while normalizing only the rendered representation.
|
|
1157
|
+
content = "\n".join(
|
|
1158
|
+
re.sub(r"[ \t]+\\[ \t]*$", r" \\", line.rstrip()) for line in content.splitlines()
|
|
1159
|
+
) + ("\n" if content.endswith("\n") else "")
|
|
1160
|
+
highlighted = _highlight_source_lines(content, language)
|
|
1161
|
+
display_path = _display_source_path(source_path, graph)
|
|
1162
|
+
generated = graph.metadata.get("generated_sources", [])
|
|
1163
|
+
note = (
|
|
1164
|
+
'<p class="api-section-intro">Generated by the build; not part of the repository.</p>'
|
|
1165
|
+
if isinstance(generated, list) and source_path in generated
|
|
1166
|
+
else ""
|
|
1167
|
+
)
|
|
1168
|
+
lines = [
|
|
1169
|
+
f"<h1>File: {html.escape(display_path)}</h1>" + note,
|
|
1170
|
+
f'<pre class="api-source"><code class="language-{html.escape(language)}">',
|
|
1171
|
+
]
|
|
1172
|
+
for number, value in enumerate(highlighted, 1):
|
|
1173
|
+
lines.append(
|
|
1174
|
+
f'<span class="source-line" id="L{number}"><a class="source-number" href="#L{number}">{number}</a><span class="source-code">{value}</span></span>'
|
|
1175
|
+
)
|
|
1176
|
+
lines.extend(["</code></pre>"])
|
|
1177
|
+
return "".join(lines), []
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
# ---------------------------------------------------------------------------
|
|
1181
|
+
# Complete layout and assets
|
|
1182
|
+
# ---------------------------------------------------------------------------
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _page_title(entity: ApiEntity | None, graph: ApiGraph, special: str) -> str:
|
|
1186
|
+
if entity is not None:
|
|
1187
|
+
return f"{entity.name} — {graph.project} API documentation"
|
|
1188
|
+
if special == "variants":
|
|
1189
|
+
return f"API Variants — {graph.project} API documentation"
|
|
1190
|
+
if special == "macros":
|
|
1191
|
+
return f"Macros — {graph.project} API documentation"
|
|
1192
|
+
return f"{graph.project} API documentation"
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def _project_docs_href(document: Path, project_docs_url: str) -> str:
|
|
1196
|
+
if re.match(r"^[A-Za-z][A-Za-z0-9+.-]*://", project_docs_url) or project_docs_url.startswith(
|
|
1197
|
+
"/"
|
|
1198
|
+
):
|
|
1199
|
+
return project_docs_url
|
|
1200
|
+
root = _root_relative(document, ".")
|
|
1201
|
+
if root == ".":
|
|
1202
|
+
root = ""
|
|
1203
|
+
elif not root.endswith("/"):
|
|
1204
|
+
root += "/"
|
|
1205
|
+
return posixpath.normpath(root + project_docs_url).rstrip("/") + "/"
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def _layout(
|
|
1209
|
+
*,
|
|
1210
|
+
graph: ApiGraph,
|
|
1211
|
+
document: Path,
|
|
1212
|
+
body: str,
|
|
1213
|
+
toc: list[tuple[str, str, int]],
|
|
1214
|
+
current: ApiEntity | None,
|
|
1215
|
+
special: str,
|
|
1216
|
+
project_docs_url: str,
|
|
1217
|
+
copyright_text: str,
|
|
1218
|
+
) -> str:
|
|
1219
|
+
body = _resolve_projectdocs_links(body, document, project_docs_url)
|
|
1220
|
+
# The version query changes whenever the asset does, so a browser never pairs new pages with a
|
|
1221
|
+
# stylesheet or script it cached from an older cpdocs.
|
|
1222
|
+
stylesheet = _root_relative(document, "assets/cpdocs-api.css") + f"?v={_asset_version('css')}"
|
|
1223
|
+
script = _root_relative(document, "assets/cpdocs-api.js") + f"?v={_asset_version('js')}"
|
|
1224
|
+
home = _page_href(document, Path("index.md"))
|
|
1225
|
+
project_docs = _project_docs_href(document, project_docs_url)
|
|
1226
|
+
version_root = _root_relative(document, ".")
|
|
1227
|
+
if version_root == ".":
|
|
1228
|
+
version_root = "./"
|
|
1229
|
+
elif not version_root.endswith("/"):
|
|
1230
|
+
version_root += "/"
|
|
1231
|
+
api_root = posixpath.normpath(version_root + "../").rstrip("/") + "/"
|
|
1232
|
+
current_route = _output_directory(document).as_posix().strip(".")
|
|
1233
|
+
if current_route and not current_route.endswith("/"):
|
|
1234
|
+
current_route += "/"
|
|
1235
|
+
return f'''<!doctype html>
|
|
1236
|
+
<html lang="en" data-theme="light">
|
|
1237
|
+
<head>
|
|
1238
|
+
<meta charset="utf-8">
|
|
1239
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1240
|
+
<meta name="color-scheme" content="light dark">
|
|
1241
|
+
<script>document.documentElement.classList.add("js");try{{for(const k of ["sidebar","toc"]){{const v=localStorage.getItem("cpdocs-api-"+k+"-width");if(v)document.documentElement.style.setProperty("--api-"+k+"-width",v+"px")}}}}catch(e){{}}try{{const t=localStorage.getItem("cpdocs-api-theme");document.documentElement.dataset.theme=t==="dark"||t==="light"?t:(matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light")}}catch(e){{}}</script>
|
|
1242
|
+
<title>{html.escape(_page_title(current, graph, special))}</title>
|
|
1243
|
+
<link rel="stylesheet" href="{html.escape(stylesheet)}">
|
|
1244
|
+
</head>
|
|
1245
|
+
<body data-version-root="{html.escape(version_root)}" data-api-root="{html.escape(api_root)}" data-current-route="{html.escape(current_route)}">
|
|
1246
|
+
<header class="api-topbar">
|
|
1247
|
+
<a class="api-brand" href="{html.escape(home)}">{html.escape(graph.project)} API documentation</a>
|
|
1248
|
+
<div class="api-topbar-actions">
|
|
1249
|
+
<div class="api-search"><span aria-hidden="true">⌕</span><input type="search" placeholder="Search" aria-label="Search API"><div class="api-search-results" hidden></div></div>
|
|
1250
|
+
<a class="api-project-docs" href="{html.escape(project_docs)}" title="Project documentation"><span aria-hidden="true">←</span><span class="api-long"> Project documentation</span><span class="api-short"> Docs</span></a>
|
|
1251
|
+
<select class="api-version" aria-label="API version"><option>{html.escape(graph.version)}</option></select>
|
|
1252
|
+
<button class="api-theme" type="button" aria-label="Toggle color theme">◐</button>
|
|
1253
|
+
</div>
|
|
1254
|
+
</header>
|
|
1255
|
+
<div class="api-shell">
|
|
1256
|
+
<aside class="api-sidebar">{_outline(graph, document, current, current_special=special)}</aside>
|
|
1257
|
+
<main class="api-main">{body}</main>
|
|
1258
|
+
{_toc(toc)}
|
|
1259
|
+
<div class="api-resizer api-resizer-sidebar" data-resize="sidebar" role="separator" aria-orientation="vertical" aria-label="Resize the outline" title="Drag to resize, double-click to reset" tabindex="0"></div>
|
|
1260
|
+
<div class="api-resizer api-resizer-toc" data-resize="toc" role="separator" aria-orientation="vertical" aria-label="Resize the page contents" title="Drag to resize, double-click to reset" tabindex="0"></div>
|
|
1261
|
+
</div>
|
|
1262
|
+
<footer class="api-footer"><span>{html.escape(copyright_text)}</span><span>Generated by cpdocs.</span></footer>
|
|
1263
|
+
<script src="{html.escape(script)}"></script>
|
|
1264
|
+
</body>
|
|
1265
|
+
</html>
|
|
1266
|
+
'''
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
_CSS = r"""
|
|
1270
|
+
:root {
|
|
1271
|
+
--api-accent: #2f62b5;
|
|
1272
|
+
--api-accent-2: #168b98;
|
|
1273
|
+
--api-bg: #ffffff;
|
|
1274
|
+
--api-fg: #20242a;
|
|
1275
|
+
--api-muted: #66707c;
|
|
1276
|
+
--api-border: #d8dde3;
|
|
1277
|
+
--api-soft: #f5f6f8;
|
|
1278
|
+
--api-selected: #eef1fb;
|
|
1279
|
+
--api-code: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
|
1280
|
+
--api-ui: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1281
|
+
--api-tree-indent: 2.45rem;
|
|
1282
|
+
}
|
|
1283
|
+
html[data-theme="dark"] {
|
|
1284
|
+
--api-accent: #83b7ff;
|
|
1285
|
+
--api-accent-2: #68d3dd;
|
|
1286
|
+
--api-bg: #17191c;
|
|
1287
|
+
--api-fg: #e8eaf0;
|
|
1288
|
+
--api-muted: #a9b1bc;
|
|
1289
|
+
--api-border: #3b4048;
|
|
1290
|
+
--api-soft: #22262b;
|
|
1291
|
+
--api-selected: #29334a;
|
|
1292
|
+
}
|
|
1293
|
+
* { box-sizing: border-box; }
|
|
1294
|
+
html, body { margin: 0; min-height: 100%; background: var(--api-bg); color: var(--api-fg); font-family: var(--api-ui); }
|
|
1295
|
+
a { color: var(--api-accent); text-decoration: none; }
|
|
1296
|
+
a:hover { text-decoration: underline; }
|
|
1297
|
+
code, pre { font-family: var(--api-code); }
|
|
1298
|
+
.api-topbar { height: 3rem; position: sticky; top: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0 .9rem; border-bottom: 1px solid var(--api-border); background: var(--api-bg); }
|
|
1299
|
+
.api-brand { color: var(--api-fg); font-weight: 650; white-space: nowrap; }
|
|
1300
|
+
.api-topbar-actions { display: flex; align-items: center; gap: .7rem; margin-left: auto; }
|
|
1301
|
+
.api-search { position: relative; display: flex; align-items: center; gap: .4rem; border: 1px solid var(--api-border); border-radius: .4rem; padding: .25rem .55rem; min-width: 12rem; }
|
|
1302
|
+
.api-search input { border: 0; outline: 0; background: transparent; color: var(--api-fg); width: 100%; }
|
|
1303
|
+
.api-search-results { position: absolute; top: calc(100% + .3rem); right: 0; width: 26rem; max-height: 24rem; overflow: auto; background: var(--api-bg); border: 1px solid var(--api-border); border-radius: .35rem; box-shadow: 0 .4rem 1rem #0002; padding: .25rem; }
|
|
1304
|
+
.api-search-results a { display: block; padding: .35rem .45rem; border-radius: .25rem; }
|
|
1305
|
+
.api-search-results a:hover { background: var(--api-soft); text-decoration: none; }
|
|
1306
|
+
.api-project-docs .api-short { display: none; }
|
|
1307
|
+
.api-outline-panel-toggle { display: none; }
|
|
1308
|
+
.api-version { min-width: 6.5rem; }
|
|
1309
|
+
.api-feature-axis th { padding-top: .8rem; color: var(--api-muted); font-weight: 650; text-align: left; }
|
|
1310
|
+
tbody th[scope="row"] { font-weight: 400; text-align: left; }
|
|
1311
|
+
.api-doc-field { margin-top: .9rem; }
|
|
1312
|
+
.api-description .api-doc-field-title { margin: 0 0 .35rem; color: var(--api-muted); font-size: .78rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
|
|
1313
|
+
.api-doc-field dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: .3rem 1.1rem; margin: 0; }
|
|
1314
|
+
.api-doc-field dt { margin: 0; font-weight: 700; }
|
|
1315
|
+
.api-doc-field dt code { color: var(--api-accent); }
|
|
1316
|
+
.api-doc-field dd { margin: 0; }
|
|
1317
|
+
.api-doc-field > p:not(.api-doc-field-title) { margin: 0; }
|
|
1318
|
+
.api-variant-tag { margin-left: .45rem; padding: .02rem .38rem; border: 1px solid var(--api-border); border-radius: .7rem; color: var(--api-muted); font-size: .72rem; white-space: nowrap; }
|
|
1319
|
+
.api-project-docs { color: #fff; background: var(--api-accent); border-radius: .25rem; padding: .38rem .65rem; font-weight: 650; white-space: nowrap; }
|
|
1320
|
+
.api-version, .api-theme { height: 2rem; border: 1px solid var(--api-border); border-radius: .3rem; background: var(--api-bg); color: var(--api-fg); }
|
|
1321
|
+
.api-theme { width: 2rem; cursor: pointer; }
|
|
1322
|
+
:root { --api-sidebar-default: 20.5rem; --api-toc-default: 24rem; }
|
|
1323
|
+
.api-shell { position: relative; display: grid; grid-template-columns: var(--api-sidebar-width, var(--api-sidebar-default)) minmax(0, 1fr) var(--api-toc-width, var(--api-toc-default)); min-height: calc(100vh - 5rem); }
|
|
1324
|
+
.api-resizer { position: absolute; top: 0; bottom: 0; z-index: 15; width: 9px; cursor: col-resize; touch-action: none; outline: none; }
|
|
1325
|
+
.api-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; left: 3px; width: 3px; background: transparent; transition: background .12s; }
|
|
1326
|
+
.api-resizer:hover::after, .api-resizer:focus-visible::after, .is-resizing .api-resizer::after { background: var(--api-accent); }
|
|
1327
|
+
.api-resizer-sidebar { left: calc(var(--api-sidebar-width, var(--api-sidebar-default)) - 5px); }
|
|
1328
|
+
.api-resizer-toc { right: calc(var(--api-toc-width, var(--api-toc-default)) - 5px); }
|
|
1329
|
+
html.is-resizing, html.is-resizing * { cursor: col-resize !important; user-select: none; }
|
|
1330
|
+
.api-sidebar, .api-toc { position: sticky; top: 3rem; align-self: start; height: calc(100vh - 3rem); overflow: auto; padding: 1.15rem .8rem 2rem; }
|
|
1331
|
+
.api-sidebar { border-right: 1px solid var(--api-border); }
|
|
1332
|
+
.api-toc { border-left: 1px solid var(--api-border); }
|
|
1333
|
+
.api-main { width: min(80%, 80rem); min-width: 0; justify-self: center; padding: 1.45rem 0 4rem; }
|
|
1334
|
+
.api-main h1 { font-size: 1.75rem; line-height: 1.15; border-bottom: 2px solid var(--api-accent); padding-bottom: .25rem; margin: 0 0 .65rem; }
|
|
1335
|
+
.api-main h2 { font-size: 1.28rem; line-height: 1.2; border-left: .28rem solid var(--api-accent-2); padding-left: .55rem; margin: 1.2rem 0 .55rem; }
|
|
1336
|
+
.api-main h3 { font-size: 1.04rem; margin: .85rem 0 .35rem; }
|
|
1337
|
+
.api-main p, .api-main li, .api-main td, .api-main th { line-height: 1.34; }
|
|
1338
|
+
.api-main p { margin: .4rem 0; }
|
|
1339
|
+
.api-defined { margin: -.1rem 0 .75rem !important; }
|
|
1340
|
+
.api-sidebar h2, .api-toc h2 { font-size: 1.18rem; margin: .15rem 0 .65rem; }
|
|
1341
|
+
.api-outline-root, .api-outline-children { list-style: none; margin: 0; padding: 0; }
|
|
1342
|
+
.api-outline-root > li { margin: 0; }
|
|
1343
|
+
.api-outline-row { display: grid; grid-template-columns: 1.45rem max-content 1.4rem; column-gap: .68rem; align-items: center; width: fit-content; min-height: 1.72rem; max-width: 100%; }
|
|
1344
|
+
.api-outline-row.is-current { background: var(--api-selected); border-radius: .25rem; padding-right: .18rem; }
|
|
1345
|
+
.api-outline-link { color: var(--api-fg); padding: .1rem 0; white-space: nowrap; }
|
|
1346
|
+
.api-outline-row.is-current .api-outline-link { color: var(--api-accent); font-weight: 650; }
|
|
1347
|
+
.api-outline-root > li > .api-outline-row .api-outline-link { font-weight: 500; }
|
|
1348
|
+
.api-outline-root > li > .api-outline-row .api-outline-link[href*="api/"] { font-weight: 700; }
|
|
1349
|
+
.api-outline-children { margin: .08rem 0 .12rem var(--api-tree-indent); padding-left: 1.15rem; }
|
|
1350
|
+
.api-outline-children > li { position: relative; }
|
|
1351
|
+
.api-outline-children > li::before { content: ""; position: absolute; left: -1.15rem; top: -.08rem; bottom: -.12rem; border-left: 1px solid var(--api-border); }
|
|
1352
|
+
.api-outline-children > li::after { content: ""; position: absolute; left: -1.15rem; top: .85rem; width: 1rem; border-top: 1px solid var(--api-border); }
|
|
1353
|
+
.api-outline-children > li:last-child::before { bottom: auto; height: .94rem; }
|
|
1354
|
+
.api-kind { display: inline-grid; place-items: center; min-width: 1.42rem; height: 1.25rem; padding: 0 .22rem; border: 1px solid var(--api-border); border-radius: .23rem; color: var(--api-muted); font-family: var(--api-code); font-size: .68rem; line-height: 1; font-weight: 950; }
|
|
1355
|
+
.api-kind-empty { border-color: transparent; }
|
|
1356
|
+
.api-outline-toggle { position: relative; width: 1.4rem; height: 1.4rem; border: 1px solid var(--api-border); border-radius: .25rem; background: transparent; color: var(--api-muted); font: 800 1rem/1 var(--api-code); cursor: pointer; }
|
|
1357
|
+
.api-outline-toggle::before { content: "+"; position: absolute; inset: 0; display: grid; place-items: center; }
|
|
1358
|
+
.api-outline-toggle[aria-expanded="true"]::before { content: "−"; }
|
|
1359
|
+
.api-outline-toggle:hover { color: var(--api-accent); border-color: var(--api-accent); }
|
|
1360
|
+
.api-outline-children[hidden] { display: none; }
|
|
1361
|
+
.api-toc ul { list-style: none; padding: 0; margin: 0; }
|
|
1362
|
+
.api-toc li { margin: .34rem 0; }
|
|
1363
|
+
.api-toc .toc-level-2 { padding-left: .85rem; font-size: .9rem; }
|
|
1364
|
+
.api-toc a { color: var(--api-muted); }
|
|
1365
|
+
.api-toc a:hover { color: var(--api-accent); }
|
|
1366
|
+
.api-section { margin-top: 1rem; }
|
|
1367
|
+
.api-legend { display: flex; flex-wrap: wrap; align-items: center; gap: .28rem; margin: .25rem 0 .55rem; font-size: .83rem; }
|
|
1368
|
+
.api-legend-entry { display: inline-flex; align-items: center; gap: .28rem; }
|
|
1369
|
+
.api-legend-separator { color: var(--api-muted); }
|
|
1370
|
+
.api-member-list, .api-hierarchy, .api-hierarchy ul, .api-file-tree { list-style: none; margin: .25rem 0; padding-left: 1.25rem; }
|
|
1371
|
+
.api-member-list { padding-left: 0; }
|
|
1372
|
+
.api-member-list li { display: flex; align-items: center; gap: .55rem; padding: .24rem 0; border-bottom: 1px solid var(--api-border); }
|
|
1373
|
+
.api-namespace-tree, .api-namespace-tree ul { list-style: none; margin: .2rem 0; padding-left: 0; }
|
|
1374
|
+
.api-namespace-tree ul { margin-left: 2.35rem; padding-left: 1.05rem; border-left: 1px solid var(--api-border); }
|
|
1375
|
+
.api-namespace-tree li { margin: 0; }
|
|
1376
|
+
.api-namespace-tree-row { display: flex; align-items: center; gap: .52rem; min-height: 1.72rem; border-bottom: 1px solid var(--api-border); }
|
|
1377
|
+
.api-namespace-tree ul > li > .api-namespace-tree-row { position: relative; }
|
|
1378
|
+
.api-namespace-tree ul > li > .api-namespace-tree-row::before { content: ""; position: absolute; left: -1.05rem; width: .8rem; border-top: 1px solid var(--api-border); }
|
|
1379
|
+
.api-hierarchy li, .api-file-tree li { margin: .18rem 0; }
|
|
1380
|
+
/* Give the in-page API and file hierarchies the same tree connectors as the primary outline. */
|
|
1381
|
+
.api-hierarchy ul,
|
|
1382
|
+
.api-file-tree .api-file-tree {
|
|
1383
|
+
margin-left: .35rem;
|
|
1384
|
+
padding-left: 1.15rem;
|
|
1385
|
+
}
|
|
1386
|
+
.api-hierarchy ul > li,
|
|
1387
|
+
.api-file-tree .api-file-tree > li {
|
|
1388
|
+
position: relative;
|
|
1389
|
+
}
|
|
1390
|
+
.api-hierarchy ul > li::before,
|
|
1391
|
+
.api-file-tree .api-file-tree > li::before {
|
|
1392
|
+
border-left: 1px solid var(--api-border);
|
|
1393
|
+
bottom: -.18rem;
|
|
1394
|
+
content: "";
|
|
1395
|
+
left: -1.15rem;
|
|
1396
|
+
position: absolute;
|
|
1397
|
+
top: -.18rem;
|
|
1398
|
+
}
|
|
1399
|
+
.api-hierarchy ul > li::after,
|
|
1400
|
+
.api-file-tree .api-file-tree > li::after {
|
|
1401
|
+
border-top: 1px solid var(--api-border);
|
|
1402
|
+
content: "";
|
|
1403
|
+
left: -1.15rem;
|
|
1404
|
+
position: absolute;
|
|
1405
|
+
top: .72rem;
|
|
1406
|
+
width: .9rem;
|
|
1407
|
+
}
|
|
1408
|
+
.api-hierarchy ul > li:last-child::before,
|
|
1409
|
+
.api-file-tree .api-file-tree > li:last-child::before {
|
|
1410
|
+
bottom: auto;
|
|
1411
|
+
height: .9rem;
|
|
1412
|
+
}
|
|
1413
|
+
.api-hierarchy .api-kind { margin-right: .35rem; }
|
|
1414
|
+
.api-file-tree .api-kind { margin-right: .35rem; }
|
|
1415
|
+
.api-entity-card, .api-signature-card { border: 1px solid var(--api-border); border-top: .22rem solid var(--api-accent); border-radius: .45rem; padding: .7rem .85rem .8rem; margin: .55rem 0 .9rem; }
|
|
1416
|
+
.api-signature { margin: 0; padding: 0 0 .55rem; background: transparent; border-bottom: 1px solid var(--api-border); overflow-x: auto; font-size: .87rem; line-height: 1.35; font-weight: 650; }
|
|
1417
|
+
.api-signature code { padding: 0; }
|
|
1418
|
+
.api-signature-name { font-weight: 800; }
|
|
1419
|
+
.api-meta-row { display: flex; align-items: baseline; gap: .65rem; margin-top: .42rem; font-size: .78rem; }
|
|
1420
|
+
.api-meta-row > strong { flex: 0 0 5rem; }
|
|
1421
|
+
.api-badges { display: flex; flex-wrap: wrap; gap: .3rem; }
|
|
1422
|
+
.api-badges > span { border: 1px solid color-mix(in srgb, var(--api-accent) 32%, var(--api-border)); border-radius: 999px; background: color-mix(in srgb, var(--api-accent) 8%, transparent); padding: .08rem .36rem; font: 650 .72rem var(--api-code); }
|
|
1423
|
+
.api-description { border-top: 1px solid var(--api-border); margin-top: .55rem; padding-top: .45rem; }
|
|
1424
|
+
.api-related { margin-top: .45rem !important; }
|
|
1425
|
+
.api-availability { display: flex; align-items: baseline; gap: .7rem; border-top: 1px solid var(--api-border); margin-top: .55rem; padding-top: .48rem; font-size: .82rem; }
|
|
1426
|
+
.api-availability > strong { flex: 0 0 5rem; }
|
|
1427
|
+
.api-record-members { border-top: 1px solid var(--api-border); }
|
|
1428
|
+
.api-member-row { display: grid; grid-template-columns: 1.5rem minmax(0, 1fr); align-items: baseline; gap: .55rem; border-bottom: 1px solid var(--api-border); padding: .34rem .08rem; }
|
|
1429
|
+
.api-member-row code { font-weight: 600; }
|
|
1430
|
+
.api-member-doc { grid-column: 2; color: var(--api-muted); }
|
|
1431
|
+
.api-namespace-doc { margin: .4rem 0 .8rem; }
|
|
1432
|
+
table { width: 100%; border-collapse: collapse; margin: .4rem 0 .8rem; }
|
|
1433
|
+
th, td { text-align: left; padding: .45rem .55rem; border-bottom: 1px solid var(--api-border); }
|
|
1434
|
+
thead th { border-bottom: 2px solid var(--api-accent); }
|
|
1435
|
+
tbody tr:nth-child(odd) { background: var(--api-soft); }
|
|
1436
|
+
.table-scroll { overflow-x: auto; }
|
|
1437
|
+
.api-section-intro { color: var(--api-muted); max-width: 70rem; }
|
|
1438
|
+
.api-variant-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: .75rem; margin: .7rem 0 1rem; }
|
|
1439
|
+
.api-variant-card { border: 1px solid var(--api-border); border-top: .2rem solid var(--api-accent-2); border-radius: .45rem; background: color-mix(in srgb, var(--api-soft) 55%, transparent); overflow: hidden; }
|
|
1440
|
+
.api-variant-card header { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; padding: .62rem .75rem .5rem; border-bottom: 1px solid var(--api-border); }
|
|
1441
|
+
.api-variant-card h3 { margin: 0; font-size: 1rem; }
|
|
1442
|
+
.api-variant-card-label { color: var(--api-muted); font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; }
|
|
1443
|
+
.api-variant-card dl { margin: 0; padding: .2rem .75rem .65rem; }
|
|
1444
|
+
.api-variant-card dl > div { padding: .45rem 0; }
|
|
1445
|
+
.api-variant-card dl > div + div { border-top: 1px solid var(--api-border); }
|
|
1446
|
+
.api-variant-card dt { font-size: .76rem; font-weight: 700; color: var(--api-muted); margin-bottom: .22rem; }
|
|
1447
|
+
.api-variant-card dd { margin: 0; display: flex; flex-wrap: wrap; gap: .28rem; }
|
|
1448
|
+
.api-chip { display: inline-block; border: 1px solid var(--api-border); border-radius: .25rem; background: var(--api-bg); padding: .08rem .32rem; }
|
|
1449
|
+
.api-none { color: var(--api-muted); font-style: italic; }
|
|
1450
|
+
.api-source { background: var(--api-soft); border: 1px solid var(--api-border); border-radius: .4rem; padding: .75rem 0; overflow: auto; font-size: .82rem; line-height: 1.35; }
|
|
1451
|
+
.source-line { display: grid; grid-template-columns: 4.2rem minmax(max-content, 1fr); min-height: 1.1rem; }
|
|
1452
|
+
.source-line:target { background: color-mix(in srgb, var(--api-accent) 12%, transparent); }
|
|
1453
|
+
.source-number { user-select: none; text-align: right; padding-right: 1rem; color: var(--api-muted); }
|
|
1454
|
+
.source-code { white-space: pre; padding-right: 1rem; }
|
|
1455
|
+
.cpdocs-syntax-keyword { color: #005cc5; font-weight: 650; }
|
|
1456
|
+
.cpdocs-syntax-comment { color: #6a737d; font-style: italic; }
|
|
1457
|
+
.cpdocs-syntax-string { color: #032f62; }
|
|
1458
|
+
.cpdocs-syntax-number { color: #005cc5; }
|
|
1459
|
+
.cpdocs-syntax-preprocessor { color: #6f42c1; }
|
|
1460
|
+
html[data-theme="dark"] .cpdocs-syntax-keyword { color: #79b8ff; }
|
|
1461
|
+
html[data-theme="dark"] .cpdocs-syntax-comment { color: #8b949e; }
|
|
1462
|
+
html[data-theme="dark"] .cpdocs-syntax-string { color: #9ecbff; }
|
|
1463
|
+
html[data-theme="dark"] .cpdocs-syntax-number { color: #79b8ff; }
|
|
1464
|
+
html[data-theme="dark"] .cpdocs-syntax-preprocessor { color: #d2a8ff; }
|
|
1465
|
+
.api-footer { min-height: 2rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; border-top: 1px solid var(--api-border); padding: .45rem 1rem; font-size: .72rem; color: var(--api-muted); }
|
|
1466
|
+
@media (max-width: 1400px) { :root { --api-sidebar-default: 19rem; --api-toc-default: 20rem; } .api-main { width: 88%; } }
|
|
1467
|
+
@media (max-width: 1100px) { :root { --api-sidebar-default: 18rem; } .api-shell { grid-template-columns: var(--api-sidebar-width, var(--api-sidebar-default)) minmax(0, 1fr); } .api-toc, .api-resizer-toc { display: none; } .api-main { width: 92%; } }
|
|
1468
|
+
@media (max-width: 760px) { .api-shell { display: block; } .api-resizer { display: none; } .api-sidebar { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--api-border); } .api-topbar-actions { gap: .35rem; } .api-search { min-width: 7rem; } .api-project-docs { padding: .38rem .5rem; } .api-project-docs .api-long { display: none; } .api-project-docs .api-short { display: inline; } .api-topbar { height: auto; min-height: 3rem; flex-wrap: wrap; row-gap: .4rem; padding: .45rem .9rem; } .api-brand { white-space: normal; } .api-main { width: auto; padding: 1rem; } .api-sidebar { padding: .5rem .8rem; } .api-sidebar h2 { display: none; } .api-outline-panel-toggle { display: flex; align-items: center; gap: .6rem; width: 100%; min-height: 2.4rem; padding: .35rem .6rem; border: 1px solid var(--api-border); border-radius: .35rem; background: var(--api-soft); color: var(--api-fg); font: inherit; font-weight: 650; cursor: pointer; text-align: left; } .api-outline-panel-toggle::after { content: "▾"; margin-left: auto; transition: transform .15s; } .api-outline-panel-toggle[aria-expanded="true"]::after { transform: rotate(180deg); } .api-outline-current { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--api-muted); font-weight: 500; } .js .api-sidebar:not(.is-open) .api-outline-root { display: none; } .api-sidebar.is-open .api-outline-root { max-height: 60vh; overflow: auto; margin-top: .5rem; } }
|
|
1469
|
+
"""
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
_JS = r"""
|
|
1473
|
+
(() => {
|
|
1474
|
+
const root = document.documentElement;
|
|
1475
|
+
|
|
1476
|
+
// Keep the sidebar where the reader left it; a full page load otherwise jumps it to the top.
|
|
1477
|
+
const sidebar = document.querySelector(".api-sidebar");
|
|
1478
|
+
const panelToggle = document.querySelector(".api-outline-panel-toggle");
|
|
1479
|
+
panelToggle?.addEventListener("click", () => {
|
|
1480
|
+
const open = sidebar?.classList.toggle("is-open") ?? false;
|
|
1481
|
+
panelToggle.setAttribute("aria-expanded", open ? "true" : "false");
|
|
1482
|
+
if (open) sidebar?.querySelector(".is-current")?.scrollIntoView({block: "nearest"});
|
|
1483
|
+
});
|
|
1484
|
+
if (sidebar) {
|
|
1485
|
+
try {
|
|
1486
|
+
const saved = sessionStorage.getItem("cpdocs-api-sidebar");
|
|
1487
|
+
if (saved !== null) sidebar.scrollTop = Number(saved);
|
|
1488
|
+
} catch {}
|
|
1489
|
+
const currentRow = sidebar.querySelector(".is-current");
|
|
1490
|
+
if (currentRow) {
|
|
1491
|
+
const box = sidebar.getBoundingClientRect();
|
|
1492
|
+
const row = currentRow.getBoundingClientRect();
|
|
1493
|
+
if (row.top < box.top || row.bottom > box.bottom) currentRow.scrollIntoView({block: "center"});
|
|
1494
|
+
}
|
|
1495
|
+
addEventListener("pagehide", () => {
|
|
1496
|
+
try { sessionStorage.setItem("cpdocs-api-sidebar", String(sidebar.scrollTop)); } catch {}
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
document.querySelector(".api-theme")?.addEventListener("click", () => {
|
|
1501
|
+
const next = root.dataset.theme === "dark" ? "light" : "dark";
|
|
1502
|
+
root.dataset.theme = next;
|
|
1503
|
+
localStorage.setItem("cpdocs-api-theme", next);
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
// Drag the border of the outline or of the page contents to resize it; the width is kept for
|
|
1507
|
+
// later visits. Arrow keys resize a focused handle, double-click restores the default.
|
|
1508
|
+
for (const handle of document.querySelectorAll(".api-resizer")) {
|
|
1509
|
+
const side = handle.dataset.resize;
|
|
1510
|
+
const panel = document.querySelector(side === "sidebar" ? ".api-sidebar" : ".api-toc");
|
|
1511
|
+
const shell = handle.closest(".api-shell");
|
|
1512
|
+
if (!panel || !shell) continue;
|
|
1513
|
+
const property = `--api-${side}-width`;
|
|
1514
|
+
const key = `cpdocs-api-${side}-width`;
|
|
1515
|
+
const clamp = value => Math.round(Math.min(Math.max(value, 160), window.innerWidth * 0.45));
|
|
1516
|
+
const apply = width => root.style.setProperty(property, `${width}px`);
|
|
1517
|
+
const save = () => {
|
|
1518
|
+
try { localStorage.setItem(key, String(Math.round(panel.getBoundingClientRect().width))); } catch {}
|
|
1519
|
+
};
|
|
1520
|
+
handle.addEventListener("pointerdown", event => {
|
|
1521
|
+
if (event.button !== 0) return;
|
|
1522
|
+
event.preventDefault();
|
|
1523
|
+
handle.setPointerCapture(event.pointerId);
|
|
1524
|
+
root.classList.add("is-resizing");
|
|
1525
|
+
const box = shell.getBoundingClientRect();
|
|
1526
|
+
const move = moving => apply(clamp(side === "sidebar" ? moving.clientX - box.left : box.right - moving.clientX));
|
|
1527
|
+
const stop = () => {
|
|
1528
|
+
handle.removeEventListener("pointermove", move);
|
|
1529
|
+
handle.removeEventListener("pointerup", stop);
|
|
1530
|
+
handle.removeEventListener("pointercancel", stop);
|
|
1531
|
+
root.classList.remove("is-resizing");
|
|
1532
|
+
save();
|
|
1533
|
+
};
|
|
1534
|
+
handle.addEventListener("pointermove", move);
|
|
1535
|
+
handle.addEventListener("pointerup", stop);
|
|
1536
|
+
handle.addEventListener("pointercancel", stop);
|
|
1537
|
+
});
|
|
1538
|
+
handle.addEventListener("keydown", event => {
|
|
1539
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
1540
|
+
event.preventDefault();
|
|
1541
|
+
const step = (event.shiftKey ? 64 : 16) * (event.key === "ArrowRight" ? 1 : -1);
|
|
1542
|
+
apply(clamp(panel.getBoundingClientRect().width + (side === "sidebar" ? step : -step)));
|
|
1543
|
+
save();
|
|
1544
|
+
});
|
|
1545
|
+
handle.addEventListener("dblclick", () => {
|
|
1546
|
+
root.style.removeProperty(property);
|
|
1547
|
+
try { localStorage.removeItem(key); } catch {}
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
for (const button of document.querySelectorAll(".api-outline-toggle")) {
|
|
1552
|
+
const row = button.closest(".api-outline-row");
|
|
1553
|
+
const children = row?.nextElementSibling;
|
|
1554
|
+
if (!children?.classList.contains("api-outline-children")) continue;
|
|
1555
|
+
button.addEventListener("click", () => {
|
|
1556
|
+
const expanded = button.getAttribute("aria-expanded") === "true";
|
|
1557
|
+
button.setAttribute("aria-expanded", expanded ? "false" : "true");
|
|
1558
|
+
children.hidden = expanded;
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
const version = document.querySelector(".api-version");
|
|
1563
|
+
const apiRoot = document.body.dataset.apiRoot || "../";
|
|
1564
|
+
const route = document.body.dataset.currentRoute || "";
|
|
1565
|
+
if (version) {
|
|
1566
|
+
fetch(apiRoot + "versions.json")
|
|
1567
|
+
.then(response => response.ok ? response.json() : null)
|
|
1568
|
+
.then(data => {
|
|
1569
|
+
if (!data?.versions) return;
|
|
1570
|
+
version.replaceChildren();
|
|
1571
|
+
for (const item of data.versions) {
|
|
1572
|
+
const option = document.createElement("option");
|
|
1573
|
+
option.value = item.name;
|
|
1574
|
+
option.textContent = item.name;
|
|
1575
|
+
option.selected = item.name === document.querySelector(".api-version")?.dataset.current;
|
|
1576
|
+
version.appendChild(option);
|
|
1577
|
+
}
|
|
1578
|
+
const current = location.pathname.split("/").filter(Boolean).at(-1);
|
|
1579
|
+
for (const option of version.options) {
|
|
1580
|
+
if (location.pathname.includes(`/${option.value}/`)) option.selected = true;
|
|
1581
|
+
}
|
|
1582
|
+
version.addEventListener("change", () => {
|
|
1583
|
+
location.href = apiRoot + version.value + "/" + route;
|
|
1584
|
+
});
|
|
1585
|
+
}).catch(() => {});
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const search = document.querySelector(".api-search input");
|
|
1589
|
+
const results = document.querySelector(".api-search-results");
|
|
1590
|
+
const versionRoot = document.body.dataset.versionRoot || "./";
|
|
1591
|
+
let index = null;
|
|
1592
|
+
function showResults(values) {
|
|
1593
|
+
if (!results) return;
|
|
1594
|
+
results.replaceChildren();
|
|
1595
|
+
for (const item of values.slice(0, 20)) {
|
|
1596
|
+
const link = document.createElement("a");
|
|
1597
|
+
link.href = versionRoot + item.url;
|
|
1598
|
+
link.innerHTML = `<strong>${item.label}</strong><br><small>${item.kind} · ${item.qualified}</small>`;
|
|
1599
|
+
results.appendChild(link);
|
|
1600
|
+
}
|
|
1601
|
+
results.hidden = values.length === 0;
|
|
1602
|
+
}
|
|
1603
|
+
search?.addEventListener("input", async () => {
|
|
1604
|
+
const query = search.value.trim().toLowerCase();
|
|
1605
|
+
if (!query) { if (results) results.hidden = true; return; }
|
|
1606
|
+
if (index === null) {
|
|
1607
|
+
try { index = await (await fetch(versionRoot + "search-index.json")).json(); }
|
|
1608
|
+
catch { index = []; }
|
|
1609
|
+
}
|
|
1610
|
+
const values = index.filter(item => `${item.label} ${item.qualified}`.toLowerCase().includes(query));
|
|
1611
|
+
showResults(values);
|
|
1612
|
+
});
|
|
1613
|
+
document.addEventListener("click", event => {
|
|
1614
|
+
if (!event.target.closest(".api-search") && results) results.hidden = true;
|
|
1615
|
+
});
|
|
1616
|
+
})();
|
|
1617
|
+
"""
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
@functools.cache
|
|
1621
|
+
def _asset_version(kind: str) -> str:
|
|
1622
|
+
text = _CSS if kind == "css" else _JS
|
|
1623
|
+
return hashlib.sha1(text.strip().encode("utf-8")).hexdigest()[:10]
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
def _write_page(
|
|
1627
|
+
*,
|
|
1628
|
+
output: Path,
|
|
1629
|
+
graph: ApiGraph,
|
|
1630
|
+
document: Path,
|
|
1631
|
+
body: str,
|
|
1632
|
+
toc: list[tuple[str, str, int]],
|
|
1633
|
+
current: ApiEntity | None,
|
|
1634
|
+
special: str,
|
|
1635
|
+
project_docs_url: str,
|
|
1636
|
+
copyright_text: str,
|
|
1637
|
+
) -> None:
|
|
1638
|
+
path = output / _output_file(document)
|
|
1639
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1640
|
+
path.write_text(
|
|
1641
|
+
_layout(
|
|
1642
|
+
graph=graph,
|
|
1643
|
+
document=document,
|
|
1644
|
+
body=body,
|
|
1645
|
+
toc=toc,
|
|
1646
|
+
current=current,
|
|
1647
|
+
special=special,
|
|
1648
|
+
project_docs_url=project_docs_url,
|
|
1649
|
+
copyright_text=copyright_text,
|
|
1650
|
+
),
|
|
1651
|
+
encoding="utf-8",
|
|
1652
|
+
)
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
def _redirect(output: Path, document: Path, target: str) -> None:
|
|
1656
|
+
path = output / _output_file(document)
|
|
1657
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1658
|
+
path.write_text(
|
|
1659
|
+
'<!doctype html><meta charset="utf-8"><meta http-equiv="refresh" content="0; url='
|
|
1660
|
+
+ html.escape(target, quote=True)
|
|
1661
|
+
+ '">'
|
|
1662
|
+
"<script>location.replace(" + json.dumps(target) + ")</script>",
|
|
1663
|
+
encoding="utf-8",
|
|
1664
|
+
)
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
def _search_index(graph: ApiGraph) -> list[dict[str, str]]:
|
|
1668
|
+
values: list[dict[str, str]] = []
|
|
1669
|
+
for entity in sorted(
|
|
1670
|
+
graph.entities.values(), key=lambda value: value.qualified_name.casefold()
|
|
1671
|
+
):
|
|
1672
|
+
target, anchor = _entity_target(entity, graph)
|
|
1673
|
+
url = _output_directory(target).as_posix().rstrip("/") + "/"
|
|
1674
|
+
if anchor:
|
|
1675
|
+
url += f"#{anchor}"
|
|
1676
|
+
values.append(
|
|
1677
|
+
{
|
|
1678
|
+
"label": entity.member_label,
|
|
1679
|
+
"qualified": entity.qualified_name,
|
|
1680
|
+
"kind": _kind_label(entity.kind),
|
|
1681
|
+
"url": url,
|
|
1682
|
+
}
|
|
1683
|
+
)
|
|
1684
|
+
return values
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def render_html_site(
|
|
1688
|
+
graph: ApiGraph,
|
|
1689
|
+
*,
|
|
1690
|
+
output_directory: Path,
|
|
1691
|
+
project_docs_url: str,
|
|
1692
|
+
copyright_text: str,
|
|
1693
|
+
introduction_file: Path | None = None,
|
|
1694
|
+
) -> None:
|
|
1695
|
+
"""Render one API version directly to a standalone HTML site."""
|
|
1696
|
+
|
|
1697
|
+
assign_documents(graph.entities.values())
|
|
1698
|
+
output = output_directory.resolve()
|
|
1699
|
+
shutil.rmtree(output, ignore_errors=True)
|
|
1700
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
1701
|
+
assets = output / "assets"
|
|
1702
|
+
assets.mkdir(parents=True, exist_ok=True)
|
|
1703
|
+
(assets / "cpdocs-api.css").write_text(_CSS.strip() + "\n", encoding="utf-8")
|
|
1704
|
+
(assets / "cpdocs-api.js").write_text(_JS.strip() + "\n", encoding="utf-8")
|
|
1705
|
+
|
|
1706
|
+
home_document = Path("index.md")
|
|
1707
|
+
introduction = None
|
|
1708
|
+
if introduction_file is not None and introduction_file.is_file():
|
|
1709
|
+
introduction = introduction_file.read_text(encoding="utf-8")
|
|
1710
|
+
body, toc = _home_page(graph, home_document, project_docs_url, introduction=introduction)
|
|
1711
|
+
_write_page(
|
|
1712
|
+
output=output,
|
|
1713
|
+
graph=graph,
|
|
1714
|
+
document=home_document,
|
|
1715
|
+
body=body,
|
|
1716
|
+
toc=toc,
|
|
1717
|
+
current=None,
|
|
1718
|
+
special="home",
|
|
1719
|
+
project_docs_url=project_docs_url,
|
|
1720
|
+
copyright_text=copyright_text,
|
|
1721
|
+
)
|
|
1722
|
+
|
|
1723
|
+
if graph.variants:
|
|
1724
|
+
variants_document = Path("api-variants.md")
|
|
1725
|
+
body, toc = _variant_page(graph, variants_document)
|
|
1726
|
+
_write_page(
|
|
1727
|
+
output=output,
|
|
1728
|
+
graph=graph,
|
|
1729
|
+
document=variants_document,
|
|
1730
|
+
body=body,
|
|
1731
|
+
toc=toc,
|
|
1732
|
+
current=None,
|
|
1733
|
+
special="variants",
|
|
1734
|
+
project_docs_url=project_docs_url,
|
|
1735
|
+
copyright_text=copyright_text,
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
if any(entity.kind == "macro" for entity in graph.entities.values()):
|
|
1739
|
+
macros_document = Path("macros.md")
|
|
1740
|
+
body, toc = _macros_page(graph, macros_document)
|
|
1741
|
+
_write_page(
|
|
1742
|
+
output=output,
|
|
1743
|
+
graph=graph,
|
|
1744
|
+
document=macros_document,
|
|
1745
|
+
body=body,
|
|
1746
|
+
toc=toc,
|
|
1747
|
+
current=None,
|
|
1748
|
+
special="macros",
|
|
1749
|
+
project_docs_url=project_docs_url,
|
|
1750
|
+
copyright_text=copyright_text,
|
|
1751
|
+
)
|
|
1752
|
+
|
|
1753
|
+
for entity in graph.entities.values():
|
|
1754
|
+
if _embedded_member_parent(entity, graph) is not None:
|
|
1755
|
+
continue
|
|
1756
|
+
document = entity_document(entity)
|
|
1757
|
+
body, toc = _entity_page(entity, graph, document)
|
|
1758
|
+
_write_page(
|
|
1759
|
+
output=output,
|
|
1760
|
+
graph=graph,
|
|
1761
|
+
document=document,
|
|
1762
|
+
body=body,
|
|
1763
|
+
toc=toc,
|
|
1764
|
+
current=entity,
|
|
1765
|
+
special="entity",
|
|
1766
|
+
project_docs_url=project_docs_url,
|
|
1767
|
+
copyright_text=copyright_text,
|
|
1768
|
+
)
|
|
1769
|
+
|
|
1770
|
+
for source_path, content in graph.sources.items():
|
|
1771
|
+
document = _source_document(source_path)
|
|
1772
|
+
body, toc = _source_page(source_path, content, document, graph)
|
|
1773
|
+
_write_page(
|
|
1774
|
+
output=output,
|
|
1775
|
+
graph=graph,
|
|
1776
|
+
document=document,
|
|
1777
|
+
body=body,
|
|
1778
|
+
toc=toc,
|
|
1779
|
+
current=None,
|
|
1780
|
+
special="source",
|
|
1781
|
+
project_docs_url=project_docs_url,
|
|
1782
|
+
copyright_text=copyright_text,
|
|
1783
|
+
)
|
|
1784
|
+
|
|
1785
|
+
symbols: dict[str, str] = {}
|
|
1786
|
+
for entity in graph.entities.values():
|
|
1787
|
+
target_document, anchor = _entity_target(entity, graph)
|
|
1788
|
+
target_url = _output_directory(target_document).as_posix().rstrip("/") + "/"
|
|
1789
|
+
if anchor:
|
|
1790
|
+
target_url += f"#{anchor}"
|
|
1791
|
+
symbols[entity.qualified_name] = target_url
|
|
1792
|
+
separator = "." if entity.language == "python" else "::"
|
|
1793
|
+
alias = Path("_symbols", *entity.qualified_name.split(separator), "index.md")
|
|
1794
|
+
redirect_target = _page_href(alias, target_document, anchor)
|
|
1795
|
+
_redirect(output, alias, redirect_target)
|
|
1796
|
+
|
|
1797
|
+
(output / "symbols.json").write_text(
|
|
1798
|
+
json.dumps(symbols, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
1799
|
+
)
|
|
1800
|
+
(output / "search-index.json").write_text(
|
|
1801
|
+
json.dumps(_search_index(graph), indent=2) + "\n", encoding="utf-8"
|
|
1802
|
+
)
|
|
1803
|
+
(output / ".nojekyll").touch()
|