Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@
},
"globals": {
"multipageMap": "readonly",
"usesMultipage": "readonly",
"isMultipage": "readonly",
"idToSection": "readonly",
"toggleMultipage": "readonly",
"sdoMap": "readonly",
"biblio": "readonly",
"debounce": "writable",
Expand Down
7 changes: 7 additions & 0 deletions css/elements.css
Original file line number Diff line number Diff line change
Expand Up @@ -1675,3 +1675,10 @@ li.menu-search-result-term::before {
background: var(--figure-background);
width: 500px;
}

/* Other dynamic elements */
:root[data-multipage-preference=''] [data-multipage-preference=''],
:root[data-multipage-preference='single-page'] [data-multipage-preference='single-page'],
:root[data-multipage-preference='multi-page'] [data-multipage-preference='multi-page'] {
font-weight: bold;
}
4 changes: 4 additions & 0 deletions css/print.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.no-print {
display: none !important;
}

@font-face {
font-family: 'Arial Plus';
src: local('Arial');
Expand Down
27 changes: 6 additions & 21 deletions js/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -1069,11 +1069,11 @@ function sortByClauseNumber(clause1, clause2) {

function makeLinkToId(id) {
let hash = '#' + id;
if (typeof idToSection === 'undefined' || !idToSection[id]) {
return hash;
if (typeof isMultipage !== 'undefined' && isMultipage && idToSection[id]) {
let targetSec = idToSection[id];
return (targetSec === 'index' ? './' : targetSec + '.html') + hash;
}
let targetSec = idToSection[id];
return (targetSec === 'index' ? './' : targetSec + '.html') + hash;
return hash;
}

function doShortcut(e) {
Expand All @@ -1088,23 +1088,8 @@ function doShortcut(e) {
if (e.altKey || e.ctrlKey || e.metaKey) {
return;
}
if (e.key === 'm' && usesMultipage) {
let pathParts = location.pathname.split('/');
let hash = location.hash;
if (pathParts[pathParts.length - 2] === 'multipage') {
if (hash === '') {
let sectionName = pathParts[pathParts.length - 1];
if (sectionName.endsWith('.html')) {
sectionName = sectionName.slice(0, -5);
}
if (idToSection['sec-' + sectionName] !== undefined) {
hash = '#sec-' + sectionName;
}
}
location = pathParts.slice(0, -2).join('/') + '/' + hash;
} else {
location = 'multipage/' + hash;
}
if (e.key === 'm' && typeof toggleMultipage !== 'undefined') {
toggleMultipage();
} else if (e.key === 'u') {
document.documentElement.classList.toggle('show-uc-annotations');
} else if (e.key === 'e') {
Expand Down
102 changes: 95 additions & 7 deletions js/multipage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
'use strict';

let parseSpecPath = url => {
let pathParts = url.pathname.split('/');
let isMultipage = pathParts[pathParts.length - 2] === 'multipage';
let pathPrefixEnd = isMultipage ? -2 : pathParts.findLastIndex(part => part !== '') + 1;
let pathPrefix = pathParts.slice(0, pathPrefixEnd).join('/');
return { pathParts, pathPrefix, isMultipage };
};

// initialize globals
let idToSection = Object.create(null);
for (let [section, ids] of Object.entries(multipageMap)) {
for (let id of ids) {
Expand All @@ -7,13 +17,91 @@ for (let [section, ids] of Object.entries(multipageMap)) {
}
}
}
if (location.hash) {
let targetSec = idToSection[location.hash.substring(1)];
if (targetSec != null) {
let match = location.pathname.match(/([^/]+)\.html?$/);
if ((match != null && match[1] !== targetSec) || location.pathname.endsWith('/multipage/')) {
window.navigating = true;
location = (targetSec === 'index' ? './' : targetSec + '.html') + location.hash;
let { pathParts, pathPrefix, isMultipage } = parseSpecPath(location);
let activeSec = isMultipage ? pathParts[pathParts.length - 1].replace(/\.html$/, '') : undefined;
let activeSecHash =
activeSec && idToSection['sec-' + activeSec] != null ? '#sec-' + activeSec : undefined;
let storage = typeof localStorage !== 'undefined' ? localStorage : Object.create(null);
let toggleMultipage = () => {
let hash = location.hash;
if (isMultipage) {
location = pathParts.slice(0, -2).join('/') + '/' + (hash || activeSecHash || '');
} else {
let targetSec = hash ? idToSection[hash.substring(1)] : undefined;
location = 'multipage/' + (targetSec ? targetSec + '.html' : '') + hash;
}
};

// redirect to single-page/multi-page per preference
(() => {
// ...except from internal links
let referrer;
try {
referrer = new URL(document.referrer);
} catch (_err) {
// ignore
}
if (referrer && referrer.host === location.host) {
if (parseSpecPath(referrer).pathPrefix === pathPrefix) return;
}

let resolvedHash = location.hash || activeSecHash || '';
let targetSec = resolvedHash ? idToSection[resolvedHash.substring(1)] : undefined;
let multipagePreference = storage.multipagePreference;
if (isMultipage && multipagePreference === 'single-page') {
window.navigating = true;
location = pathParts.slice(0, -2).join('/') + '/' + resolvedHash;
} else if (
isMultipage
? targetSec != null && (activeSec || 'index') !== targetSec
: multipagePreference === 'multi-page'
) {
window.navigating = true;
location = 'multipage/' + (targetSec ? targetSec + '.html' : '') + location.hash;
}
})();

// enable preference togglers
document.documentElement.dataset.multipagePreference = storage.multipagePreference || '';
if (typeof localStorage !== 'undefined') {
let enableToggles = () => {
for (let el of document.querySelectorAll('[disabled][data-multipage-preference]')) {
el.disabled = false;
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', enableToggles);
} else {
enableToggles();
}
document.addEventListener('click', e => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just listen on the fieldset that contains the three [data-multipage-preference] elements, or on the elements themselves?

if (!(e.target instanceof HTMLElement)) {
return;
}
let target = e.target;
let toggler = target.closest('[data-multipage-preference]');
if (target.isContentEditable || !toggler || toggler === document.documentElement) {
return;
}
switch (target.nodeName.toLowerCase()) {
case 'textarea':
case 'select':
return;
case 'input': {
let isCheckable = target.type === 'checkbox' || target.type === 'radio';
if (toggler !== target || !isCheckable || !target.checked) {
return;
}
}
}
let multipagePreference = toggler.dataset.multipagePreference;
if (multipagePreference !== (storage.multipagePreference || '')) {
storage.multipagePreference = multipagePreference;
document.documentElement.dataset.multipagePreference = multipagePreference;
if (multipagePreference === (isMultipage ? 'single-page' : 'multi-page')) {
toggleMultipage();
}
}
e.preventDefault();
});
}
6 changes: 6 additions & 0 deletions spec/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ <h1>Stylesheets and other assets</h1>
<p>Ecmarkup requires CSS styles and other assets. By default all assets are inlined into the document. You can override this by setting assets to “none” (for example if you want to manually link to external assets) or “external”. When using “external” the default directory for assets is `assets` in the same directory as the output file, but you can override this with `--assets-dir`.</p>
</emu-clause>

<emu-clause id="multipage">
<h1>Multipage</h1>
<p>Multi-page builds support a sticky preference for accessing the resulting document as a single page or as multiple pages. It can be set by embedding elements (such as <code>&lt;a&gt;</code> or <code>&lt;button&gt;</code>) with attribute <code>data-multipage-preference</code> set to either an empty string (for the default lack of preference), "single-page", or "multi-page". When such an element is clicked, the corresponding preference is written into <code>localStorage</code> and a navigation is triggered if necessary. From that point forward, loading any page directly or from an external link will respect that preference and redirect as necessary.</p>
<emu-note>Links <em>internal</em> to the document are not subject to such redirection, allowing free alternation between single-page and multi-page experiences.</emu-note>
</emu-clause>

<emu-clause id="editorial-conventions">
<h1>Editorial Conventions</h1>
<p>There are a large number of features in Ecmarkup. Detailed documentation can be found in later sections. This section provides a high-level overview of what capabilities are available and when to use them.</p>
Expand Down
20 changes: 13 additions & 7 deletions src/Spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,8 +687,7 @@ export default class Spec {
this.doc.body.insertBefore(ele, this.doc.body.firstChild);
}

const jsContents =
(await concatJs(sdoJs, tocJs)) + `\n;let usesMultipage = ${!!this.opts.multipage}`;
const jsContents = await concatJs(sdoJs, tocJs);
const jsSha = sha(jsContents);

await this.buildAssets(jsContents, jsSha);
Expand Down Expand Up @@ -1010,8 +1009,6 @@ export default class Spec {
htmlEle = src.substring(0, src.length - '<head></head><body></body></html>'.length);
}

const head = this.doc.head.cloneNode(true) as HTMLHeadElement;

const containedMap = JSON.stringify(Object.fromEntries(sectionToContainedIds)).replace(
/[\\`$]/g,
'\\$&',
Expand All @@ -1035,10 +1032,13 @@ ${await utils.readFile(path.join(__dirname, '../js/multipage.js'))}
path.relative(this.opts.outfile!, multipageLocationOnDisk) +
'?cache=' +
sha(multipageJsContents);
multipageScript.setAttribute('defer', '');
head.insertBefore(multipageScript, head.querySelector('script'));
// fetch in parallel, but evaluate ASAP in case of redirect
multipageScript.setAttribute('async', '');
this.doc.head.insertBefore(multipageScript, this.doc.head.querySelector('script'));
}

const head = this.doc.head.cloneNode(true) as HTMLHeadElement;

for (const { name, eles } of sections) {
this.log(`Generating section ${name}...`);
const headClone = head.cloneNode(true) as HTMLHeadElement;
Expand Down Expand Up @@ -1335,7 +1335,13 @@ ${this.opts.multipage ? `<li><span>Navigate to/from multipage</span><code>m</cod
<li><span>Jump to the <i>n</i><sup>th</sup> pin</span><code>1-9</code></li>
<li><span>Jump to the 10<sup>th</sup> pin</span><code>0</code></li>
<li><span>Jump to the most recent link target</span><code>\`</code></li>
</ul>`;
</ul>
<fieldset>
<legend>Multipage preference</legend>
<button type="button" data-multipage-preference="">No preference</button>
<button type="button" data-multipage-preference="single-page">Always single-page</button>
<button type="button" data-multipage-preference="multi-page">Always multi-page</button>
</fieldset>`;
return shortcutsHelp;
}

Expand Down
8 changes: 7 additions & 1 deletion test/baselines/generated-reference/abstract-methods.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
<li><span>Jump to the <i>n</i><sup>th</sup> pin</span><code>1-9</code></li>
<li><span>Jump to the 10<sup>th</sup> pin</span><code>0</code></li>
<li><span>Jump to the most recent link target</span><code>`</code></li>
</ul></div><div id="spec-container">
</ul>
<fieldset>
<legend>Multipage preference</legend>
<button type="button" data-multipage-preference="">No preference</button>
<button type="button" data-multipage-preference="single-page">Always single-page</button>
<button type="button" data-multipage-preference="multi-page">Always multi-page</button>
</fieldset></div><div id="spec-container">
<emu-table type="abstract methods" of="Module Record" id="table-abstract-methods-of-module-records" caption="Abstract Methods of Module Record"><figure><figcaption>Table 1: Abstract Methods of <emu-xref href="#sec-abstract-module-records"><a href="https://tc39.es/ecma262/#sec-abstract-module-records">Module Record</a></emu-xref></figcaption>
<table>
<thead>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
<li><span>Jump to the <i>n</i><sup>th</sup> pin</span><code>1-9</code></li>
<li><span>Jump to the 10<sup>th</sup> pin</span><code>0</code></li>
<li><span>Jump to the most recent link target</span><code>`</code></li>
</ul></div><div id="spec-container">
</ul>
<fieldset>
<legend>Multipage preference</legend>
<button type="button" data-multipage-preference="">No preference</button>
<button type="button" data-multipage-preference="single-page">Always single-page</button>
<button type="button" data-multipage-preference="multi-page">Always multi-page</button>
</fieldset></div><div id="spec-container">

<emu-clause id="test">
<h1><span class="secnum">1</span> Title</h1>
Expand Down
8 changes: 7 additions & 1 deletion test/baselines/generated-reference/algorithms.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
<li><span>Jump to the <i>n</i><sup>th</sup> pin</span><code>1-9</code></li>
<li><span>Jump to the 10<sup>th</sup> pin</span><code>0</code></li>
<li><span>Jump to the most recent link target</span><code>`</code></li>
</ul></div><div id="spec-container">
</ul>
<fieldset>
<legend>Multipage preference</legend>
<button type="button" data-multipage-preference="">No preference</button>
<button type="button" data-multipage-preference="single-page">Always single-page</button>
<button type="button" data-multipage-preference="multi-page">Always multi-page</button>
</fieldset></div><div id="spec-container">

<emu-alg><ol><li>Can call <emu-xref href="#sec-algorithm-conventions-abstract-operations"><a href="https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations">abstract operations</a></emu-xref> in this spec: <emu-xref aoid="Internal" id="_ref_0"><a href="#sec-internal">Internal</a></emu-xref>();</li><li>Can call <emu-xref href="#sec-algorithm-conventions-abstract-operations"><a href="https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations">abstract operations</a></emu-xref> in ES6: <emu-xref aoid="ReturnIfAbrupt"><a href="https://tc39.es/ecma262/#sec-returnifabrupt">ReturnIfAbrupt</a></emu-xref>(<var>completion</var>);</li><li>Can call <emu-xref href="#sec-algorithm-conventions-abstract-operations"><a href="https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations">abstract operations</a></emu-xref> in a biblio file: <emu-xref aoid="Biblio"><a href="http://example.com/fooSite.html#sec-biblio">Biblio</a></emu-xref>();</li><li>Unfound <emu-xref href="#sec-algorithm-conventions-abstract-operations"><a href="https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations">abstract operations</a></emu-xref> just don't link: Unfound();</li><li>Can prefix with !&nbsp;and ?.<ol><li class="exit">Let <var>foo</var> be ?&nbsp;<emu-xref aoid="Internal" id="_ref_1"><a href="#sec-internal">Internal</a></emu-xref>();</li><li>Set <var>foo</var> to !&nbsp;<emu-xref aoid="Internal" id="_ref_2"><a href="#sec-internal">Internal</a></emu-xref>();</li><li>Set <var>foo</var> to !&nbsp;SDO of <var>operation</var>.</li><li>Set <var>foo</var> to !&nbsp;<var>operation</var>.<var class="field">[[MOP]]</var>().</li></ol></li><li>A <emu-xref href="#sec-list-and-record-specification-type"><a href="https://tc39.es/ecma262/#sec-list-and-record-specification-type">Record</a></emu-xref> looks like this: <emu-xref href="#sec-list-and-record-specification-type"><a href="https://tc39.es/ecma262/#sec-list-and-record-specification-type">Record</a></emu-xref> { <var class="field">[[Key]]</var>: 0&nbsp;}.</li><li>A <emu-xref href="#sec-list-and-record-specification-type"><a href="https://tc39.es/ecma262/#sec-list-and-record-specification-type">List</a></emu-xref> looks like this: « 0, 1&nbsp;».</li></ol></emu-alg>

Expand Down
49 changes: 25 additions & 24 deletions test/baselines/generated-reference/assets-inline.html
Original file line number Diff line number Diff line change
Expand Up @@ -1153,11 +1153,11 @@

function makeLinkToId(id) {
let hash = '#' + id;
if (typeof idToSection === 'undefined' || !idToSection[id]) {
return hash;
if (typeof isMultipage !== 'undefined' && isMultipage && idToSection[id]) {
let targetSec = idToSection[id];
return (targetSec === 'index' ? './' : targetSec + '.html') + hash;
}
let targetSec = idToSection[id];
return (targetSec === 'index' ? './' : targetSec + '.html') + hash;
return hash;
}

function doShortcut(e) {
Expand All @@ -1172,23 +1172,8 @@
if (e.altKey || e.ctrlKey || e.metaKey) {
return;
}
if (e.key === 'm' && usesMultipage) {
let pathParts = location.pathname.split('/');
let hash = location.hash;
if (pathParts[pathParts.length - 2] === 'multipage') {
if (hash === '') {
let sectionName = pathParts[pathParts.length - 1];
if (sectionName.endsWith('.html')) {
sectionName = sectionName.slice(0, -5);
}
if (idToSection['sec-' + sectionName] !== undefined) {
hash = '#sec-' + sectionName;
}
}
location = pathParts.slice(0, -2).join('/') + '/' + hash;
} else {
location = 'multipage/' + hash;
}
if (e.key === 'm' && typeof toggleMultipage !== 'undefined') {
toggleMultipage();
} else if (e.key === 'u') {
document.documentElement.classList.toggle('show-uc-annotations');
} else if (e.key === 'e') {
Expand Down Expand Up @@ -1612,8 +1597,7 @@
});

let sdoMap = JSON.parse(`{}`);
let biblio = JSON.parse(`{"refsByClause":{},"entries":[{"type":"clause","id":"sec-intro","titleHTML":"Intro","number":""},{"type":"clause","id":"sec-copyright-and-software-license","title":"Copyright & Software License","titleHTML":"Copyright &amp; Software License","number":"A"}]}`);
;let usesMultipage = false</script><style>:root {
let biblio = JSON.parse(`{"refsByClause":{},"entries":[{"type":"clause","id":"sec-intro","titleHTML":"Intro","number":""},{"type":"clause","id":"sec-copyright-and-software-license","title":"Copyright & Software License","titleHTML":"Copyright &amp; Software License","number":"A"}]}`);</script><style>:root {
--foreground-color: #111;
--background-color: #fff;

Expand Down Expand Up @@ -3290,7 +3274,18 @@
background: var(--figure-background);
width: 500px;
}

/* Other dynamic elements */
:root[data-multipage-preference=''] [data-multipage-preference=''],
:root[data-multipage-preference='single-page'] [data-multipage-preference='single-page'],
:root[data-multipage-preference='multi-page'] [data-multipage-preference='multi-page'] {
font-weight: bold;
}
</style><style>@media print {
.no-print {
display: none !important;
}

@font-face {
font-family: 'Arial Plus';
src: local('Arial');
Expand Down Expand Up @@ -4063,7 +4058,13 @@
<li><span>Jump to the <i>n</i><sup>th</sup> pin</span><code>1-9</code></li>
<li><span>Jump to the 10<sup>th</sup> pin</span><code>0</code></li>
<li><span>Jump to the most recent link target</span><code>`</code></li>
</ul></div><div id="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%; stroke:currentColor" viewBox="0 0 120 120" width="54" height="54">
</ul>
<fieldset>
<legend>Multipage preference</legend>
<button type="button" data-multipage-preference="">No preference</button>
<button type="button" data-multipage-preference="single-page">Always single-page</button>
<button type="button" data-multipage-preference="multi-page">Always multi-page</button>
</fieldset></div><div id="menu-toggle"><svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%; stroke:currentColor" viewBox="0 0 120 120" width="54" height="54">
<title>Menu</title>
<path stroke-width="10" stroke-linecap="round" d="M30,60 h60 M30,30 m0,5 h60 M30,90 m0,-5 h60"></path>
</svg></div><div id="menu-spacer" class="menu-spacer"></div><div id="menu"><div id="menu-search"><input type="text" id="menu-search-box" placeholder="Search..."><div id="menu-search-results" class="inactive"></div></div><div id="menu-pins"><div class="menu-pane-header">Pins<button class="unpin-all">clear</button></div><ul id="menu-pins-list"></ul></div><div class="menu-pane-header">Table of Contents</div><div id="menu-toc"><ol class="toc"><li><span class="item-toggle-none"></span><a href="#sec-intro" title="Intro">Intro</a></li><li><span class="item-toggle-none"></span><a href="#sec-copyright-and-software-license" title="Copyright &amp; Software License">Copyright &amp; Software License</a></li></ol></div></div><div id="spec-container">
Expand Down
Loading
Loading