Skip to content
Draft
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
14 changes: 7 additions & 7 deletions src/core/link-to-dfn.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,13 @@ function showLinkingError(elems) {
elems.forEach(elem => {
const msg = `Found linkless \`<a>\` element with text "${elem.textContent}" but no matching \`<dfn>\``;
const title = "Linking error: no matching `<dfn>`";
// 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]")
);
const scopingNote = scopedSection
? ` 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 \`<a>\` or move it outside the scoped section.`
// The nearest [data-link-for] sets the scope (as in getLinkTargets), so an
// empty one, or one on the <a> itself (from `[=Iface/term=]`), is the
// author's own doing and needs no hint.
const scope = elem.closest("[data-link-for]");
const linkFor = scope === elem ? "" : scope?.getAttribute("data-link-for");
const scopingNote = linkFor
? ` This link is inside a \`data-link-for="${linkFor}"\` section — \`[=term=]\` links are scoped to that context. To link to a global concept instead, either add \`data-link-for=""\` on this \`<a>\` or move it outside the scoped section.`
: "";
const hint = `Add a matching \`<dfn>\` element, ${docLink`use ${"[data-cite]"} to link to an external definition, or enable ${"[xref]"} for automatic cross-spec linking.`}${scopingNote}`;
showWarning(msg, name, { title, hint, elements: [elem] });
Expand Down
34 changes: 33 additions & 1 deletion tests/spec/core/link-to-dfn-spec.js
Original file line number Diff line number Diff line change
@@ -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 = `
Expand Down Expand Up @@ -404,4 +410,30 @@ describe("Core — Link to definitions", () => {
const corrupt = doc.querySelector("[data-cite*='__SPEC__']");
expect(corrupt).toBeNull();
});

it("hints at data-link-for scoping only when an ancestor sets it", async () => {
const body = `
<section data-link-for="Iface">
<h2>Scoped</h2>
<p><a>ancestorScope</a>, [= Iface/ownScope =], <a data-link-for="">optedOut</a></p>
<section data-link-for="">
<h3>Unscoped subsection</h3>
<p><a>emptyAncestor</a></p>
</section>
</section>
<section>
<h2>Unscoped</h2>
<p><a>noScope</a></p>
</section>
`;
const doc = await makeRSDoc(makeStandardOps({ xref: false }, body));
const linkErrors = warnings(doc);
expect(linkErrors).toHaveSize(5);
const hintFor = text => linkErrors.find(w => w.message.includes(text)).hint;
expect(hintFor("ancestorScope")).toContain('data-link-for="Iface"');
expect(hintFor("ownScope")).not.toContain("data-link-for=");
expect(hintFor("optedOut")).not.toContain("data-link-for=");
expect(hintFor("emptyAncestor")).not.toContain("data-link-for=");
expect(hintFor("noScope")).not.toContain("data-link-for=");
});
});