Skip to content
32 changes: 27 additions & 5 deletions src/w3c/templates/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ export default (conf, options) => {
if (details) details.open = true;
}
);
// Link text shows the readable IRI; the href keeps the encoded URL.
const iri = {
thisVersion: safeDecodeURI(conf.thisVersion),
latestVersion: safeDecodeURI(conf.latestVersion),
edDraftURI: safeDecodeURI(conf.edDraftURI),
historyURI: safeDecodeURI(conf.historyURI),
prevVersion: safeDecodeURI(conf.prevVersion),
};
return html`<div class="head">
${
(conf.logos ?? []).length
Expand All @@ -256,7 +264,7 @@ export default (conf, options) => {
? html`<dt>${l10n.this_version}</dt>
<dd>
<a class="u-url" href="${conf.thisVersion}"
>${conf.thisVersion}</a
>${iri.thisVersion}</a
>
</dd>`
: ""
Expand All @@ -268,7 +276,7 @@ export default (conf, options) => {
${
conf.latestVersion
? html`<a href="${conf.latestVersion}"
>${conf.latestVersion}</a
>${iri.latestVersion}</a
>`
: "none"
}
Expand All @@ -279,7 +287,7 @@ export default (conf, options) => {
conf.edDraftURI
? html`
<dt>${l10n.latest_editors_draft}</dt>
<dd><a href="${conf.edDraftURI}">${conf.edDraftURI}</a></dd>
<dd><a href="${conf.edDraftURI}">${iri.edDraftURI}</a></dd>
`
: ""
}
Expand All @@ -289,7 +297,7 @@ export default (conf, options) => {
${
conf.historyURI
? html`<dd>
<a href="${conf.historyURI}">${conf.historyURI}</a>
<a href="${conf.historyURI}">${iri.historyURI}</a>
</dd>`
: ""
}
Expand Down Expand Up @@ -339,7 +347,7 @@ export default (conf, options) => {
conf.showPreviousVersion
? html`
<dt>${l10n.prev_version}</dt>
<dd><a href="${conf.prevVersion}">${conf.prevVersion}</a></dd>
<dd><a href="${conf.prevVersion}">${iri.prevVersion}</a></dd>
`
: ""
}
Expand Down Expand Up @@ -504,6 +512,20 @@ function renderSpecTitle(conf) {
}`;
}

/**
* Shows a percent-encoded URL as the IRI a human can read, or as-is when it
* has malformed escape sequences.
* @param {string | null} [url]
*/
function safeDecodeURI(url) {
if (!url) return url;
try {
return decodeURI(url);
} catch {
return url;
}
}

/**
* @param { LicenseInfo } licenseInfo license information
*/
Expand Down
2 changes: 1 addition & 1 deletion tests/spec/core/anchor-expander-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ describe("Core - anchor-expander", () => {
expect(span.textContent.trim()).toEqual("MediaDevices");
expect(span.attributes).toHaveSize(0);
}
});
}, 10000);

it("expands references to dfn elements, including terms starting with '<'", async () => {
const body = `
Expand Down
69 changes: 69 additions & 0 deletions tests/spec/w3c/headers-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,75 @@ describe("W3C — Headers", () => {
expect(latestVersionDd.textContent.trim()).toBe("none");
});
}

it("shows the IRI as link text, but keeps the encoded URL in href", async () => {
const ops = makeStandardOps({
specStatus: "WD",
group: "webapps",
latestVersion: "https://www.w3.org/TR/föö-spec/",
});
const doc = await makeRSDoc(ops);
const [dt] = contains(doc, ".head dt", "Latest published version:");
const anchor = dt.nextElementSibling.querySelector("a");
expect(anchor.getAttribute("href")).toBe(
"https://www.w3.org/TR/f%C3%B6%C3%B6-spec/"
);
expect(anchor.textContent.trim()).toBe("https://www.w3.org/TR/föö-spec/");
});

it("keeps the link text as-is when the URL can't be decoded", async () => {
const latestVersion = "https://www.w3.org/TR/foo%zz/";
const ops = makeStandardOps({
specStatus: "WD",
group: "webapps",
latestVersion,
});
const doc = await makeRSDoc(ops);
const [dt] = contains(doc, ".head dt", "Latest published version:");
const anchor = dt.nextElementSibling.querySelector("a");
expect(anchor.getAttribute("href")).toBe(latestVersion);
expect(anchor.textContent.trim()).toBe(latestVersion);
});

it("shows the IRI for the other URLs in the same list", async () => {
// These four are used verbatim, unlike latestVersion which w3Url()
// normalizes, so the href stays exactly as the author wrote it.
const edDraftURI = "https://example.com/f%C3%B6%C3%B6/ed/";
const historyURI = "https://www.w3.org/standards/history/f%C3%B6%C3%B6/";
const ops = makeStandardOps({
specStatus: "WD",
group: "webapps",
edDraftURI,
historyURI,
});
const doc = await makeRSDoc(ops);
[
["Latest editor's draft:", edDraftURI, "https://example.com/föö/ed/"],
["History:", historyURI, "https://www.w3.org/standards/history/föö/"],
].forEach(([label, href, iri]) => {
const [dt] = contains(doc, ".head dt", label);
const anchor = dt.nextElementSibling.querySelector("a");
expect(anchor.textContent.trim()).toBe(iri);
expect(anchor.getAttribute("href")).toBe(href);
});
});

it("shows the IRI for an author-supplied thisVersion", async () => {
// thisVersion is only author-supplied for CG/BG documents; elsewhere
// w3Url() derives it. prevVersion is always w3Url()-derived, so it can
// never carry percent-encoding from config.
const thisVersion = "https://example.com/f%C3%B6%C3%B6/2026/";
const ops = makeStandardOps({
specStatus: "CG-DRAFT",
wgPublicList: "public-webapps",
thisVersion,
});
const doc = await makeRSDoc(ops);
const [dt] = contains(doc, ".head dt", "This version:");
const anchor = dt.nextElementSibling.querySelector("a");
expect(anchor.textContent.trim()).toBe("https://example.com/föö/2026/");
expect(anchor.getAttribute("href")).toBe(thisVersion);
});
});

describe("prevED", () => {
Expand Down