Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 82 additions & 76 deletions builds/respec-w3c.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion builds/respec-w3c.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions examples/basic.built.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ <h2>WebDriver BiDi Extension for Foo</h2>
<section>
<h3>The <code>foo.doTheFoo</code> Command</h3>
<p>
The <dfn cddl-type>FooDoTheFooCommand</dfn> invokes {{Foo/doTheFoo()}}
on the remote end. The {^FooDoTheFooParameters/bar^} parameter maps to
the {{Foo}}'s {{Foo/bar}} attribute.
The {^FooDoTheFooCommand^} invokes {{Foo/doTheFoo()}} on the remote
end. The {^FooDoTheFooParameters/bar^} parameter maps to the {{Foo}}'s
{{Foo/bar}} attribute.
</p>
<pre class="cddl">
FooDoTheFooCommand = {
Expand Down
6 changes: 3 additions & 3 deletions examples/basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ <h2>WebDriver BiDi Extension for Foo</h2>
<section>
<h3>The <code>foo.doTheFoo</code> Command</h3>
<p>
The <dfn cddl-type>FooDoTheFooCommand</dfn> invokes {{Foo/doTheFoo()}}
on the remote end. The {^FooDoTheFooParameters/bar^} parameter maps to
the {{Foo}}'s {{Foo/bar}} attribute.
The {^FooDoTheFooCommand^} invokes {{Foo/doTheFoo()}} on the remote
end. The {^FooDoTheFooParameters/bar^} parameter maps to the {{Foo}}'s
{{Foo/bar}} attribute.
</p>
<pre class="cddl">
FooDoTheFooCommand = {
Expand Down
182 changes: 142 additions & 40 deletions src/core/cddl.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function isMemberKey(node) {
/**
* @typedef {object} CddlState
* @property {Map<string, {type: string, for: string|null, id: string}>} definitions
* @property {Set<string>} proseDfns - IDs of prose-level dfns already created
* @property {Map<string, string>} proseDfns - generated CDDL IDs mapped to prose-level dfn IDs
* @property {Map<string, Set<string>>} genericParams - rule to param names
*/

Expand Down Expand Up @@ -244,19 +244,30 @@ class ReSpecCDDLMarker extends CddlMarker {

// Check for duplicate
if (state.definitions.has(key)) {
return html`<a href="#${id}" class="cddl-name" data-link-type="cddl-type"
const def = state.definitions.get(key);
if (!def) {
return html`<span class="cddl-name">${xmlEscape(name)}</span>`;
}
return html`<a
href="#${def.id}"
class="cddl-name"
data-link-type="cddl-type"
>${xmlEscape(name)}</a
>`;
}

// Check if prose dfn already exists
if (state.proseDfns.has(id)) {
const proseId = state.proseDfns.get(id);
if (proseId) {
state.definitions.set(key, {
type: "cddl-type",
for: null,
id,
id: proseId,
});
return html`<a href="#${id}" class="cddl-name" data-link-type="cddl-type"
return html`<a
href="#${proseId}"
class="cddl-name"
data-link-type="cddl-type"
>${xmlEscape(name)}</a
>`;
}
Expand Down Expand Up @@ -284,19 +295,28 @@ class ReSpecCDDLMarker extends CddlMarker {
const key = `cddl-key:${forType}/${name}`;

if (state.definitions.has(key)) {
const def = state.definitions.get(key);
if (!def) {
return html`<span class="cddl-name">${xmlEscape(name)}</span>`;
}
return html`<a
href="#${id}"
href="#${def.id}"
class="cddl-name"
data-link-type="cddl-key"
data-xref-for="${xmlEscape(forType)}"
>${xmlEscape(name)}</a
>`;
}

if (state.proseDfns.has(id)) {
state.definitions.set(key, { type: "cddl-key", for: forType, id });
const proseId = state.proseDfns.get(id);
if (proseId) {
state.definitions.set(key, {
type: "cddl-key",
for: forType,
id: proseId,
});
return html`<a
href="#${id}"
href="#${proseId}"
class="cddl-name"
data-link-type="cddl-key"
data-xref-for="${xmlEscape(forType)}"
Expand Down Expand Up @@ -387,10 +407,15 @@ class ReSpecCDDLMarker extends CddlMarker {
>`;
}

if (state.proseDfns.has(id)) {
state.definitions.set(key, { type: "cddl-value", for: forType, id });
const proseId = state.proseDfns.get(id);
if (proseId) {
state.definitions.set(key, {
type: "cddl-value",
for: forType,
id: proseId,
});
return html`<a
href="#${id}"
href="#${proseId}"
class="cddl-str"
data-link-type="cddl-value"
data-xref-for="${xmlEscape(forType)}"
Expand Down Expand Up @@ -533,41 +558,115 @@ class ReSpecCDDLMarker extends CddlMarker {

/**
* Normalize prose-level CDDL dfn elements.
* Converts shorthand attributes (cddl-type, cddl-key, cddl-value) to
* data-dfn-type/data-dfn-for attributes.
* Registers prose dfns that already use conforming data-dfn-type attributes.
*
* @param {Document} doc
* @param {Set<string>} proseDfns - set of dfn IDs to populate
* @param {Map<string, string>} proseDfns - generated CDDL IDs mapped to dfn IDs
*/
function normalizeProseDfns(doc, proseDfns) {
/** @type {NodeListOf<HTMLElement>} */
const dfns = doc.querySelectorAll(
"dfn[cddl-type], dfn[cddl-key], dfn[cddl-value]"
[
"dfn[data-dfn-type='cddl-type']",
"dfn[data-dfn-type='cddl-key']",
"dfn[data-dfn-type='cddl-value']",
].join(", ")
);
dfns.forEach(dfn => {
const attr = /** @type {string} */ (
["cddl-type", "cddl-key", "cddl-value"].find(a => dfn.hasAttribute(a))
);
dfn.dataset.dfnType = attr;
dfn.removeAttribute(attr);
const id = getCddlGeneratedId(dfn);
if (!id) return;
dfn.id ||= id;
proseDfns.set(id, dfn.id);
});
}

const forValue = dfn.getAttribute("for");
if (forValue) {
dfn.dataset.dfnFor = forValue;
dfn.removeAttribute("for");
}
/**
* Prefer prose-level CDDL dfns over generated block dfns when both exist.
* @param {Document} doc
* @param {Map<string, {type: string, for: string|null, id: string}>} definitions
*/
function linkGeneratedDfnsToProseDfns(doc, definitions) {
/** @type {Map<string, string>} */
const proseIds = new Map();
/** @type {NodeListOf<HTMLElement>} */
const proseDfns = doc.querySelectorAll(
[
"dfn[data-dfn-type='cddl-type']",
"dfn[data-dfn-type='cddl-key']",
"dfn[data-dfn-type='cddl-value']",
].join(", ")
);

const name = dfn.textContent.trim();
const forPart = forValue ? `${sanitizeId(forValue)}-` : "";
const typePart = attr.replace("cddl-", "");
const id = `cddl-${typePart}-${forPart}${sanitizeId(name)}`;
dfn.id ||= id;
proseDfns.add(dfn.id);
proseDfns.forEach(dfn => {
if (dfn.closest("pre.cddl")) return;
const id = getCddlGeneratedId(dfn);
if (id) dfn.id ||= id;
const key = getCddlDefinitionKey(dfn);
if (key) proseIds.set(key, dfn.id);
});

registerDefinition(dfn, [name]);
/** @type {NodeListOf<HTMLElement>} */
const blockDfns = doc.querySelectorAll(
"pre.cddl dfn[data-dfn-type^='cddl-']"
);
blockDfns.forEach(dfn => {
const key = getCddlDefinitionKey(dfn);
const proseId = key ? proseIds.get(key) : null;
if (!key || !proseId) return;

const link = doc.createElement("a");
link.href = `#${proseId}`;
link.className = dfn.className;
link.dataset.linkType = dfn.dataset.dfnType || "";
if (dfn.dataset.dfnFor) link.dataset.xrefFor = dfn.dataset.dfnFor;
link.textContent = dfn.textContent;
dfn.replaceWith(link);

const def = definitions.get(key);
if (def) def.id = proseId;
});
}

/**
* @param {HTMLElement} dfn
* @returns {string|null}
*/
function getCddlDefinitionKey(dfn) {
const type = dfn.dataset.dfnType;
const text = dfn.textContent.trim();
const forValue = dfn.dataset.dfnFor || "";
switch (type) {
case "cddl-type":
return `cddl-type:${text}`;
case "cddl-key":
return forValue ? `cddl-key:${forValue}/${text}` : null;
case "cddl-value":
return forValue ? `cddl-value:${forValue}/${text}` : null;
default:
return null;
}
}

/**
* @param {HTMLElement} dfn
* @returns {string|null}
*/
function getCddlGeneratedId(dfn) {
const dfnType = dfn.dataset.dfnType;
if (
dfnType !== "cddl-type" &&
dfnType !== "cddl-key" &&
dfnType !== "cddl-value"
) {
return null;
}
const forValue = dfn.dataset.dfnFor;
const name = dfn.textContent.trim();
const forPart = forValue ? `${sanitizeId(forValue)}-` : "";
const typePart = dfnType.replace("cddl-", "");
return `cddl-${typePart}-${forPart}${sanitizeId(name)}`;
}

/**
* Resolve pending references after all blocks are processed.
* Nodes with data-cddl-pending that now have matching definitions
Expand Down Expand Up @@ -650,7 +749,7 @@ function resolveInlineCddlRefs(doc, definitions) {
function registerCddlDfns(doc, definitions) {
definitions.forEach(def => {
const dfn = doc.getElementById(def.id);
if (dfn?.localName === "dfn") {
if (dfn?.localName === "dfn" && dfn.closest("pre.cddl")) {
registerDefinition(/** @type {HTMLElement} */ (dfn), [
dfn.textContent.trim(),
]);
Expand Down Expand Up @@ -722,7 +821,7 @@ export async function run() {
/** @type {CddlState} */
const state = {
definitions: new Map(),
proseDfns: new Set(),
proseDfns: new Map(),
genericParams: new Map(),
};

Expand All @@ -732,7 +831,10 @@ export async function run() {
// Step 2: Process each CDDL block
cddls.forEach(pre => processCddlBlock(pre, parse, state));

// Step 3: Resolve pending references (forward references across blocks)
// Step 3: Prefer prose dfns when CDDL blocks define the same term
linkGeneratedDfnsToProseDfns(document, state.definitions);

// Step 4: Resolve pending references (forward references across blocks)
resolvePendingRefs(document.body, state.definitions);

// Warn about any still-unresolved pending refs (likely typos)
Expand All @@ -744,16 +846,16 @@ export async function run() {
});
});

// Step 4: Resolve inline {^ ^} references to CDDL dfns
// Step 5: Resolve inline {^ ^} references to CDDL dfns
resolveInlineCddlRefs(document, state.definitions);

// Step 5: Register definitions with ReSpec's dfn-map
// Step 6: Register definitions with ReSpec's dfn-map
registerCddlDfns(document, state.definitions);

// Step 6: Inject runtime copy-button script (survives export)
// Step 7: Inject runtime copy-button script (survives export)
injectCopyScript();

// Step 7: Clean up CDDL-specific attributes on export
// Step 8: Clean up CDDL-specific attributes on export
sub("beforesave", (/** @type {Document} */ outputDoc) => {
outputDoc
.querySelectorAll("[data-cddl-pending]")
Expand Down
16 changes: 11 additions & 5 deletions tests/spec/core/cddl-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

const errorsFilter = errorFilters.filter("core/cddl");
const warningsFilter = warningFilters.filter("core/cddl");
const linkToDfnErrorsFilter = errorFilters.filter("core/link-to-dfn");
const linkToDfnWarningsFilter = warningFilters.filter("core/link-to-dfn");

describe("Core - CDDL", () => {
Expand Down Expand Up @@ -652,9 +653,9 @@ describe("Core - CDDL", () => {
});

describe("prose-level definitions", () => {
it("normalizes dfn[cddl-type] to data-dfn-type", async () => {
it("uses prose dfn[data-dfn-type='cddl-type'] definitions", async () => {
const body = `
<p>The <dfn cddl-type>attire</dfn> type represents clothing.</p>
<p>The <dfn data-dfn-type="cddl-type">attire</dfn> type represents clothing.</p>
<pre class="cddl">
attire = "bow tie" / "necktie"
</pre>
Expand All @@ -672,11 +673,12 @@ describe("Core - CDDL", () => {
d => d.textContent === "attire"
);
expect(attireBlockDfns).toHaveSize(0);
expect(linkToDfnErrorsFilter(doc)).toHaveSize(0);
});

it("normalizes dfn[cddl-key] with for attribute", async () => {
it("uses prose dfn[data-dfn-type='cddl-key'] with data-dfn-for attribute", async () => {
const body = `
<p>The <dfn cddl-key for="delivery">address</dfn> key.</p>
<p>The <dfn data-dfn-type="cddl-key" data-dfn-for="delivery">address</dfn> key.</p>
<pre class="cddl">
delivery = {
address: tstr,
Expand All @@ -693,14 +695,18 @@ describe("Core - CDDL", () => {

it("resolves inline refs to prose-defined cddl values", async () => {
const body = `
<p><dfn cddl-value for="attire">"bow tie"</dfn> means formal attire.</p>
<p><dfn data-dfn-type="cddl-value" data-dfn-for="attire">"bow tie"</dfn> means formal attire.</p>
<pre class="cddl">
attire = "bow tie" / "necktie"
</pre>
<p>See {^attire/"bow tie"^} for details.</p>
`;
const ops = makeStandardOps(null, body);
const doc = await makeRSDoc(ops);
const dfn = doc.querySelector(
"dfn[data-dfn-type='cddl-value'][data-dfn-for='attire']"
);
expect(dfn).toBeTruthy();
const link = doc.querySelector("p a[data-link-type='cddl-value'][href]");
expect(link).toBeTruthy();
expect(link.getAttribute("href")).toBe("#cddl-value-attire-bow-tie");
Expand Down
Loading