From 11e17b779da504f652e1f31996a103a1c864f5a2 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Sat, 25 Apr 2026 16:31:22 +1000 Subject: [PATCH 1/5] fix(xref): disambiguate overloaded method signatures in search results When multiple xref results share the same term and context (e.g., overloaded methods like Window/postMessage), extract parameter info from the URI fragment to show distinct linking text suggestions. Closes #383 --- static/xref/script.js | 95 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/static/xref/script.js b/static/xref/script.js index 6c143ace..dcb60334 100644 --- a/static/xref/script.js +++ b/static/xref/script.js @@ -144,6 +144,10 @@ function renderResults(entries, query) { return; } + // Detect overloaded IDL entries that would produce identical citations. + // Build a map of citation key -> list of entries to find duplicates. + const overloadedURIs = detectOverloadedEntries(entries, term); + let html = ''; for (const entry of entries) { // Use the canonical matched term when available (case-insensitive fallback @@ -152,8 +156,9 @@ function renderResults(entries, query) { const specInfo = metadata.specs[entry.status][entry.spec]; const link = new URL(entry.uri, specInfo.url).href; const title = escapeHTML(specInfo.title); + const isOverloaded = overloadedURIs.has(entry.uri); const cite = metadata.types.idl.has(entry.type) - ? howToCiteIDL(citeTerm, entry) + ? howToCiteIDL(citeTerm, entry, isOverloaded) : metadata.types.markup.has(entry.type) ? howToCiteMarkup(citeTerm, entry) : metadata.types.css.has(entry.type) || @@ -172,7 +177,30 @@ function renderResults(entries, query) { output.innerHTML = html; } -function howToCiteIDL(term, entry) { +/** + * Detects IDL entries that would produce identical citations (overloaded + * methods/constructors). Returns a Set of URIs that need disambiguation. + */ +function detectOverloadedEntries(entries, term) { + const citationGroups = new Map(); + for (const entry of entries) { + if (!metadata.types.idl.has(entry.type)) continue; + const forKey = (entry.for || []).join(','); + const key = `${entry.type}|${forKey}|${term}`; + const group = citationGroups.get(key) ?? []; + if (!group.length) citationGroups.set(key, group); + group.push(entry); + } + const overloadedURIs = new Set(); + for (const group of citationGroups.values()) { + if (group.length > 1) { + group.forEach(entry => overloadedURIs.add(entry.uri)); + } + } + return overloadedURIs; +} + +function howToCiteIDL(term, entry, isOverloaded = false) { const { type, for: forList } = entry; const safeTerm = escapeHTML(term); if (forList) { @@ -180,18 +208,75 @@ function howToCiteIDL(term, entry) { .map(f => { const safeF = escapeHTML(f); const termPart = type === 'enum-value' ? `"${safeTerm}"` : safeTerm; - return `{{${safeF}/${safeTerm ? termPart : '""'}}}`; + let cite = `{{${safeF}/${safeTerm ? termPart : '""'}}}`; + if (isOverloaded) { + const hint = extractOverloadHint(entry.uri, f, term); + if (hint) { + cite += ` (${escapeHTML(hint)})`; + } + } + return cite; }) .join('
'); } + let cite; switch (type) { case 'exception': if (!exceptionExceptions.has(term)) { - return `{{"${safeTerm}"}}`; + cite = `{{"${safeTerm}"}}`; + break; } default: - return `{{${safeTerm}}}`; + cite = `{{${safeTerm}}}`; + } + if (isOverloaded) { + const hint = extractOverloadHint(entry.uri, null, term); + if (hint) { + cite += ` (${escapeHTML(hint)})`; + } + } + return cite; +} + +/** + * Extracts a human-readable overload hint from a URI fragment. + * + * URI fragments for WebIDL definitions follow the pattern: + * #dom-----... + * + * For example: + * #dom-window-postmessage-message-targetorigin-transfer + * #dom-window-postmessage-message-options + * + * This function strips the known prefix (interface + method) and returns + * the remaining parts as a parameter list, e.g. "message, targetorigin, transfer". + */ +function extractOverloadHint(uri, forContext, term) { + if (!uri) return ''; + const hash = uri.includes('#') ? uri.split('#')[1] : uri; + if (!hash) return ''; + + const parts = hash.toLowerCase().split('-'); + + // Build the prefix to strip: typically "dom", interface, method + const prefixParts = ['dom']; + if (forContext) { + prefixParts.push(...forContext.toLowerCase().split('-')); + } + const cleanTerm = term.replace(/\(.*\)$/, '').toLowerCase(); + if (cleanTerm) { + prefixParts.push(...cleanTerm.split('-')); } + + const matches = + prefixParts.length <= parts.length && + prefixParts.every((p, i) => parts[i] === p); + + if (matches && prefixParts.length < parts.length) { + return parts.slice(prefixParts.length).join(', '); + } + + return ''; } function howToCiteMarkup(term, entry) { From 83195f7c49d2dcd78951fb4d317c80a0b15e0d40 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Mon, 27 Apr 2026 14:26:27 +1000 Subject: [PATCH 2/5] fix(xref): embed full parameter signature in overloaded cite text Instead of appending a hint after the citation, embed the parameter names directly inside the {{ }} brackets. This produces copy-pasteable cite text that resolves without ambiguity, e.g. {{Window/postMessage(message, options)}} instead of {{Window/postMessage()}} (message, options). --- static/xref/script.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/static/xref/script.js b/static/xref/script.js index dcb60334..6155ebfb 100644 --- a/static/xref/script.js +++ b/static/xref/script.js @@ -207,15 +207,14 @@ function howToCiteIDL(term, entry, isOverloaded = false) { return forList .map(f => { const safeF = escapeHTML(f); - const termPart = type === 'enum-value' ? `"${safeTerm}"` : safeTerm; - let cite = `{{${safeF}/${safeTerm ? termPart : '""'}}}`; + let displayTerm = type === 'enum-value' ? `"${safeTerm}"` : safeTerm; if (isOverloaded) { const hint = extractOverloadHint(entry.uri, f, term); if (hint) { - cite += ` (${escapeHTML(hint)})`; + displayTerm = displayTerm.replace('()', `(${escapeHTML(hint)})`); } } - return cite; + return `{{${safeF}/${displayTerm ? displayTerm : '""'}}}`; }) .join('
'); } @@ -232,7 +231,7 @@ function howToCiteIDL(term, entry, isOverloaded = false) { if (isOverloaded) { const hint = extractOverloadHint(entry.uri, null, term); if (hint) { - cite += ` (${escapeHTML(hint)})`; + cite = cite.replace('()', `(${escapeHTML(hint)})`); } } return cite; From 6fa7d38fe6828d2d28f3b54b6db02b53d7549188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20C=C3=A1ceres?= Date: Mon, 27 Apr 2026 18:35:13 +1000 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- static/xref/script.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/xref/script.js b/static/xref/script.js index 6155ebfb..190f8d3a 100644 --- a/static/xref/script.js +++ b/static/xref/script.js @@ -186,7 +186,9 @@ function detectOverloadedEntries(entries, term) { for (const entry of entries) { if (!metadata.types.idl.has(entry.type)) continue; const forKey = (entry.for || []).join(','); - const key = `${entry.type}|${forKey}|${term}`; + const specKey = entry.spec || ''; + const statusKey = entry.status || ''; + const key = `${entry.type}|${forKey}|${term}|${specKey}|${statusKey}`; const group = citationGroups.get(key) ?? []; if (!group.length) citationGroups.set(key, group); group.push(entry); From f131fba880da04cb5f5bed69078f44550b5a0c8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:40:28 +0000 Subject: [PATCH 4/5] fix(xref): preserve fragment casing in hints; group overloads per citation Agent-Logs-Url: https://github.com/speced/respec-web-services/sessions/3856cbfc-2f26-4105-b882-db96f03098c9 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- static/xref/script.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/static/xref/script.js b/static/xref/script.js index 190f8d3a..6db0505d 100644 --- a/static/xref/script.js +++ b/static/xref/script.js @@ -185,13 +185,20 @@ function detectOverloadedEntries(entries, term) { const citationGroups = new Map(); for (const entry of entries) { if (!metadata.types.idl.has(entry.type)) continue; - const forKey = (entry.for || []).join(','); const specKey = entry.spec || ''; const statusKey = entry.status || ''; - const key = `${entry.type}|${forKey}|${term}|${specKey}|${statusKey}`; - const group = citationGroups.get(key) ?? []; - if (!group.length) citationGroups.set(key, group); - group.push(entry); + const forList = entry.for || []; + // Group per individual rendered citation (one per `f`), so overlapping + // `for` contexts across entries are correctly detected as ambiguous. + const keys = + forList.length > 0 + ? forList.map(f => `${f}|${term}|${specKey}|${statusKey}`) + : [`|${term}|${specKey}|${statusKey}`]; + for (const key of keys) { + const group = citationGroups.get(key) ?? []; + if (!group.length) citationGroups.set(key, group); + group.push(entry); + } } const overloadedURIs = new Set(); for (const group of citationGroups.values()) { @@ -257,7 +264,8 @@ function extractOverloadHint(uri, forContext, term) { const hash = uri.includes('#') ? uri.split('#')[1] : uri; if (!hash) return ''; - const parts = hash.toLowerCase().split('-'); + const originalParts = hash.split('-'); + const lowerParts = hash.toLowerCase().split('-'); // Build the prefix to strip: typically "dom", interface, method const prefixParts = ['dom']; @@ -270,11 +278,11 @@ function extractOverloadHint(uri, forContext, term) { } const matches = - prefixParts.length <= parts.length && - prefixParts.every((p, i) => parts[i] === p); + prefixParts.length <= lowerParts.length && + prefixParts.every((p, i) => lowerParts[i] === p); - if (matches && prefixParts.length < parts.length) { - return parts.slice(prefixParts.length).join(', '); + if (matches && prefixParts.length < lowerParts.length) { + return originalParts.slice(prefixParts.length).join(', '); } return ''; From 805b4ac63f5d1eeea1c248cbb8f5197f78c2abc9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 09:08:56 +0000 Subject: [PATCH 5/5] fix(xref): track overloaded pairs per (uri, forContext) not just uri Agent-Logs-Url: https://github.com/speced/respec-web-services/sessions/ddb44eac-0c8b-4050-9210-f242b70f3e59 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --- static/xref/script.js | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/static/xref/script.js b/static/xref/script.js index 6db0505d..7696a39a 100644 --- a/static/xref/script.js +++ b/static/xref/script.js @@ -145,8 +145,8 @@ function renderResults(entries, query) { } // Detect overloaded IDL entries that would produce identical citations. - // Build a map of citation key -> list of entries to find duplicates. - const overloadedURIs = detectOverloadedEntries(entries, term); + // Build a set of "uri|forContext" pairs for entries that need disambiguation. + const overloadedPairs = detectOverloadedEntries(entries, term); let html = ''; for (const entry of entries) { @@ -156,9 +156,8 @@ function renderResults(entries, query) { const specInfo = metadata.specs[entry.status][entry.spec]; const link = new URL(entry.uri, specInfo.url).href; const title = escapeHTML(specInfo.title); - const isOverloaded = overloadedURIs.has(entry.uri); const cite = metadata.types.idl.has(entry.type) - ? howToCiteIDL(citeTerm, entry, isOverloaded) + ? howToCiteIDL(citeTerm, entry, overloadedPairs) : metadata.types.markup.has(entry.type) ? howToCiteMarkup(citeTerm, entry) : metadata.types.css.has(entry.type) || @@ -179,7 +178,9 @@ function renderResults(entries, query) { /** * Detects IDL entries that would produce identical citations (overloaded - * methods/constructors). Returns a Set of URIs that need disambiguation. + * methods/constructors). Returns a Set of "uri|forContext" keys for entries + * that need disambiguation. Entries without a `for` list use an empty string + * as the forContext part. */ function detectOverloadedEntries(entries, term) { const citationGroups = new Map(); @@ -190,26 +191,33 @@ function detectOverloadedEntries(entries, term) { const forList = entry.for || []; // Group per individual rendered citation (one per `f`), so overlapping // `for` contexts across entries are correctly detected as ambiguous. - const keys = - forList.length > 0 - ? forList.map(f => `${f}|${term}|${specKey}|${statusKey}`) - : [`|${term}|${specKey}|${statusKey}`]; - for (const key of keys) { + if (forList.length > 0) { + for (const f of forList) { + const key = `${f}|${term}|${specKey}|${statusKey}`; + const group = citationGroups.get(key) ?? []; + if (!group.length) citationGroups.set(key, group); + group.push({ entry, f }); + } + } else { + const key = `|${term}|${specKey}|${statusKey}`; const group = citationGroups.get(key) ?? []; if (!group.length) citationGroups.set(key, group); - group.push(entry); + group.push({ entry, f: '' }); } } - const overloadedURIs = new Set(); + // Build a Set of "uri|forContext" strings for ambiguous (entry, f) pairs. + const overloadedPairs = new Set(); for (const group of citationGroups.values()) { if (group.length > 1) { - group.forEach(entry => overloadedURIs.add(entry.uri)); + for (const { entry, f } of group) { + overloadedPairs.add(`${entry.uri}|${f}`); + } } } - return overloadedURIs; + return overloadedPairs; } -function howToCiteIDL(term, entry, isOverloaded = false) { +function howToCiteIDL(term, entry, overloadedPairs = null) { const { type, for: forList } = entry; const safeTerm = escapeHTML(term); if (forList) { @@ -217,7 +225,7 @@ function howToCiteIDL(term, entry, isOverloaded = false) { .map(f => { const safeF = escapeHTML(f); let displayTerm = type === 'enum-value' ? `"${safeTerm}"` : safeTerm; - if (isOverloaded) { + if (overloadedPairs?.has(`${entry.uri}|${f}`)) { const hint = extractOverloadHint(entry.uri, f, term); if (hint) { displayTerm = displayTerm.replace('()', `(${escapeHTML(hint)})`); @@ -237,7 +245,7 @@ function howToCiteIDL(term, entry, isOverloaded = false) { default: cite = `{{${safeTerm}}}`; } - if (isOverloaded) { + if (overloadedPairs?.has(`${entry.uri}|`)) { const hint = extractOverloadHint(entry.uri, null, term); if (hint) { cite = cite.replace('()', `(${escapeHTML(hint)})`);