Skip to content
Draft
3 changes: 2 additions & 1 deletion src/core/link-to-dfn.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment on lines +173 to +174
}
}

Expand Down
32 changes: 32 additions & 0 deletions src/core/location-hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -39,6 +40,37 @@ export function run() {
const updatedElement = document.getElementById(id);
if (updatedElement) {
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 scopedDfnElements = [
...document.querySelectorAll(
`[data-dfn-type][id^='${DFN_ID_PREFIX}']`
),
];
let matchingElements = scopedDfnElements.filter(({ id }) => {
const scopedId = id.slice(DFN_ID_PREFIX.length);
return (
scopedId === legacyTerm || scopedId.endsWith(termWithLeadingHyphen)
);
});
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;
}
}
}
window.location.hash = `#${newHash}`;
Expand Down
27 changes: 26 additions & 1 deletion tests/spec/core/link-to-dfn-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,36 @@ 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();
Comment on lines 18 to 22
});

it("uses data-dfn-for in generated IDs for duplicate terms", async () => {
const body = `
<section>
<h2>Test section</h2>
<p>
<dfn data-dfn-for="Request">state</dfn>
<dfn data-dfn-for="Response">state</dfn>
<a data-link-for="Request" data-link-type="dfn" id="requestState">state</a>
<a data-link-for="Response" data-link-type="dfn" id="responseState">state</a>
</p>
</section>`;
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 = `
<section data-link-for="Request">
Expand Down
25 changes: 25 additions & 0 deletions tests/spec/core/location-hash-legacy-indexed.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!doctype html>
<html>
<meta charset="utf-8" />
<title>Legacy Indexed Hashes</title>
<script class="remove">
var respecConfig = {
specStatus: "ED",
shortName: "basics",
editors: [{ name: "Robin Berjon", url: "http://berjon.com/" }],
github: "speced/respec",
group: "webapps",
};
</script>
<section id="abstract">
<p>Basic doc</p>
</section>
<section id="sotd">
<p>CUSTOM PARAGRAPH</p>
</section>
<section>
<h2>Legacy indexed terms</h2>
<p><dfn data-dfn-for="Window">unsafe current time</dfn></p>
<p><dfn data-dfn-for="Worker">unsafe current time</dfn></p>
</section>
</html>
8 changes: 7 additions & 1 deletion tests/spec/core/location-hash-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -22,7 +23,12 @@ 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);
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);
});
});
2 changes: 1 addition & 1 deletion tests/spec/core/xref-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,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]]
Expand Down