Old IDs for any element can be stored separated by commas with optional whitespace inside an oldids attribute, allowing links using them to continue working as expected.
+
In multipage builds, an old ID on a top-level clause or annex which looks like a top-level section ID (i.e., it begins with `sec-`) also causes a redirect page to be generated at the corresponding old location. For example, if a clause with id `sec-numbers-and-math` has `oldids="sec-numbers-and-dates"`, then `multipage/numbers-and-dates.html` will redirect to `multipage/numbers-and-math.html`. The fragment is preserved: if it identifies an element which is now in a different section, the redirect goes to that section instead.
diff --git a/src/Spec.ts b/src/Spec.ts
index 544eefbf..d6f9bcf1 100644
--- a/src/Spec.ts
+++ b/src/Spec.ts
@@ -862,45 +862,17 @@ export default class Spec {
}
private readSectionId(ele: Element) {
- if (ele.id == null) {
+ const result = sectionNameFromId(ele.id);
+ if (typeof result !== 'string') {
this.warn({
type: 'node',
ruleId: 'top-level-section-id',
- message: 'When using --multipage, top-level sections must have ids',
+ message: result.error,
node: ele,
});
return undefined;
}
- if (!ele.id.startsWith('sec-')) {
- this.warn({
- type: 'node',
- ruleId: 'top-level-section-id',
- message: 'When using --multipage, top-level sections must have ids beginning with `sec-`',
- node: ele,
- });
- return undefined;
- }
- const name = ele.id.substring(4);
- if (!/^[A-Za-z0-9-_]+$/.test(name)) {
- this.warn({
- type: 'node',
- ruleId: 'top-level-section-id',
- message:
- 'When using --multipage, top-level sections must have ids matching /^[A-Za-z0-9-_]+$/',
- node: ele,
- });
- return undefined;
- }
- if (name.toLowerCase() === 'index') {
- this.warn({
- type: 'node',
- ruleId: 'top-level-section-id',
- message: 'When using --multipage, top-level sections must not be named "index"',
- node: ele,
- });
- return undefined;
- }
- return name;
+ return result;
}
private propagateEffects() {
@@ -960,6 +932,7 @@ export default class Spec {
let stillIntro = true;
const introEles = [];
const sections: { name: string; eles: Element[] }[] = [];
+ const redirects: { from: string; to: string; node: Element }[] = [];
const containedIdToSection: Map = new Map();
const sectionToContainedIds: Map = new Map();
const clauseTypes = ['EMU-ANNEX', 'EMU-CLAUSE'];
@@ -1001,6 +974,15 @@ export default class Spec {
}
section = { name, eles: [child] };
+
+ const oldids = child.getAttribute('oldids');
+ if (oldids) {
+ for (const oldid of oldids.split(',').map((s: string) => s.trim())) {
+ const oldName = sectionNameFromId(oldid);
+ if (typeof oldName !== 'string') continue;
+ redirects.push({ from: oldName, to: name, node: child });
+ }
+ }
}
sections.push(section);
@@ -1031,6 +1013,7 @@ export default class Spec {
/[\\`$]/g,
'\\$&',
);
+ let multipageScriptSrc: string | null = null;
if (this.assets.type !== 'none') {
const multipageJsContents = `'use strict';
let multipageMap = JSON.parse(\`${containedMap}\`);
@@ -1046,13 +1029,15 @@ ${await utils.readFile(path.join(__dirname, '../js/multipage.js'))}
// the path will be rewritten below
// so it should initially be relative to outfile, not outfile/multipage
const multipageScript = this.doc.createElement('script');
- multipageScript.src =
+ const multipageScriptPath =
path.relative(this.opts.outfile!, multipageLocationOnDisk) +
'?cache=' +
sha(multipageJsContents);
+ multipageScript.src = multipageScriptPath;
// fetch in parallel, but evaluate ASAP in case of redirect
multipageScript.setAttribute('async', '');
this.doc.head.insertBefore(multipageScript, this.doc.head.querySelector('script'));
+ multipageScriptSrc = path.relative('multipage', multipageScriptPath);
}
const head = this.doc.head.cloneNode(true) as HTMLHeadElement;
@@ -1132,6 +1117,35 @@ ${await utils.readFile(path.join(__dirname, '../js/multipage.js'))}
this.generatedFiles.set(path.join(this.opts.outfile!, `multipage/${name}.html`), content);
}
+
+ const pageNames = new Set(sections.map(s => s.name.toLowerCase()));
+ const redirectNames = new Set();
+ for (const { from, to, node } of redirects) {
+ const lower = from.toLowerCase();
+ if (pageNames.has(lower)) {
+ this.warn({
+ type: 'node',
+ ruleId: 'multipage-redirect',
+ message: `oldid "sec-${from}" conflicts with an existing multipage section; not generating a redirect`,
+ node,
+ });
+ continue;
+ }
+ if (redirectNames.has(lower)) {
+ this.warn({
+ type: 'node',
+ ruleId: 'multipage-redirect',
+ message: `oldid "sec-${from}" is used by multiple top-level sections; not generating a second redirect`,
+ node,
+ });
+ continue;
+ }
+ redirectNames.add(lower);
+ this.generatedFiles.set(
+ path.join(this.opts.outfile!, `multipage/${from}.html`),
+ multipageRedirectPage(to, multipageScriptSrc),
+ );
+ }
}
private async buildAssets(jsContents: string, jsSha: string) {
@@ -2330,6 +2344,73 @@ function sha(str: string) {
.replace(/\//g, '_');
}
+function sectionNameFromId(id: string | null | undefined): string | { error: string } {
+ if (id == null) {
+ return { error: 'When using --multipage, top-level sections must have ids' };
+ }
+ if (!id.startsWith('sec-')) {
+ return {
+ error: 'When using --multipage, top-level sections must have ids beginning with `sec-`',
+ };
+ }
+ const name = id.substring(4);
+ if (!/^[A-Za-z0-9-_]+$/.test(name)) {
+ return {
+ error: 'When using --multipage, top-level sections must have ids matching /^[A-Za-z0-9-_]+$/',
+ };
+ }
+ if (name.toLowerCase() === 'index') {
+ return { error: 'When using --multipage, top-level sections must not be named "index"' };
+ }
+ return name;
+}
+
+function escapeAttr(str: string) {
+ return str.replace(/&/g, '&').replace(/"/g, '"').replace(/
+
+
+
+Redirecting…
+
+${multipageScriptSrc == null ? '' : `\n`}
+
+
+
+
+
+
+`;
+}
+
// jsdom does not handle the `.hostname` (etc) parts correctly, so we have to look at the href directly
// it also (some?) relative links as links to about:blank, for the purposes of testing the href
function linkIsAbsolute(link: HTMLAnchorElement | HTMLLinkElement | HTMLScriptElement) {
diff --git a/test/baselines/generated-reference/multipage-redirect.html/index.html b/test/baselines/generated-reference/multipage-redirect.html/index.html
new file mode 100644
index 00000000..ee0f011f
--- /dev/null
+++ b/test/baselines/generated-reference/multipage-redirect.html/index.html
@@ -0,0 +1,33 @@
+
+
\ No newline at end of file
diff --git a/test/baselines/generated-reference/multipage-redirect.html/multipage/index.html b/test/baselines/generated-reference/multipage-redirect.html/multipage/index.html
new file mode 100644
index 00000000..5698a172
--- /dev/null
+++ b/test/baselines/generated-reference/multipage-redirect.html/multipage/index.html
@@ -0,0 +1,29 @@
+
+
+
\ No newline at end of file
diff --git a/test/baselines/generated-reference/multipage-redirect.html/multipage/new-name.html b/test/baselines/generated-reference/multipage-redirect.html/multipage/new-name.html
new file mode 100644
index 00000000..48550fec
--- /dev/null
+++ b/test/baselines/generated-reference/multipage-redirect.html/multipage/new-name.html
@@ -0,0 +1,29 @@
+
+
+