From 405beb36500ab0b8b61a0aee87c2324c20342a06 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Mon, 4 May 2026 19:23:08 +1000 Subject: [PATCH 1/2] fix(core/link-to-dfn): use parentElement.closest() for scoping hint `elem.closest("[data-link-for]")` matches the element itself, so when an `` has `data-link-for` (e.g., from `[=bar/foo=]` syntax), the hint incorrectly says the link is "inside" a scoped section when the attribute is on the link itself. Use `parentElement.closest()` to restrict to ancestor elements. Also guard against the empty-string case (`data-link-for=""` means intentionally unscoped). Closes #5257 --- src/core/link-to-dfn.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/link-to-dfn.js b/src/core/link-to-dfn.js index e412c29aba..3f4d41b564 100644 --- a/src/core/link-to-dfn.js +++ b/src/core/link-to-dfn.js @@ -323,9 +323,9 @@ function showLinkingError(elems) { // Check if the link is inside a data-link-for section — a common footgun // where [=global-term=] gets scoped to the interface and fails. const scopedSection = /** @type {HTMLElement | null} */ ( - elem.closest("[data-link-for]") + elem.parentElement?.closest("[data-link-for]") ); - const scopingNote = scopedSection + const scopingNote = scopedSection?.dataset.linkFor ? ` This link is inside a \`data-link-for="${scopedSection.dataset.linkFor}"\` section — \`[=term=]\` links are scoped to that context. To link to a global concept instead, either add \`data-link-for=""\` on this \`\` or move it outside the scoped section.` : ""; const hint = `Add a matching \`\` element, ${docLink`use ${"[data-cite]"} to link to an external definition, or enable ${"[xref]"} for automatic cross-spec linking.`}${scopingNote}`; From a1e718366e4b43ec1675f23dc0390c10582614d2 Mon Sep 17 00:00:00 2001 From: Marcos Caceres Date: Mon, 4 May 2026 19:55:42 +1000 Subject: [PATCH 2/2] fix: normalize type annotation and add regression test Address Copilot feedback: - Add `?? null` to normalize `undefined` from optional chaining to `null`, matching the JSDoc type `HTMLElement | null` - Add test covering all 3 scoping hint cases: non-empty ancestor data-link-for (hint fires), empty data-link-for (hint omitted), and no data-link-for (hint omitted) --- src/core/link-to-dfn.js | 2 +- tests/spec/core/link-to-dfn-spec.js | 46 ++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/core/link-to-dfn.js b/src/core/link-to-dfn.js index 3f4d41b564..b6e12a2232 100644 --- a/src/core/link-to-dfn.js +++ b/src/core/link-to-dfn.js @@ -323,7 +323,7 @@ function showLinkingError(elems) { // Check if the link is inside a data-link-for section — a common footgun // where [=global-term=] gets scoped to the interface and fails. const scopedSection = /** @type {HTMLElement | null} */ ( - elem.parentElement?.closest("[data-link-for]") + elem.parentElement?.closest("[data-link-for]") ?? null ); const scopingNote = scopedSection?.dataset.linkFor ? ` This link is inside a \`data-link-for="${scopedSection.dataset.linkFor}"\` section — \`[=term=]\` links are scoped to that context. To link to a global concept instead, either add \`data-link-for=""\` on this \`\` or move it outside the scoped section.` diff --git a/tests/spec/core/link-to-dfn-spec.js b/tests/spec/core/link-to-dfn-spec.js index 4be40fe6a5..be01454a32 100644 --- a/tests/spec/core/link-to-dfn-spec.js +++ b/tests/spec/core/link-to-dfn-spec.js @@ -1,9 +1,15 @@ "use strict"; -import { flushIframes, makeRSDoc, makeStandardOps } from "../SpecHelper.js"; +import { + flushIframes, + makeRSDoc, + makeStandardOps, + warningFilters, +} from "../SpecHelper.js"; describe("Core — Link to definitions", () => { afterAll(flushIframes); + const warnings = warningFilters.filter("core/link-to-dfn"); it("removes non-alphanum chars from fragment components", async () => { const bodyText = ` @@ -404,4 +410,42 @@ describe("Core — Link to definitions", () => { const corrupt = doc.querySelector("[data-cite*='__SPEC__']"); expect(corrupt).toBeNull(); }); + + it("scoping hint only fires for ancestor data-link-for, not self", async () => { + const body = ` +
+

Scoping

+
+

Inside scope

+

globalTerm

+
+
+

Empty scope

+

anotherTerm

+
+
+

No scope

+

noScopeTerm

+
+
+ `; + const ops = makeStandardOps(null, body); + const doc = await makeRSDoc(ops); + const scopedWarnings = warnings(doc); + const insideScopeWarning = scopedWarnings.find(w => + w.message?.includes("globalTerm") + ); + const emptyScopeWarning = scopedWarnings.find(w => + w.message?.includes("anotherTerm") + ); + const noScopeWarning = scopedWarnings.find(w => + w.message?.includes("noScopeTerm") + ); + expect(insideScopeWarning).toBeTruthy(); + expect(insideScopeWarning.hint).toContain('data-link-for="Iface"'); + expect(emptyScopeWarning).toBeTruthy(); + expect(emptyScopeWarning.hint).not.toContain("data-link-for="); + expect(noScopeWarning).toBeTruthy(); + expect(noScopeWarning.hint).not.toContain("data-link-for="); + }); });