diff --git a/src/core/inline-idl-parser.js b/src/core/inline-idl-parser.js index d905838835..392a8ad7fb 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,9 +114,17 @@ const isProbablySlotRegex = /\[\[.+\]\]/; /** * @param {string} str - * @returns {InlineIdl[]} + * @returns {{ tokens: InlineIdl[], typeHint: string }} */ 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 +252,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 +312,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 +423,20 @@ export function idlStringToHtml(str) { }); return el; } + 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: `Common types: attribute, method, event, interface, dict-member, const, enum-value.`, + } + ); + typeHint = ""; + } const render = html(document.createDocumentFragment()); const output = []; for (const details of results) { @@ -391,13 +447,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/tests/spec/core/inlines-spec.js b/tests/spec/core/inlines-spec.js index bdb85132d5..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); @@ -538,6 +545,42 @@ describe("Core - Inlines", () => { ); }); + it("links {{ Interface/event!!event }} to event-type definitions", async () => { + const body = ` +
+

ScreenOrientation

+ change +

{{ ScreenOrientation/change!!event }}

+
+ `; + const doc = await makeRSDoc(makeStandardOps(null, body)); + 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("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 = `