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
80 changes: 68 additions & 12 deletions src/core/inline-idl-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" }}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 };
}

/**
Expand Down Expand Up @@ -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 ? "." : ""}<a
data-link-type="idl"
data-xref-type="attribute|dict-member|const"
data-link-type="${linkType}"
data-xref-type="${xrefType}"
data-link-for="${linkFor}"
data-xref-for="${linkFor}"
><code>${identifier}</code></a
Expand All @@ -292,16 +332,18 @@ function renderAttribute(details) {
/**
* Method: .identifier(arg1, arg2, ...), identifier(arg1, arg2, ...)
* @param {IdlMethod} details
* @param {string} [typeHint]
*/
function renderMethod(details) {
function renderMethod(details, typeHint) {
const { args, identifier, type, parent, renderParent } = details;
const { renderText: text, renderArgs: textArgs } = details;
const { identifier: linkFor } = parent || {};
const argsText = htmlJoinComma(textArgs || args, htmlArgMapper);
const searchText = `${identifier}(${args.join(", ")})`;
const xrefType = typeHint || type;
const element = html`${parent && renderParent ? "." : ""}<a
data-link-type="idl"
data-xref-type="${type}"
data-xref-type="${xrefType}"
data-link-for="${linkFor}"
data-xref-for="${linkFor}"
data-lt="${searchText}"
Expand Down Expand Up @@ -369,9 +411,9 @@ function renderIdlPrimitiveType(details) {
* @return {Node} html output
*/
export function idlStringToHtml(str) {
let results;
let parsed;
try {
results = parseInlineIDL(str);
parsed = parseInlineIDL(str);
} catch (error) {
const el = html`<span>{{ ${str} }}</span>`;
const title = "Error: Invalid inline IDL string.";
Expand All @@ -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`<span>{{ ${str} }}</span>`;
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) {
Expand All @@ -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));
Expand Down
45 changes: 44 additions & 1 deletion tests/spec/core/inlines-spec.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -538,6 +545,42 @@ describe("Core - Inlines", () => {
);
});

it("links {{ Interface/event!!event }} to event-type definitions", async () => {
const body = `
<section data-dfn-for="ScreenOrientation">
<h2><dfn>ScreenOrientation</dfn></h2>
<dfn data-dfn-type="event" data-dfn-for="ScreenOrientation">change</dfn>
<p id="test">{{ ScreenOrientation/change!!event }}</p>
</section>
`;
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 = `
<section>
<p id="test">{{ Window/event!!nonsense }}</p>
</section>
`;
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 = `
<section>
Expand Down