Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
42fb572
feat: auto-generate a JSON Schema for DOM output (--schemas option)
gennaroprota Apr 16, 2026
14ecba9
fix: serialize OperatorKind as a string in the DOM
gennaroprota Apr 16, 2026
142dfc6
feat: have --schemas also generate an XML schema
gennaroprota Apr 16, 2026
566df75
refactor: describe AccessKind, ConstexprKind, ParamDirection, Storage…
gennaroprota Apr 16, 2026
2868ac4
build: replace the hand-written mrdocs.rnc with the one generated via…
gennaroprota Apr 16, 2026
2f1c7eb
feat(schemas): describe DOM types and members in the JSON schema
gennaroprota May 1, 2026
96da0a5
refactor: use dom::JSON::stringify instead of a duplicate emitter
gennaroprota May 4, 2026
3490604
refactor: rename buildDomSchema to buildDomJsonSchema
gennaroprota May 4, 2026
fd19b4e
refactor: move the schema headers out of the public API
gennaroprota May 4, 2026
2354117
fix: missing template args, noexcept, and explicit specifiers in XML …
gennaroprota May 4, 2026
079f863
refactor: extract a generic XML emitter in src/lib/Support/Xml/
gennaroprota May 4, 2026
c2cc495
feat: have --schemas emit a .rng, not a .rnc schema file
gennaroprota May 4, 2026
fd29567
build: drop the Java prerequisite
gennaroprota May 4, 2026
3139086
fix: assert when a DOM type or member lacks a description
gennaroprota May 4, 2026
3265157
docs: add documentation for the --schemas option
gennaroprota May 4, 2026
297857e
build: commit mrdocs.rng and mrdocs-dom-schema.json, with two verific…
gennaroprota May 5, 2026
f9d5f63
docs: replace the manual DOM reference with a generated one
gennaroprota May 5, 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