diff --git a/spec/index.html b/spec/index.html index 2de5a9e1..68c72b97 100644 --- a/spec/index.html +++ b/spec/index.html @@ -1202,6 +1202,7 @@

Other Styles & Conventions

Old IDs

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`} + + + +

This page has moved to ${target}.

+ + +`; +} + // 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 @@ + +
+
    +
  • Toggle shortcuts help?
  • +
  • Toggle "can call user code" annotationsu
  • +
  • Toggle early exit annotationse
  • +
  • Navigate to/from multipagem
  • +
  • Jump to search box/
  • +
  • Toggle pinning of the current clausep
  • +
  • Jump to the nth pin1-9
  • +
  • Jump to the 10th pin0
  • +
  • Jump to the most recent link target`
  • +
+
+ Multipage preference + + + +
+ + +

Intro

+

Some text.

+
+ + +

1 Renamed Clause

+

Some text.

+
+
\ 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 @@ + + + + + +
+
    +
  • Toggle shortcuts help?
  • +
  • Toggle "can call user code" annotationsu
  • +
  • Toggle early exit annotationse
  • +
  • Navigate to/from multipagem
  • +
  • Jump to search box/
  • +
  • Toggle pinning of the current clausep
  • +
  • Jump to the nth pin1-9
  • +
  • Jump to the 10th pin0
  • +
  • Jump to the most recent link target`
  • +
+
+ Multipage preference + + + +
+

Intro

+

Some text.

+
\ 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 @@ + + + + + +
+
    +
  • Toggle shortcuts help?
  • +
  • Toggle "can call user code" annotationsu
  • +
  • Toggle early exit annotationse
  • +
  • Navigate to/from multipagem
  • +
  • Jump to search box/
  • +
  • Toggle pinning of the current clausep
  • +
  • Jump to the nth pin1-9
  • +
  • Jump to the 10th pin0
  • +
  • Jump to the most recent link target`
  • +
+
+ Multipage preference + + + +
+

1 Renamed Clause

+

Some text.

+
\ No newline at end of file diff --git a/test/baselines/generated-reference/multipage-redirect.html/multipage/old-name.html b/test/baselines/generated-reference/multipage-redirect.html/multipage/old-name.html new file mode 100644 index 00000000..bd3ed7d9 --- /dev/null +++ b/test/baselines/generated-reference/multipage-redirect.html/multipage/old-name.html @@ -0,0 +1,32 @@ + + + + +Redirecting… + + + + + +

This page has moved to new-name.html.

+ + diff --git a/test/baselines/generated-reference/multipage-redirect.html/multipage/older-name.html b/test/baselines/generated-reference/multipage-redirect.html/multipage/older-name.html new file mode 100644 index 00000000..bd3ed7d9 --- /dev/null +++ b/test/baselines/generated-reference/multipage-redirect.html/multipage/older-name.html @@ -0,0 +1,32 @@ + + + + +Redirecting… + + + + + +

This page has moved to new-name.html.

+ + diff --git a/test/baselines/sources/multipage-redirect.html b/test/baselines/sources/multipage-redirect.html new file mode 100644 index 00000000..799d06be --- /dev/null +++ b/test/baselines/sources/multipage-redirect.html @@ -0,0 +1,15 @@ + + + +

Intro

+

Some text.

+
+ + +

Renamed Clause

+

Some text.

+