diff --git a/src/w3c/headers.js b/src/w3c/headers.js index 2874398879..8ed195c80b 100644 --- a/src/w3c/headers.js +++ b/src/w3c/headers.js @@ -400,13 +400,26 @@ export async function run(conf) { } if (conf.isEd) conf.thisVersion = conf.edDraftURI; - if (conf.isCGBG) validateCGBG(conf); + if (conf.isCGBG) { + validateCGBG(conf); + } + if (conf.isTagEditorFinding && !conf.latestVersion) { + conf.latestVersion = null; + } if (conf.latestVersion !== null) { conf.latestVersion = conf.latestVersion ? w3Url(conf.latestVersion) : pubSpace ? w3Url(`${pubSpace}/${conf.shortName}/`) : ""; + if (conf.latestVersion && !conf.isNoTrack && conf.specStatus !== "FPWD") { + const exists = await resourceExists(conf.latestVersion); + if (exists === null) { + const msg = `The "Latest published version:" header link points to a URL that does not exist.`; + const hint = docLink`Check that the ${"[shortName]"} is correct and you are using the right ${"[specStatus]"} for this kind of document.`; + showWarning(msg, name, { hint }); + } + } } if (conf.latestVersion) validateIfAllowedOnTR(conf); @@ -485,7 +498,12 @@ export async function run(conf) { conf.publishISODate = conf.publishDate.toISOString(); conf.shortISODate = ISODate.format(conf.publishDate); validatePatentPolicies(conf); - await deriveHistoryURI(conf); + + // Only derive historyURI if not explicitly suppressed by the user (null). + if (conf.historyURI !== null) { + conf.historyURI = await deriveHistoryURI(conf); + } + if (conf.isTagEditorFinding) { delete conf.thisVersion; delete conf.latestVersion; @@ -674,7 +692,6 @@ function validateIfAllowedOnTR(conf) { const msg = docLink`Documents with a status of \`"${conf.specStatus}"\` can't be published on the W3C's /TR/ (Technical Report) space.`; const hint = docLink`Ask a W3C Team Member for a W3C URL where the report can be published and change ${"[latestVersion]"} to something else.`; showError(msg, name, { hint }); - return; } } @@ -713,9 +730,6 @@ function derivePubSpace(conf) { function validateCGBG(conf) { // @ts-expect-error -- specStatus is always set by defaults const reportType = status2text[conf.specStatus]; - const latestVersionURL = conf.latestVersion - ? new URL(w3Url(conf.latestVersion)) - : null; if (!conf.wg) { const msg = docLink`The ${"[group]"} configuration option is required for this kind of document (${reportType}).`; @@ -723,8 +737,17 @@ function validateCGBG(conf) { return; } + // @ts-expect-error -- specStatus is always set by defaults + if (conf.specStatus.endsWith("-DRAFT") && !conf.latestVersion) { + conf.latestVersion = null; + return; + } + // Deal with final reports if (conf.isCGFinal) { + const latestVersionURL = conf.latestVersion + ? new URL(w3Url(conf.latestVersion)) + : null; // Final report require a w3.org URL. const isW3C = latestVersionURL?.origin === "https://www.w3.org" || @@ -740,19 +763,18 @@ function validateCGBG(conf) { /** @param {Conf} conf */ async function deriveHistoryURI(conf) { - if (!conf.shortName || conf.historyURI === null || !conf.latestVersion) { - return; // Nothing to do + if (!conf.shortName || !conf.latestVersion) { + return null; } // @ts-expect-error -- specStatus is always set by defaults const canShowHistory = conf.isEd || trStatus.includes(conf.specStatus); - if (conf.historyURI && !canShowHistory) { - const msg = docLink`The ${"[historyURI]"} can't be used with non /TR/ documents.`; + if (!canShowHistory && conf.historyURI) { + const msg = docLink`The ${"[historyURI]"} can't be used with non-standards track documents.`; const hint = docLink`Please remove ${"[historyURI]"}.`; showError(msg, name, { hint }); - conf.historyURI = null; - return; + return null; } const historyURL = new URL( @@ -767,21 +789,23 @@ async function deriveHistoryURI(conf) { // @ts-expect-error -- specStatus is always set by defaults ["FPWD", "DNOTE", "NOTE", "DRY"].includes(conf.specStatus) ) { - conf.historyURI = historyURL.href; - return; + return historyURL.href; } // Let's get the history from the W3C. // Do a fetch HEAD request to see if the history exists... // We don't discriminate... if it's on the W3C website with a history, // we show it. + return await resourceExists(historyURL); +} + +/** @returns {Promise} Final URL after redirects, or null if unreachable. */ +async function resourceExists(/** @type {string|URL} */ url) { try { - const response = await fetch(historyURL, { method: "HEAD" }); - if (response.ok) { - conf.historyURI = response.url; - } + const response = await fetch(url, { method: "HEAD" }); + return response.ok ? response.url : null; } catch { - // Ignore fetch errors + return null; } } diff --git a/src/w3c/templates/cgbg-headers.js b/src/w3c/templates/cgbg-headers.js index 3209302de6..cf8242ab03 100644 --- a/src/w3c/templates/cgbg-headers.js +++ b/src/w3c/templates/cgbg-headers.js @@ -41,10 +41,10 @@ export default (conf, options) => { > ` : ""} - ${"latestVersion" in conf // latestVersion can be falsy + ${conf.latestVersion !== null && conf.latestVersion !== undefined ? html`
${l10n.latest_published_version}
- ${conf.latestVersion + ${conf.latestVersion !== "" ? html`${conf.latestVersion}` diff --git a/tests/spec/w3c/headers-spec.js b/tests/spec/w3c/headers-spec.js index 19dd6197eb..7f8596ee6c 100644 --- a/tests/spec/w3c/headers-spec.js +++ b/tests/spec/w3c/headers-spec.js @@ -16,9 +16,11 @@ import { makeDefaultBody, makeRSDoc, makeStandardOps, + warningFilters, } from "../SpecHelper.js"; const headerErrors = errorFilters.filter("w3c/headers"); +const headerWarnings = warningFilters.filter("w3c/headers"); const defaultErrors = errorFilters.filter("w3c/defaults"); const findContent = string => { @@ -62,26 +64,27 @@ describe("W3C — Headers", () => { expect(exportedDoc.querySelector(".head details[open]")).toBeTruthy(); }); - it("links to the 'kinds of documents' only for W3C documents", async () => { - const statuses = ["FPWD", "WD", "CR", "CRD", "PR", "REC", "NOTE"]; - for (const specStatus of statuses) { + for (const specStatus of recTrackStatus) { + it(`links to the 'kinds of documents' only for W3C documents with status ${specStatus}`, async () => { const doc = await makeRSDoc( makeStandardOps({ specStatus, group: "webapps" }) ); const w3cLink = doc.querySelector( `.head a[href='https://www.w3.org/standards/types#${specStatus}']` ); - expect(w3cLink).withContext(`specStatus: ${specStatus}`).toBeTruthy(); - } + expect(w3cLink).toBeTruthy(); + }); + } - for (const specStatus of ["unofficial", "base"]) { + for (const specStatus of noTrackStatus) { + it(`doesn't link to the 'kinds of documents' for non-rec track ${specStatus}`, async () => { const doc = await makeRSDoc(makeStandardOps({ specStatus })); const w3cLink = doc.querySelector( ".head a[href='https://www.w3.org/standards/types#UD']" ); - expect(w3cLink).withContext(`specStatus: ${specStatus}`).toBeNull(); - } - }); + expect(w3cLink).toBeNull(); + }); + } describe("prevRecShortname & prevRecURI", () => { it("takes prevRecShortname and prevRecURI into account", async () => { @@ -1449,6 +1452,21 @@ describe("W3C — Headers", () => { expect(latestVersionLink.textContent).toBe("https://www.w3.org/TR/foo/"); }); + it("warns if latestVersion URL doesn't exist", async () => { + const ops = makeStandardOps({ + shortName: "foo", + specStatus: "WD", + group: "webapps", + github: "w3c/respec", + }); + const doc = await makeRSDoc(ops); + const warnings = headerWarnings(doc); + expect(warnings).toHaveSize(1); + expect(warnings[0].message).toContain( + `The "Latest published version:" header link points to a URL that does not exist` + ); + }); + it("allows skipping latest published version link in initial ED", async () => { const ops = makeStandardOps({ specStatus: "ED", @@ -1563,11 +1581,7 @@ describe("W3C — Headers", () => { group: "wicg", }); const doc = await makeRSDoc(ops); - const terms = [...doc.querySelectorAll(".head dt")]; - const latestVersion = terms.find( - el => el.textContent.trim() === "Latest published version:" - ); - expect(latestVersion).toHaveSize(0); + expect(contains(doc, "dt", "Latest published version:")).toHaveSize(0); }); } for (const specStatus of noTrackStatus) { @@ -2056,7 +2070,7 @@ describe("W3C — Headers", () => { { specStatus: "BG-FINAL", group: "publishingbg" }, ]; for (const { specStatus, group } of finalReportStatus) { - it("requires that the ${specStatus} latestVersion be a w3c URL", async () => { + it(`requires that the ${specStatus} latestVersion be a w3c URL`, async () => { const ops = makeStandardOps({ specStatus, group, @@ -2633,8 +2647,8 @@ describe("W3C — Headers", () => { ); }); - for (const specStatus of trStatus) { - it(`includes the history for "${specStatus}" rec-track status`, async () => { + for (const specStatus of recTrackStatus) { + it(`includes the history for rec-track "${specStatus}" docs`, async () => { const shortName = `push-api`; const ops = makeStandardOps({ shortName, @@ -2647,7 +2661,7 @@ describe("W3C — Headers", () => { expect(history).withContext(specStatus).toBeTruthy(); expect(history.nextElementSibling).withContext(specStatus).toBeTruthy(); const historyLink = history.nextElementSibling.querySelector("a"); - expect(historyLink).toBeTruthy(); + expect(historyLink).withContext(specStatus).toBeTruthy(); expect(historyLink.href).toBe( `https://www.w3.org/standards/history/${shortName}/` );