From 6ef5e8c73ef59006be2f010f5b4e96849b072c6a Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Sat, 9 May 2026 14:19:25 +1000 Subject: [PATCH 1/7] fix(core/link-to-dfn): incorporate data-dfn-for into generated IDs When a dfn has a data-dfn-for attribute, include the for-context in the generated ID. This produces semantically meaningful IDs like dfn-wall-clock-unsafe-current-time instead of dfn-unsafe-current-time-0 when multiple dfns share the same text but target different interfaces. Closes #4415 --- src/core/link-to-dfn.js | 3 ++- tests/spec/core/link-to-dfn-spec.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/link-to-dfn.js b/src/core/link-to-dfn.js index e412c29aba..d85b425e81 100644 --- a/src/core/link-to-dfn.js +++ b/src/core/link-to-dfn.js @@ -170,7 +170,8 @@ function collectDfns(title) { if ("idl" in dfn.dataset || dfnType !== "dfn") { result.get(dfnFor)?.set("idl", dfn); } - addId(dfn, "dfn", title); + const idText = dfnFor ? `${dfnFor}-${title}` : title; + addId(dfn, "dfn", idText); } } diff --git a/tests/spec/core/link-to-dfn-spec.js b/tests/spec/core/link-to-dfn-spec.js index 4be40fe6a5..fcdd250785 100644 --- a/tests/spec/core/link-to-dfn-spec.js +++ b/tests/spec/core/link-to-dfn-spec.js @@ -17,7 +17,7 @@ describe("Core — Link to definitions", () => { const doc = await makeRSDoc(ops); const a = doc.getElementById("testAnchor"); expect(a).toBeTruthy(); - expect(a.hash).toBe("#dfn-test"); + expect(a.hash).toBe("#dfn-window-test"); const decodedHash = decodeURIComponent(a.hash); expect(doc.getElementById(decodedHash.slice(1))).toBeTruthy(); }); From cc836ceec3c059eaa2c2e363f5c3135cb3e62374 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 16:18:10 +0000 Subject: [PATCH 2/7] fix(location-hash): recover legacy slot fragments for dfn-for ids Agent-Logs-Url: https://github.com/speced/respec/sessions/7f9f0a1d-7edc-41b3-a99b-731fff0c9a77 --- src/core/location-hash.js | 8 ++++++++ tests/spec/core/location-hash-spec.js | 2 +- tests/spec/core/xref-spec.js | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/location-hash.js b/src/core/location-hash.js index 1135d28ebd..825ef0ca18 100644 --- a/src/core/location-hash.js +++ b/src/core/location-hash.js @@ -39,6 +39,14 @@ export function run() { const updatedElement = document.getElementById(id); if (updatedElement) { newHash = id; + } else if (id.startsWith("dfn-")) { + const legacySuffix = `-${id.slice("dfn-".length)}`; + const matchingElements = [...document.querySelectorAll("[id]")].filter( + ({ id }) => id.startsWith("dfn-") && id.endsWith(legacySuffix) + ); + if (matchingElements.length === 1) { + newHash = matchingElements[0].id; + } } } window.location.hash = `#${newHash}`; diff --git a/tests/spec/core/location-hash-spec.js b/tests/spec/core/location-hash-spec.js index a95be7d3b8..1940c1dfa5 100644 --- a/tests/spec/core/location-hash-spec.js +++ b/tests/spec/core/location-hash-spec.js @@ -22,7 +22,7 @@ describe("Core — Location Hash", () => { it("recovers legacy encoded hashes for slots", async () => { const testURL = `${simpleURL}#dfn-%5B%5Bescapedslot%5D%5D`; const doc = await makeRSDoc(ops, testURL); - expect(doc.location.hash).toBe("#dfn-escapedslot"); + expect(doc.location.hash).toBe("#dfn-test-escapedslot"); }, 20000); }); }); diff --git a/tests/spec/core/xref-spec.js b/tests/spec/core/xref-spec.js index faa0c40ac3..83910285a7 100644 --- a/tests/spec/core/xref-spec.js +++ b/tests/spec/core/xref-spec.js @@ -848,7 +848,7 @@ describe("Core — xref", () => { // as base == [[type]], it is treated as a local internal slot const link1 = doc.querySelector("#link1 a"); - expect(link1.getAttribute("href")).toBe("#dfn-type"); + expect(link1.getAttribute("href")).toBe("#dfn-window-type"); expect(link1.firstElementChild.localName).toBe("code"); // the base "Credential" is used as "forContext" for [[type]] From 12868e911986f9b755a9598ec965e65622923d5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 16:19:57 +0000 Subject: [PATCH 3/7] perf(location-hash): narrow legacy dfn fallback lookup Agent-Logs-Url: https://github.com/speced/respec/sessions/7f9f0a1d-7edc-41b3-a99b-731fff0c9a77 --- src/core/location-hash.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/location-hash.js b/src/core/location-hash.js index 825ef0ca18..77cc70c0c8 100644 --- a/src/core/location-hash.js +++ b/src/core/location-hash.js @@ -40,10 +40,10 @@ export function run() { if (updatedElement) { newHash = id; } else if (id.startsWith("dfn-")) { - const legacySuffix = `-${id.slice("dfn-".length)}`; - const matchingElements = [...document.querySelectorAll("[id]")].filter( - ({ id }) => id.startsWith("dfn-") && id.endsWith(legacySuffix) - ); + const legacyTermSuffix = `-${id.slice("dfn-".length)}`; + const matchingElements = [ + ...document.querySelectorAll("[id^='dfn-']"), + ].filter(({ id }) => id.endsWith(legacyTermSuffix)); if (matchingElements.length === 1) { newHash = matchingElements[0].id; } From 3269d6ee92485074d2520efbe7c3deef86da35bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 16:21:22 +0000 Subject: [PATCH 4/7] refactor(location-hash): consolidate dfn prefix and selector matching Agent-Logs-Url: https://github.com/speced/respec/sessions/7f9f0a1d-7edc-41b3-a99b-731fff0c9a77 --- src/core/location-hash.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/location-hash.js b/src/core/location-hash.js index 77cc70c0c8..d419f0bfe5 100644 --- a/src/core/location-hash.js +++ b/src/core/location-hash.js @@ -6,6 +6,7 @@ // the window to the correct point in the document when processing is done. export const name = "core/location-hash"; +const DFN_ID_PREFIX = "dfn-"; export function run() { if (!window.location.hash) { @@ -39,11 +40,13 @@ export function run() { const updatedElement = document.getElementById(id); if (updatedElement) { newHash = id; - } else if (id.startsWith("dfn-")) { - const legacyTermSuffix = `-${id.slice("dfn-".length)}`; + } else if (id.startsWith(DFN_ID_PREFIX)) { + const legacyTermSuffix = `-${id.slice(DFN_ID_PREFIX.length)}`; const matchingElements = [ - ...document.querySelectorAll("[id^='dfn-']"), - ].filter(({ id }) => id.endsWith(legacyTermSuffix)); + ...document.querySelectorAll( + `[id^='${DFN_ID_PREFIX}'][id$='${CSS.escape(legacyTermSuffix)}']` + ), + ]; if (matchingElements.length === 1) { newHash = matchingElements[0].id; } From d5aa6cbcea7c6f7ed43862d3eced5030f0c29454 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 16:22:41 +0000 Subject: [PATCH 5/7] chore(location-hash): clarify fallback variable naming Agent-Logs-Url: https://github.com/speced/respec/sessions/7f9f0a1d-7edc-41b3-a99b-731fff0c9a77 --- src/core/location-hash.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/location-hash.js b/src/core/location-hash.js index d419f0bfe5..c7b9e08918 100644 --- a/src/core/location-hash.js +++ b/src/core/location-hash.js @@ -41,10 +41,10 @@ export function run() { if (updatedElement) { newHash = id; } else if (id.startsWith(DFN_ID_PREFIX)) { - const legacyTermSuffix = `-${id.slice(DFN_ID_PREFIX.length)}`; + const termWithLeadingHyphen = `-${id.slice(DFN_ID_PREFIX.length)}`; const matchingElements = [ ...document.querySelectorAll( - `[id^='${DFN_ID_PREFIX}'][id$='${CSS.escape(legacyTermSuffix)}']` + `[id^='${DFN_ID_PREFIX}'][id$='${CSS.escape(termWithLeadingHyphen)}']` ), ]; if (matchingElements.length === 1) { From a697576c4578c33eea87c418ebc10b7917800149 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 01:47:55 +0000 Subject: [PATCH 6/7] fix(location-hash): ignore dfn panel ids in legacy slot recovery Agent-Logs-Url: https://github.com/speced/respec/sessions/6b35abee-131c-42cf-814d-c3ecf3b0e035 --- src/core/location-hash.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/core/location-hash.js b/src/core/location-hash.js index c7b9e08918..13b4da3bbe 100644 --- a/src/core/location-hash.js +++ b/src/core/location-hash.js @@ -41,12 +41,18 @@ export function run() { if (updatedElement) { newHash = id; } else if (id.startsWith(DFN_ID_PREFIX)) { - const termWithLeadingHyphen = `-${id.slice(DFN_ID_PREFIX.length)}`; + const legacyTerm = id.slice(DFN_ID_PREFIX.length); + const termWithLeadingHyphen = `-${legacyTerm}`; const matchingElements = [ ...document.querySelectorAll( - `[id^='${DFN_ID_PREFIX}'][id$='${CSS.escape(termWithLeadingHyphen)}']` + `[data-dfn-type][id^='${DFN_ID_PREFIX}']` ), - ]; + ].filter(({ id }) => { + const scopedId = id.slice(DFN_ID_PREFIX.length).replace(/-\d+$/, ""); + return ( + scopedId === legacyTerm || scopedId.endsWith(termWithLeadingHyphen) + ); + }); if (matchingElements.length === 1) { newHash = matchingElements[0].id; } From 5ffd7c7349cda02c12c3fd1693ea255d32d2d3b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 05:54:36 +0000 Subject: [PATCH 7/7] fix(location-hash): recover legacy numeric-suffix dfn fragments Agent-Logs-Url: https://github.com/speced/respec/sessions/684c9c74-a3fb-4068-a957-3f7a1fdc1498 --- src/core/location-hash.js | 23 ++++++++++++++--- tests/spec/core/link-to-dfn-spec.js | 25 +++++++++++++++++++ .../core/location-hash-legacy-indexed.html | 25 +++++++++++++++++++ tests/spec/core/location-hash-spec.js | 6 +++++ 4 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 tests/spec/core/location-hash-legacy-indexed.html diff --git a/src/core/location-hash.js b/src/core/location-hash.js index 13b4da3bbe..2e5f41feff 100644 --- a/src/core/location-hash.js +++ b/src/core/location-hash.js @@ -42,18 +42,33 @@ export function run() { newHash = id; } else if (id.startsWith(DFN_ID_PREFIX)) { const legacyTerm = id.slice(DFN_ID_PREFIX.length); + const numericSuffixMatch = legacyTerm.match(/^(.+)-(\d+)$/); const termWithLeadingHyphen = `-${legacyTerm}`; - const matchingElements = [ + const scopedDfnElements = [ ...document.querySelectorAll( `[data-dfn-type][id^='${DFN_ID_PREFIX}']` ), - ].filter(({ id }) => { - const scopedId = id.slice(DFN_ID_PREFIX.length).replace(/-\d+$/, ""); + ]; + let matchingElements = scopedDfnElements.filter(({ id }) => { + const scopedId = id.slice(DFN_ID_PREFIX.length); return ( scopedId === legacyTerm || scopedId.endsWith(termWithLeadingHyphen) ); }); - if (matchingElements.length === 1) { + if (!matchingElements.length && numericSuffixMatch) { + const [, baseTerm, index] = numericSuffixMatch; + const baseTermWithLeadingHyphen = `-${baseTerm}`; + matchingElements = scopedDfnElements.filter(({ id }) => { + const scopedId = id.slice(DFN_ID_PREFIX.length); + return ( + scopedId === baseTerm || + scopedId.endsWith(baseTermWithLeadingHyphen) + ); + }); + if (matchingElements[Number(index)]) { + newHash = matchingElements[Number(index)].id; + } + } else if (matchingElements.length === 1) { newHash = matchingElements[0].id; } } diff --git a/tests/spec/core/link-to-dfn-spec.js b/tests/spec/core/link-to-dfn-spec.js index fcdd250785..4b29d30379 100644 --- a/tests/spec/core/link-to-dfn-spec.js +++ b/tests/spec/core/link-to-dfn-spec.js @@ -22,6 +22,31 @@ describe("Core — Link to definitions", () => { expect(doc.getElementById(decodedHash.slice(1))).toBeTruthy(); }); + it("uses data-dfn-for in generated IDs for duplicate terms", async () => { + const body = ` +
+

Test section

+

+ state + state + state + state +

+
`; + const ops = makeStandardOps(null, body); + const doc = await makeRSDoc(ops); + const [requestState, responseState] = doc.querySelectorAll("dfn"); + expect(requestState.id).toBe("dfn-request-state"); + expect(responseState.id).toBe("dfn-response-state"); + expect(requestState.id).not.toMatch(/-\d+$/); + expect(responseState.id).not.toMatch(/-\d+$/); + + const requestStateLink = doc.getElementById("requestState"); + const responseStateLink = doc.getElementById("responseState"); + expect(requestStateLink.hash).toBe("#dfn-request-state"); + expect(responseStateLink.hash).toBe("#dfn-response-state"); + }); + it("links to IDL definitions and wraps in code if needed", async () => { const bodyText = `
diff --git a/tests/spec/core/location-hash-legacy-indexed.html b/tests/spec/core/location-hash-legacy-indexed.html new file mode 100644 index 0000000000..1e7743377f --- /dev/null +++ b/tests/spec/core/location-hash-legacy-indexed.html @@ -0,0 +1,25 @@ + + + + Legacy Indexed Hashes + +
+

Basic doc

+
+
+

CUSTOM PARAGRAPH

+
+
+

Legacy indexed terms

+

unsafe current time

+

unsafe current time

+
+ diff --git a/tests/spec/core/location-hash-spec.js b/tests/spec/core/location-hash-spec.js index 1940c1dfa5..cb644b69d9 100644 --- a/tests/spec/core/location-hash-spec.js +++ b/tests/spec/core/location-hash-spec.js @@ -6,6 +6,7 @@ describe("Core — Location Hash", () => { afterAll(flushIframes); const ops = makeStandardOps(); const simpleURL = "/tests/spec/core/simple.html"; + const legacyIndexedURL = "/tests/spec/core/location-hash-legacy-indexed.html"; describe("legacy fragment format", () => { it("leaves editor defined id alone, even if they include illegal chars", async () => { @@ -24,5 +25,10 @@ describe("Core — Location Hash", () => { const doc = await makeRSDoc(ops, testURL); expect(doc.location.hash).toBe("#dfn-test-escapedslot"); }, 20000); + it("recovers legacy numeric-suffixed hashes", async () => { + const testURL = `${legacyIndexedURL}#dfn-unsafe-current-time-0`; + const doc = await makeRSDoc(ops, testURL); + expect(doc.location.hash).toBe("#dfn-window-unsafe-current-time"); + }, 20000); }); });