From aaaae26a3b50ac3e6c1e1d3c7bf5041964ca88d5 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Mon, 4 May 2026 11:01:53 +1000 Subject: [PATCH 1/5] fix(core/inlines): allow [= =] to link event-type definitions Closes #4290 --- src/core/inlines.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/inlines.js b/src/core/inlines.js index 58fc671498..3e98bc5f25 100644 --- a/src/core/inlines.js +++ b/src/core/inlines.js @@ -271,7 +271,7 @@ function inlineAnchorMatches(matched) { const processedContent = processInlineContent(text); const forContext = isFor ? norm(isFor) : null; return html` Date: Mon, 4 May 2026 21:17:40 +1000 Subject: [PATCH 2/5] test(core/inlines): add regression test for event-type linking --- tests/spec/core/inlines-spec.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/spec/core/inlines-spec.js b/tests/spec/core/inlines-spec.js index bdb85132d5..df62a40f41 100644 --- a/tests/spec/core/inlines-spec.js +++ b/tests/spec/core/inlines-spec.js @@ -538,6 +538,20 @@ describe("Core - Inlines", () => { ); }); + it("links [= =] to event-type definitions", async () => { + const body = ` +
+ orientationchange +

[= orientationchange =]

+
+ `; + const doc = await makeRSDoc(makeStandardOps(null, body)); + const anchor = doc.querySelector("#test a"); + expect(anchor).toBeTruthy(); + expect(anchor.getAttribute("href")).toBe("#dfn-orientationchange"); + expect(anchor.textContent).toBe("orientationchange"); + }); + it("processes {{ forContext/term }} IDL", async () => { const body = `
From 7f20b0b6c91ba2a249d08ae0419b342bbd66def9 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Wed, 6 May 2026 20:22:39 +1000 Subject: [PATCH 3/5] feat(core/inline-idl-parser): add !!type disambiguation for IDL links Adds Bikeshed-compatible !!type suffix to {{ }} IDL inline syntax. When present, the type hint overrides the default fallback type list, allowing authors to disambiguate between IDL members that share a name (e.g., an attribute and an event on the same interface). Example: {{ PermissionStatus/change!!event }} links specifically to the "change" event, not the "onchange" attribute. Reverts the [= =] approach for event linking in favor of this general type disambiguation mechanism. Closes #4290 --- src/core/inline-idl-parser.js | 34 +++++++++++++++++++++++---------- src/core/inlines.js | 2 +- tests/spec/core/inlines-spec.js | 19 ++++++++++-------- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/core/inline-idl-parser.js b/src/core/inline-idl-parser.js index d905838835..86d877590f 100644 --- a/src/core/inline-idl-parser.js +++ b/src/core/inline-idl-parser.js @@ -88,6 +88,14 @@ const isProbablySlotRegex = /\[\[.+\]\]/; * @returns {InlineIdl[]} */ function parseInlineIDL(str) { + // Extract !!type suffix for type disambiguation (Bikeshed compat) + let typeHint = ""; + if (str.includes("!!")) { + [str, typeHint] = str.split("!!", 2); + str = str.trim(); + typeHint = typeHint.trim(); + } + // If it's got [[ string ]], then split as an internal slot const isSlot = isProbablySlotRegex.test(str); const splitter = isSlot ? slotSplitRegex : methodSplitRegex; @@ -215,7 +223,7 @@ function parseInlineIDL(str) { item.parent = list[i + 1] || null; }); // return them in the order we found them... - return results.reverse(); + return { tokens: results.reverse(), typeHint }; } /** @@ -275,13 +283,16 @@ function htmlArgMapper(str, i, array) { /** * Attribute: .identifier * @param {IdlAttribute} details + * @param {string} [typeHint] */ -function renderAttribute(details) { +function renderAttribute(details, typeHint) { const { parent, identifier, renderParent } = details; const { identifier: linkFor } = parent || {}; + const xrefType = typeHint || "attribute|dict-member|const"; + const linkType = typeHint || "idl"; const element = html`${renderParent ? "." : ""}${identifier}{{ ${str} }}`; const title = "Error: Invalid inline IDL string."; @@ -381,6 +394,7 @@ export function idlStringToHtml(str) { }); return el; } + const { tokens: results, typeHint } = parsed; const render = html(document.createDocumentFragment()); const output = []; for (const details of results) { @@ -391,13 +405,13 @@ export function idlStringToHtml(str) { break; } case "attribute": - output.push(renderAttribute(details)); + output.push(renderAttribute(details, typeHint)); break; case "internal-slot": output.push(renderInternalSlot(details)); break; case "method": - output.push(renderMethod(details)); + output.push(renderMethod(details, typeHint)); break; case "enum": output.push(renderEnum(details)); diff --git a/src/core/inlines.js b/src/core/inlines.js index 3e98bc5f25..58fc671498 100644 --- a/src/core/inlines.js +++ b/src/core/inlines.js @@ -271,7 +271,7 @@ function inlineAnchorMatches(matched) { const processedContent = processInlineContent(text); const forContext = isFor ? norm(isFor) : null; return html` { ); }); - it("links [= =] to event-type definitions", async () => { + it("links {{ Interface/event!!event }} to event-type definitions", async () => { const body = ` -
- orientationchange -

[= orientationchange =]

+
+

ScreenOrientation

+ change +

{{ ScreenOrientation/change!!event }}

`; const doc = await makeRSDoc(makeStandardOps(null, body)); - const anchor = doc.querySelector("#test a"); - expect(anchor).toBeTruthy(); - expect(anchor.getAttribute("href")).toBe("#dfn-orientationchange"); - expect(anchor.textContent).toBe("orientationchange"); + const para = doc.getElementById("test"); + const anchor = para.querySelector("a"); + expect(anchor).withContext(para.innerHTML).toBeTruthy(); + expect(anchor.dataset.xrefType).toBe("event"); + expect(anchor.dataset.linkFor).toBe("ScreenOrientation"); + expect(anchor.textContent).toBe("change"); }); it("processes {{ forContext/term }} IDL", async () => { From dfcda62f86267fbffba648b13cb2ca9a756d9787 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:20:45 +0000 Subject: [PATCH 4/5] fix(core/inline-idl-parser): add warning for invalid !!type hints and fix JSDoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `showWarning` to imports - Define `validTypeHints` set for the `!!type` disambiguation suffix - Fix `parseInlineIDL` JSDoc: `@returns {InlineIdl[]}` → `@returns {{ tokens: InlineIdl[], typeHint: string }}` - Validate `typeHint` in `idlStringToHtml`: warn and fall back to default when type is unknown - Add test for invalid `!!type` hint warning behavior Agent-Logs-Url: https://github.com/speced/respec/sessions/e4d82a3b-b17c-4efd-bbdf-5170f1489756 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- src/core/inline-idl-parser.js | 48 ++++++++++++++++++++++++++++++--- tests/spec/core/inlines-spec.js | 28 ++++++++++++++++++- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/core/inline-idl-parser.js b/src/core/inline-idl-parser.js index 86d877590f..12b90858df 100644 --- a/src/core/inline-idl-parser.js +++ b/src/core/inline-idl-parser.js @@ -2,7 +2,7 @@ // Parses an inline IDL string (`{{ idl string }}`) // and renders its components as HTML -import { htmlJoinComma, showError } from "./utils.js"; +import { htmlJoinComma, showError, showWarning } from "./utils.js"; import { html } from "./import-maps.js"; const idlPrimitiveRegex = /^[a-z]+(\s+[a-z]+)+\??$/; // {{unrestricted double?}} {{ double }} const exceptionRegex = /\B"([^"]*)"\B/; // {{ "SomeException" }} @@ -30,6 +30,35 @@ const enumRegex = /^(\w+)\["([\w- ]*)"\]$/; const methodSplitRegex = /\.?(\w+\(.*\)$)/; const slotSplitRegex = /\/(.+)/; const isProbablySlotRegex = /\[\[.+\]\]/; + +/** Valid types for the `!!type` disambiguation suffix in `{{ }}` inline IDL syntax */ +const validTypeHints = new Set([ + "abstract-op", + "attr-value", + "attribute", + "callback", + "const", + "dict-member", + "dfn", + "dictionary", + "element", + "element-attr", + "element-state", + "enum", + "enum-value", + "event", + "exception", + "extended-attribute", + "http-header", + "interface", + "interface-mixin", + "method", + "namespace", + "permission", + "scheme", + "typedef", +]); + /** * @typedef {object} IdlBase * @property {"base"} type @@ -85,7 +114,7 @@ const isProbablySlotRegex = /\[\[.+\]\]/; /** * @param {string} str - * @returns {InlineIdl[]} + * @returns {{ tokens: InlineIdl[], typeHint: string }} */ function parseInlineIDL(str) { // Extract !!type suffix for type disambiguation (Bikeshed compat) @@ -394,7 +423,20 @@ export function idlStringToHtml(str) { }); return el; } - const { tokens: results, typeHint } = parsed; + const { tokens: results, typeHint: rawTypeHint } = parsed; + let typeHint = rawTypeHint; + if (typeHint && !validTypeHints.has(typeHint)) { + const el = html`{{ ${str} }}`; + showWarning( + `Unknown type hint "!!${typeHint}" in \`{{ ${str} }}\`. Falling back to default type resolution.`, + "core/inlines", + { + elements: [el], + hint: `Expected one of: ${[...validTypeHints].join(", ")}.`, + } + ); + typeHint = ""; + } const render = html(document.createDocumentFragment()); const output = []; for (const details of results) { diff --git a/tests/spec/core/inlines-spec.js b/tests/spec/core/inlines-spec.js index 51b4193140..5cd9d65ba2 100644 --- a/tests/spec/core/inlines-spec.js +++ b/tests/spec/core/inlines-spec.js @@ -1,6 +1,13 @@ "use strict"; -import { flushIframes, makeRSDoc, makeStandardOps } from "../SpecHelper.js"; +import { + flushIframes, + makeRSDoc, + makeStandardOps, + warningFilters, +} from "../SpecHelper.js"; + +const inlinesWarningsFilter = warningFilters.filter("core/inlines"); describe("Core - Inlines", () => { afterAll(flushIframes); @@ -555,6 +562,25 @@ describe("Core - Inlines", () => { expect(anchor.textContent).toBe("change"); }); + it("warns on unknown !!type hints and falls back to default", async () => { + const body = ` +
+

{{ Window/event!!nonsense }}

+
+ `; + const doc = await makeRSDoc(makeStandardOps(null, body)); + const para = doc.getElementById("test"); + const anchor = para.querySelector("a"); + // Falls back to default (no type override), anchor still renders + expect(anchor).withContext(para.innerHTML).toBeTruthy(); + // data-xref-type should be the default for an attribute (no typeHint applied) + expect(anchor.dataset.xrefType).not.toBe("nonsense"); + // A warning was emitted for the invalid type hint + const warnings = inlinesWarningsFilter(doc); + expect(warnings.length).toBeGreaterThanOrEqual(1); + expect(warnings[0].message).toContain("!!nonsense"); + }); + it("processes {{ forContext/term }} IDL", async () => { const body = `
From 9ead02387463ddb6d57351f1558622c2782f810e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:22:04 +0000 Subject: [PATCH 5/5] fix(core/inline-idl-parser): shorten invalid !!type hint message Use concise common-type hint instead of listing all 20 valid types. Agent-Logs-Url: https://github.com/speced/respec/sessions/e4d82a3b-b17c-4efd-bbdf-5170f1489756 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- src/core/inline-idl-parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/inline-idl-parser.js b/src/core/inline-idl-parser.js index 12b90858df..392a8ad7fb 100644 --- a/src/core/inline-idl-parser.js +++ b/src/core/inline-idl-parser.js @@ -432,7 +432,7 @@ export function idlStringToHtml(str) { "core/inlines", { elements: [el], - hint: `Expected one of: ${[...validTypeHints].join(", ")}.`, + hint: `Common types: attribute, method, event, interface, dict-member, const, enum-value.`, } ); typeHint = "";