Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a638f6d
feat: auto-generate a JSON Schema for DOM output (--schemas option)
gennaroprota Apr 16, 2026
b715b79
fix: serialize OperatorKind as a string in the DOM
gennaroprota Apr 16, 2026
9f8174c
feat: have --schemas also generate an XML schema
gennaroprota Apr 16, 2026
5a5f43e
refactor: describe AccessKind, ConstexprKind, ParamDirection, Storage…
gennaroprota Apr 16, 2026
b5d3db3
build: replace the hand-written mrdocs.rnc with the one generated via…
gennaroprota Apr 16, 2026
112f9e1
feat(schemas): describe DOM types and members in the JSON schema
gennaroprota May 1, 2026
35b5d2f
refactor: use dom::JSON::stringify instead of a duplicate emitter
gennaroprota May 4, 2026
1cf59f8
refactor: rename buildDomSchema to buildDomJsonSchema
gennaroprota May 4, 2026
d4f7446
refactor: move the schema headers out of the public API
gennaroprota May 4, 2026
4e609e7
fix: missing template args, noexcept, and explicit specifiers in XML
gennaroprota May 4, 2026
e740437
refactor: extract a generic XML emitter in src/lib/Support/Xml/
gennaroprota May 4, 2026
a846a63
feat: have --schemas emit a .rng, not a .rnc schema file
gennaroprota May 4, 2026
a6a12c7
build: drop the Java prerequisite
gennaroprota May 4, 2026
f01c3a1
fix: missing description for DOM type or member goes undetected
gennaroprota May 4, 2026
7fa789f
docs: add documentation for the --schemas option
gennaroprota May 4, 2026
5f3b63d
build: commit mrdocs.rng and mrdocs-dom-schema.json, with two verific…
gennaroprota May 5, 2026
6ce122b
docs: replace the manual DOM reference with a generated one
gennaroprota May 5, 2026
019d019
refactor: drop Symbol's isXxx booleans
gennaroprota May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
test-files/**/*.xml binary
test-files/golden-tests/** text eol=lf
**.sh text eol=lf
docs/mrdocs.rng text eol=lf
docs/mrdocs-dom-schema.json text eol=lf
39 changes: 26 additions & 13 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -553,24 +553,37 @@ if (MRDOCS_BUILD_TESTS)
)
endif()

#-------------------------------------------------
# Schemas
#-------------------------------------------------
# Generate mrdocs.rng and mrdocs-dom-schema.json via --schemas.
add_custom_command(
COMMAND mrdocs --schemas=${CMAKE_CURRENT_BINARY_DIR}
OUTPUT
${CMAKE_CURRENT_BINARY_DIR}/mrdocs.rng
${CMAKE_CURRENT_BINARY_DIR}/mrdocs-dom-schema.json
DEPENDS mrdocs
COMMENT "Generating schemas via --schemas")
add_custom_target(mrdocs_schemas ALL DEPENDS
${CMAKE_CURRENT_BINARY_DIR}/mrdocs.rng
${CMAKE_CURRENT_BINARY_DIR}/mrdocs-dom-schema.json)

# Freshness checks: the regenerated schemas must match the
# copies checked in under docs/.
add_test(NAME rng-schema-check
COMMAND ${CMAKE_COMMAND} -E compare_files
${CMAKE_CURRENT_BINARY_DIR}/mrdocs.rng
${CMAKE_CURRENT_SOURCE_DIR}/docs/mrdocs.rng)
add_test(NAME dom-schema-check
COMMAND ${CMAKE_COMMAND} -E compare_files
${CMAKE_CURRENT_BINARY_DIR}/mrdocs-dom-schema.json
${CMAKE_CURRENT_SOURCE_DIR}/docs/mrdocs-dom-schema.json)

#-------------------------------------------------
# XML lint
#-------------------------------------------------
find_package(LibXml2 ${REQUIRED_IF_STRICT})
if (LibXml2_FOUND)
find_package(Java REQUIRED Runtime)
# FindJava
if (NOT Java_FOUND)
message(FATAL_ERROR "Java is needed to run xml-lint")
endif()

add_custom_command(
COMMAND ${Java_JAVA_EXECUTABLE} -jar ${CMAKE_CURRENT_SOURCE_DIR}/util/trang.jar
${CMAKE_CURRENT_SOURCE_DIR}/mrdocs.rnc ${CMAKE_CURRENT_BINARY_DIR}/mrdocs.rng
OUTPUT mrdocs.rng
DEPENDS mrdocs.rnc)
add_custom_target(mrdocs_rng ALL DEPENDS mrdocs.rng)

file(GLOB_RECURSE XML_SOURCES CONFIGURE_DEPENDS test-files/golden-tests/*.xml)
add_test(NAME xml-lint
COMMAND ${LIBXML2_XMLLINT_EXECUTABLE} --dropdtd --noout
Expand Down
1 change: 1 addition & 0 deletions docs/antora-playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ asciidoc:
- ./extensions/mrdocs-demos.js
- ./extensions/mrdocs-releases.js
- ./extensions/config-options-reference.js
- ./extensions/dom-reference.js
165 changes: 165 additions & 0 deletions docs/extensions/dom-reference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
Copyright (c) 2026 Gennaro Prota (gennaro.prota@gmail.com)

Distributed under the Boost Software License, Version 1.0. (See accompanying
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

Official repository: https://github.com/cppalliance/mrdocs

Antora extension that renders the DOM Reference section of the
docs site from mrdocs-dom-schema.json. Drop-in counterpart to
config-options-reference.js for the JSON-Schema describing the
Handlebars DOM.
*/

function escapeHtml(str)
{
return str
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}

// Convert a CamelCase / PascalCase type name into a kebab-case
// anchor id. Adjacent uppercase letters that look like an acronym
// (e.g. "TParam", "TArg") stay glued together - the existing
// manually-maintained docs use `tparam-fields` and `targ-fields`,
// not `t-param-fields` / `t-arg-fields`, and we preserve that.
function kebabAnchor(name)
{
return name
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.toLowerCase();
}

function anchorFor(typeName)
{
return kebabAnchor(typeName) + '-fields';
}

// Strip "#/$defs/X" -> "X".
function refTypeName(ref)
{
const prefix = '#/$defs/';
return ref.startsWith(prefix) ? ref.substring(prefix.length) : ref;
}

// Render the type cell of a property. Returns HTML.
function describeType(prop)
{
if (prop.$ref) {
const t = refTypeName(prop.$ref);
return `<a href="#${anchorFor(t)}"><code>${t}</code></a>`;
}
if (prop.const !== undefined) {
return `<code>"${escapeHtml(String(prop.const))}"</code>`;
}
if (prop.type === 'array') {
return `array of ${describeType(prop.items)}`;
}
if (prop.type === 'string' && Array.isArray(prop.enum)) {
const values = prop.enum.map(v =>
`<code>"${escapeHtml(v)}"</code>`).join(' &#124; ');
return `string (${values})`;
}
if (prop.type === 'object') {
return 'object';
}
return prop.type || 'any';
}

// Render one `$defs` entry: heading + description + members table
// (or a `oneOf` list, for polymorphic unions).
function renderTypeSection(typeName, schema, level, block)
{
block.lines.push(
`<div class="sect${level - 1}">`);
block.lines.push(
`<h${level} id="${anchorFor(typeName)}">`);
block.lines.push(
`<a class="anchor" href="#${anchorFor(typeName)}"></a>`);
block.lines.push(escapeHtml(typeName));
block.lines.push(`</h${level}>`);
block.lines.push(`<div class="sectionbody">`);

if (schema.description) {
block.lines.push(
`<div class="paragraph"><p>${escapeHtml(schema.description)}</p></div>`);
}

if (Array.isArray(schema.oneOf)) {
// Polymorphic union: list each variant as a link.
block.lines.push(`<div class="paragraph"><p>One of:</p></div>`);
block.lines.push(`<div class="ulist"><ul>`);
for (const variant of schema.oneOf) {
const name = refTypeName(variant.$ref);
block.lines.push(
`<li><a href="#${anchorFor(name)}"><code>${escapeHtml(name)}</code></a></li>`);
}
block.lines.push(`</ul></div>`);
} else if (schema.properties) {
// Object type: render the property table.
const required = new Set(schema.required || []);
block.lines.push(
`<table class="tableblock frame-all grid-all stretch">`);
block.lines.push(`<colgroup>`);
block.lines.push(`<col style="width: 25%;">`);
block.lines.push(`<col style="width: 25%;">`);
block.lines.push(`<col style="width: 50%;">`);
block.lines.push(`</colgroup>`);
block.lines.push(`<thead><tr>`);
block.lines.push(
`<th class="tableblock halign-left valign-top">Property</th>`);
block.lines.push(
`<th class="tableblock halign-left valign-top">Type</th>`);
block.lines.push(
`<th class="tableblock halign-left valign-top">Description</th>`);
block.lines.push(`</tr></thead>`);
block.lines.push(`<tbody>`);
for (const [name, prop] of Object.entries(schema.properties)) {
block.lines.push(`<tr>`);
block.lines.push(
`<td class="tableblock halign-left valign-top">`
+ `<code>${escapeHtml(name)}</code>`
+ (required.has(name)
? ` <span style="color: orangered;">(required)</span>`
: '')
+ `</td>`);
block.lines.push(
`<td class="tableblock halign-left valign-top">${describeType(prop)}</td>`);
block.lines.push(
`<td class="tableblock halign-left valign-top">`
+ (prop.description ? escapeHtml(prop.description) : '')
+ `</td>`);
block.lines.push(`</tr>`);
}
block.lines.push(`</tbody>`);
block.lines.push(`</table>`);
}

block.lines.push(`</div>`); // sectionbody
block.lines.push(`</div>`); // sect
}

module.exports = function (registry) {
if (!registry) {
throw new Error('registry must be defined');
}
registry.block('dom-reference', function () {
const self = this;
self.onContext('example');
self.process((parent, reader, attrs) => {
const level = parseInt(attrs.level || 3, 10);
const code = reader.getLines().join('\n');
const schema = JSON.parse(code);
const block = self.$create_pass_block(parent, '', Opal.hash(attrs));
const defs = schema['$defs'] || {};
for (const [typeName, def] of Object.entries(defs)) {
renderTypeSection(typeName, def, level, block);
}
return block;
});
});
};
1 change: 1 addition & 0 deletions docs/modules/ROOT/attachments/mrdocs-dom-schema.json
1 change: 1 addition & 0 deletions docs/modules/ROOT/attachments/mrdocs.rng
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* xref:config-file.adoc[]
* xref:commands.adoc[Documenting the Code]
* xref:generators.adoc[]
* xref:schemas.adoc[Output Schemas]
* xref:design-notes.adoc[]
* xref:reference:index.adoc[Library Reference]
* Contribute
Expand Down
Loading
Loading