Skip to content

Commit fb9c795

Browse files
committed
docs: replace the manual DOM reference with a generated one
This replaces the hand-written DOM reference with one generated from mrdocs-dom-schema.json. A new Antora extension walks the file's `$defs` in source order and emits one section per type.
1 parent a4a85e7 commit fb9c795

4 files changed

Lines changed: 172 additions & 830 deletions

File tree

docs/antora-playbook.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ asciidoc:
5454
- ./extensions/mrdocs-demos.js
5555
- ./extensions/mrdocs-releases.js
5656
- ./extensions/config-options-reference.js
57+
- ./extensions/dom-reference.js

docs/extensions/dom-reference.js

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com)
3+
4+
Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
7+
Official repository: https://github.com/cppalliance/mrdocs
8+
9+
Antora extension that renders the DOM Reference section of the
10+
docs site from mrdocs-dom-schema.json. Drop-in counterpart to
11+
config-options-reference.js for the JSON-Schema describing the
12+
Handlebars DOM.
13+
*/
14+
15+
function escapeHtml(str)
16+
{
17+
return str
18+
.replace(/&/g, "&")
19+
.replace(/</g, "&lt;")
20+
.replace(/>/g, "&gt;")
21+
.replace(/"/g, "&quot;")
22+
.replace(/'/g, "&#039;");
23+
}
24+
25+
// Convert a CamelCase / PascalCase type name into a kebab-case
26+
// anchor id. Adjacent uppercase letters that look like an acronym
27+
// (e.g. "TParam", "TArg") stay glued together - the existing
28+
// manually-maintained docs use `tparam-fields` and `targ-fields`,
29+
// not `t-param-fields` / `t-arg-fields`, and we preserve that.
30+
function kebabAnchor(name)
31+
{
32+
return name
33+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
34+
.toLowerCase();
35+
}
36+
37+
function anchorFor(typeName)
38+
{
39+
return kebabAnchor(typeName) + '-fields';
40+
}
41+
42+
// Strip "#/$defs/X" -> "X".
43+
function refTypeName(ref)
44+
{
45+
const prefix = '#/$defs/';
46+
return ref.startsWith(prefix) ? ref.substring(prefix.length) : ref;
47+
}
48+
49+
// Render the type cell of a property. Returns HTML.
50+
function describeType(prop)
51+
{
52+
if (prop.$ref) {
53+
const t = refTypeName(prop.$ref);
54+
return `<a href="#${anchorFor(t)}"><code>${t}</code></a>`;
55+
}
56+
if (prop.const !== undefined) {
57+
return `<code>"${escapeHtml(String(prop.const))}"</code>`;
58+
}
59+
if (prop.type === 'array') {
60+
return `array of ${describeType(prop.items)}`;
61+
}
62+
if (prop.type === 'string' && Array.isArray(prop.enum)) {
63+
const values = prop.enum.map(v =>
64+
`<code>"${escapeHtml(v)}"</code>`).join(' &#124; ');
65+
return `string (${values})`;
66+
}
67+
if (prop.type === 'object') {
68+
return 'object';
69+
}
70+
return prop.type || 'any';
71+
}
72+
73+
// Render one `$defs` entry: heading + description + members table
74+
// (or a `oneOf` list, for polymorphic unions).
75+
function renderTypeSection(typeName, schema, level, block)
76+
{
77+
block.lines.push(
78+
`<div class="sect${level - 1}">`);
79+
block.lines.push(
80+
`<h${level} id="${anchorFor(typeName)}">`);
81+
block.lines.push(
82+
`<a class="anchor" href="#${anchorFor(typeName)}"></a>`);
83+
block.lines.push(escapeHtml(typeName));
84+
block.lines.push(`</h${level}>`);
85+
block.lines.push(`<div class="sectionbody">`);
86+
87+
if (schema.description) {
88+
block.lines.push(
89+
`<div class="paragraph"><p>${escapeHtml(schema.description)}</p></div>`);
90+
}
91+
92+
if (Array.isArray(schema.oneOf)) {
93+
// Polymorphic union: list each variant as a link.
94+
block.lines.push(`<div class="paragraph"><p>One of:</p></div>`);
95+
block.lines.push(`<div class="ulist"><ul>`);
96+
for (const variant of schema.oneOf) {
97+
const name = refTypeName(variant.$ref);
98+
block.lines.push(
99+
`<li><a href="#${anchorFor(name)}"><code>${escapeHtml(name)}</code></a></li>`);
100+
}
101+
block.lines.push(`</ul></div>`);
102+
} else if (schema.properties) {
103+
// Object type: render the property table.
104+
const required = new Set(schema.required || []);
105+
block.lines.push(
106+
`<table class="tableblock frame-all grid-all stretch">`);
107+
block.lines.push(`<colgroup>`);
108+
block.lines.push(`<col style="width: 25%;">`);
109+
block.lines.push(`<col style="width: 25%;">`);
110+
block.lines.push(`<col style="width: 50%;">`);
111+
block.lines.push(`</colgroup>`);
112+
block.lines.push(`<thead><tr>`);
113+
block.lines.push(
114+
`<th class="tableblock halign-left valign-top">Property</th>`);
115+
block.lines.push(
116+
`<th class="tableblock halign-left valign-top">Type</th>`);
117+
block.lines.push(
118+
`<th class="tableblock halign-left valign-top">Description</th>`);
119+
block.lines.push(`</tr></thead>`);
120+
block.lines.push(`<tbody>`);
121+
for (const [name, prop] of Object.entries(schema.properties)) {
122+
block.lines.push(`<tr>`);
123+
block.lines.push(
124+
`<td class="tableblock halign-left valign-top">`
125+
+ `<code>${escapeHtml(name)}</code>`
126+
+ (required.has(name)
127+
? ` <span style="color: orangered;">(required)</span>`
128+
: '')
129+
+ `</td>`);
130+
block.lines.push(
131+
`<td class="tableblock halign-left valign-top">${describeType(prop)}</td>`);
132+
block.lines.push(
133+
`<td class="tableblock halign-left valign-top">`
134+
+ (prop.description ? escapeHtml(prop.description) : '')
135+
+ `</td>`);
136+
block.lines.push(`</tr>`);
137+
}
138+
block.lines.push(`</tbody>`);
139+
block.lines.push(`</table>`);
140+
}
141+
142+
block.lines.push(`</div>`); // sectionbody
143+
block.lines.push(`</div>`); // sect
144+
}
145+
146+
module.exports = function (registry) {
147+
if (!registry) {
148+
throw new Error('registry must be defined');
149+
}
150+
registry.block('dom-reference', function () {
151+
const self = this;
152+
self.onContext('example');
153+
self.process((parent, reader, attrs) => {
154+
const level = parseInt(attrs.level || 3, 10);
155+
const code = reader.getLines().join('\n');
156+
const schema = JSON.parse(code);
157+
const block = self.$create_pass_block(parent, '', Opal.hash(attrs));
158+
const defs = schema['$defs'] || {};
159+
for (const [typeName, def] of Object.entries(defs)) {
160+
renderTypeSection(typeName, def, level, block);
161+
}
162+
return block;
163+
});
164+
});
165+
};

0 commit comments

Comments
 (0)