ecmarkup
Version:
Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.
1,280 lines (1,119 loc) • 37.6 kB
JavaScript
'use strict';
function Search(menu) {
this.menu = menu;
this.$search = document.getElementById('menu-search');
this.$searchBox = document.getElementById('menu-search-box');
this.$searchResults = document.getElementById('menu-search-results');
this.loadBiblio();
document.addEventListener('keydown', this.documentKeydown.bind(this));
this.$searchBox.addEventListener(
'keydown',
debounce(this.searchBoxKeydown.bind(this), { stopPropagation: true }),
);
this.$searchBox.addEventListener(
'keyup',
debounce(this.searchBoxKeyup.bind(this), { stopPropagation: true }),
);
// Perform an initial search if the box is not empty.
if (this.$searchBox.value) {
this.search(this.$searchBox.value);
}
}
Search.prototype.loadBiblio = function () {
if (typeof biblio === 'undefined') {
console.error('could not find biblio');
this.biblio = { refToClause: {}, entries: [] };
} else {
this.biblio = biblio;
this.biblio.clauses = this.biblio.entries.filter(e => e.type === 'clause');
this.biblio.byId = this.biblio.entries.reduce((map, entry) => {
map[entry.id] = entry;
return map;
}, {});
let refParentClause = Object.create(null);
this.biblio.refParentClause = refParentClause;
let refsByClause = this.biblio.refsByClause;
Object.keys(refsByClause).forEach(clause => {
refsByClause[clause].forEach(ref => {
refParentClause[ref] = clause;
});
});
}
};
Search.prototype.documentKeydown = function (e) {
if (e.key === '/') {
e.preventDefault();
e.stopPropagation();
this.triggerSearch();
}
};
Search.prototype.searchBoxKeydown = function (e) {
e.stopPropagation();
e.preventDefault();
if (e.keyCode === 191 && e.target.value.length === 0) {
e.preventDefault();
} else if (e.keyCode === 13) {
e.preventDefault();
this.selectResult();
}
};
Search.prototype.searchBoxKeyup = function (e) {
if (e.keyCode === 13 || e.keyCode === 9) {
return;
}
this.search(e.target.value);
};
Search.prototype.triggerSearch = function () {
if (this.menu.isVisible()) {
this._closeAfterSearch = false;
} else {
this._closeAfterSearch = true;
this.menu.show();
}
this.$searchBox.focus();
this.$searchBox.select();
};
// bit 12 - Set if the result starts with searchString
// bits 8-11: 8 - number of chunks multiplied by 2 if cases match, otherwise 1.
// bits 1-7: 127 - length of the entry
// General scheme: prefer case sensitive matches with fewer chunks, and otherwise
// prefer shorter matches.
function relevance(result) {
let relevance = 0;
relevance = Math.max(0, 8 - result.match.chunks) << 7;
if (result.match.caseMatch) {
relevance *= 2;
}
if (result.match.prefix) {
relevance += 2048;
}
relevance += Math.max(0, 255 - result.key.length);
return relevance;
}
Search.prototype.search = function (searchString) {
if (searchString === '') {
this.displayResults([]);
this.hideSearch();
return;
} else {
this.showSearch();
}
if (searchString.length === 1) {
this.displayResults([]);
return;
}
let results;
if (/^[\d.]*$/.test(searchString)) {
results = this.biblio.clauses
.filter(clause => clause.number.substring(0, searchString.length) === searchString)
.map(clause => ({ key: getKey(clause), entry: clause }));
} else {
results = [];
for (let i = 0; i < this.biblio.entries.length; i++) {
let entry = this.biblio.entries[i];
let key = getKey(entry);
if (!key) {
// biblio entries without a key aren't searchable
continue;
}
let match = fuzzysearch(searchString, key);
if (match) {
results.push({ key, entry, match });
}
}
results.forEach(result => {
result.relevance = relevance(result, searchString);
});
results = results.sort((a, b) => b.relevance - a.relevance);
}
if (results.length > 50) {
results = results.slice(0, 50);
}
this.displayResults(results);
};
Search.prototype.hideSearch = function () {
this.$search.classList.remove('active');
};
Search.prototype.showSearch = function () {
this.$search.classList.add('active');
};
Search.prototype.selectResult = function () {
let $first = this.$searchResults.querySelector('li:first-child a');
if ($first) {
document.location = $first.getAttribute('href');
}
this.$searchBox.value = '';
this.$searchBox.blur();
this.displayResults([]);
this.hideSearch();
if (this._closeAfterSearch) {
this.menu.hide();
}
};
Search.prototype.displayResults = function (results) {
if (results.length > 0) {
this.$searchResults.classList.remove('no-results');
let html = '<ul>';
results.forEach(result => {
let key = result.key;
let entry = result.entry;
let id = entry.id;
let cssClass = '';
let text = '';
if (entry.type === 'clause') {
let number = entry.number ? entry.number + ' ' : '';
text = number + key;
cssClass = 'clause';
id = entry.id;
} else if (entry.type === 'production') {
text = key;
cssClass = 'prod';
id = entry.id;
} else if (entry.type === 'op') {
text = key;
cssClass = 'op';
id = entry.id || entry.refId;
} else if (entry.type === 'term') {
text = key;
cssClass = 'term';
id = entry.id || entry.refId;
}
if (text) {
html += `<li class=menu-search-result-${cssClass}><a href="${makeLinkToId(id)}">${text}</a></li>`;
}
});
html += '</ul>';
this.$searchResults.innerHTML = html;
} else {
this.$searchResults.innerHTML = '';
this.$searchResults.classList.add('no-results');
}
};
function getKey(item) {
if (item.key) {
return item.key;
}
switch (item.type) {
case 'clause':
return item.title || item.titleHTML;
case 'production':
return item.name;
case 'op':
return item.aoid;
case 'term':
return item.term;
case 'table':
case 'figure':
case 'example':
case 'note':
return item.caption;
case 'step':
return item.id;
default:
throw new Error("Can't get key for " + item.type);
}
}
function Menu() {
this.$toggle = document.getElementById('menu-toggle');
this.$menu = document.getElementById('menu');
this.$toc = document.querySelector('menu-toc > ol');
this.$pins = document.querySelector('#menu-pins');
this.$pinList = document.getElementById('menu-pins-list');
this.$toc = document.querySelector('#menu-toc > ol');
this.$specContainer = document.getElementById('spec-container');
this.search = new Search(this);
this._pinnedIds = {};
this.loadPinEntries();
// unpin all button
document
.querySelector('#menu-pins .unpin-all')
.addEventListener('click', this.unpinAll.bind(this));
// individual unpinning buttons
this.$pinList.addEventListener('click', this.pinListClick.bind(this));
// toggle menu
this.$toggle.addEventListener('click', this.toggle.bind(this));
// keydown events for pinned clauses
document.addEventListener('keydown', this.documentKeydown.bind(this));
// toc expansion
let tocItems = this.$menu.querySelectorAll('#menu-toc li');
for (let i = 0; i < tocItems.length; i++) {
let $item = tocItems[i];
$item.addEventListener('click', event => {
$item.classList.toggle('active');
event.stopPropagation();
});
}
// close toc on toc item selection
let tocLinks = this.$menu.querySelectorAll('#menu-toc li > a');
for (let i = 0; i < tocLinks.length; i++) {
let $link = tocLinks[i];
$link.addEventListener('click', event => {
this.toggle();
event.stopPropagation();
});
}
// update active clause on scroll
window.addEventListener('scroll', debounce(this.updateActiveClause.bind(this)));
this.updateActiveClause();
// prevent menu scrolling from scrolling the body
this.$toc.addEventListener('wheel', e => {
let target = e.currentTarget;
let offTop = e.deltaY < 0 && target.scrollTop === 0;
if (offTop) {
e.preventDefault();
}
let offBottom = e.deltaY > 0 && target.offsetHeight + target.scrollTop >= target.scrollHeight;
if (offBottom) {
e.preventDefault();
}
});
}
Menu.prototype.documentKeydown = function (e) {
e.stopPropagation();
if (e.key === 'p') {
this.togglePinEntry();
} else if ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].includes(parseInt(e.key))) {
this.selectPin((e.keyCode - 9) % 10);
} else if (e.key === '`') {
const hash = document.location.hash;
const id = decodeURIComponent(hash.slice(1));
const target = document.getElementById(id);
target?.scrollIntoView(true);
}
};
Menu.prototype.updateActiveClause = function () {
this.setActiveClause(findActiveClause(this.$specContainer));
};
Menu.prototype.setActiveClause = function (clause) {
this.$activeClause = clause;
this.revealInToc(this.$activeClause);
};
Menu.prototype.revealInToc = function (path) {
let current = this.$toc.querySelectorAll('li.revealed');
for (let i = 0; i < current.length; i++) {
current[i].classList.remove('revealed');
current[i].classList.remove('revealed-leaf');
}
current = this.$toc;
let index = 0;
outer: while (index < path.length) {
let children = current.children;
for (let i = 0; i < children.length; i++) {
if ('#' + path[index].id === children[i].children[1].hash) {
children[i].classList.add('revealed');
if (index === path.length - 1) {
children[i].classList.add('revealed-leaf');
let rect = children[i].getBoundingClientRect();
// this.$toc.getBoundingClientRect().top;
let tocRect = this.$toc.getBoundingClientRect();
if (rect.top + 10 > tocRect.bottom) {
this.$toc.scrollTop =
this.$toc.scrollTop + (rect.top - tocRect.bottom) + (rect.bottom - rect.top);
} else if (rect.top < tocRect.top) {
this.$toc.scrollTop = this.$toc.scrollTop - (tocRect.top - rect.top);
}
}
current = children[i].querySelector('ol');
index++;
continue outer;
}
}
console.log('could not find location in table of contents', path);
break;
}
};
function findActiveClause(root, path) {
path = path || [];
let visibleClauses = getVisibleClauses(root, path);
let midpoint = Math.floor(window.innerHeight / 2);
for (let [$clause, path] of visibleClauses) {
let { top: clauseTop, bottom: clauseBottom } = $clause.getBoundingClientRect();
let isFullyVisibleAboveTheFold =
clauseTop > 0 && clauseTop < midpoint && clauseBottom < window.innerHeight;
if (isFullyVisibleAboveTheFold) {
return path;
}
}
visibleClauses.sort(([, pathA], [, pathB]) => pathB.length - pathA.length);
for (let [$clause, path] of visibleClauses) {
let { top: clauseTop, bottom: clauseBottom } = $clause.getBoundingClientRect();
let $header = $clause.querySelector('h1');
let clauseStyles = getComputedStyle($clause);
let marginTop = Math.max(
0,
parseInt(clauseStyles['margin-top']),
parseInt(getComputedStyle($header)['margin-top']),
);
let marginBottom = Math.max(0, parseInt(clauseStyles['margin-bottom']));
let crossesMidpoint =
clauseTop - marginTop <= midpoint && clauseBottom + marginBottom >= midpoint;
if (crossesMidpoint) {
return path;
}
}
return path;
}
function getVisibleClauses(root, path) {
let childClauses = getChildClauses(root);
path = path || [];
let result = [];
let seenVisibleClause = false;
for (let $clause of childClauses) {
let { top: clauseTop, bottom: clauseBottom } = $clause.getBoundingClientRect();
let isPartiallyVisible =
(clauseTop > 0 && clauseTop < window.innerHeight) ||
(clauseBottom > 0 && clauseBottom < window.innerHeight) ||
(clauseTop < 0 && clauseBottom > window.innerHeight);
if (isPartiallyVisible) {
seenVisibleClause = true;
let innerPath = path.concat($clause);
result.push([$clause, innerPath]);
result.push(...getVisibleClauses($clause, innerPath));
} else if (seenVisibleClause) {
break;
}
}
return result;
}
function* getChildClauses(root) {
for (let el of root.children) {
switch (el.nodeName) {
// descend into <emu-import>
case 'EMU-IMPORT':
yield* getChildClauses(el);
break;
// accept <emu-clause>, <emu-intro>, and <emu-annex>
case 'EMU-CLAUSE':
case 'EMU-INTRO':
case 'EMU-ANNEX':
yield el;
}
}
}
Menu.prototype.toggle = function () {
this.$menu.classList.toggle('active');
};
Menu.prototype.show = function () {
this.$menu.classList.add('active');
};
Menu.prototype.hide = function () {
this.$menu.classList.remove('active');
};
Menu.prototype.isVisible = function () {
return this.$menu.classList.contains('active');
};
Menu.prototype.showPins = function () {
this.$pins.classList.add('active');
};
Menu.prototype.hidePins = function () {
this.$pins.classList.remove('active');
};
Menu.prototype.addPinEntry = function (id) {
let entry = this.search.biblio.byId[id];
if (!entry) {
// id was deleted after pin (or something) so remove it
delete this._pinnedIds[id];
this.persistPinEntries();
return;
}
let text;
if (entry.type === 'clause') {
let prefix;
if (entry.number) {
prefix = entry.number + ' ';
} else {
prefix = '';
}
text = `${prefix}${entry.titleHTML}`;
} else {
text = getKey(entry);
}
let link = `<a href="${makeLinkToId(entry.id)}">${text}</a>`;
this.$pinList.innerHTML += `<li data-section-id="${id}">${link}<button class="unpin">\u{2716}</button></li>`;
if (Object.keys(this._pinnedIds).length === 0) {
this.showPins();
}
this._pinnedIds[id] = true;
this.persistPinEntries();
};
Menu.prototype.removePinEntry = function (id) {
let item = this.$pinList.querySelector(`li[data-section-id="${id}"]`);
this.$pinList.removeChild(item);
delete this._pinnedIds[id];
if (Object.keys(this._pinnedIds).length === 0) {
this.hidePins();
}
this.persistPinEntries();
};
Menu.prototype.unpinAll = function () {
for (let id of Object.keys(this._pinnedIds)) {
this.removePinEntry(id);
}
};
Menu.prototype.pinListClick = function (event) {
if (event?.target?.classList.contains('unpin')) {
let id = event.target.parentNode.dataset.sectionId;
if (id) {
this.removePinEntry(id);
}
}
};
Menu.prototype.persistPinEntries = function () {
try {
if (!window.localStorage) return;
} catch (e) {
return;
}
localStorage.pinEntries = JSON.stringify(Object.keys(this._pinnedIds));
};
Menu.prototype.loadPinEntries = function () {
try {
if (!window.localStorage) return;
} catch (e) {
return;
}
let pinsString = window.localStorage.pinEntries;
if (!pinsString) return;
let pins = JSON.parse(pinsString);
for (let i = 0; i < pins.length; i++) {
this.addPinEntry(pins[i]);
}
};
Menu.prototype.togglePinEntry = function (id) {
if (!id) {
id = this.$activeClause[this.$activeClause.length - 1].id;
}
if (this._pinnedIds[id]) {
this.removePinEntry(id);
} else {
this.addPinEntry(id);
}
};
Menu.prototype.selectPin = function (num) {
if (num >= this.$pinList.children.length) return;
document.location = this.$pinList.children[num].children[0].href;
};
let menu;
document.addEventListener('DOMContentLoaded', init);
function debounce(fn, opts) {
opts = opts || {};
let timeout;
return function (e) {
if (opts.stopPropagation) {
e.stopPropagation();
}
let args = arguments;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
timeout = null;
fn.apply(this, args);
}, 150);
};
}
let CLAUSE_NODES = ['EMU-CLAUSE', 'EMU-INTRO', 'EMU-ANNEX'];
function findContainer($elem) {
let parentClause = $elem.parentNode;
while (parentClause && CLAUSE_NODES.indexOf(parentClause.nodeName) === -1) {
parentClause = parentClause.parentNode;
}
return parentClause;
}
function findLocalReferences(parentClause, name) {
let vars = parentClause.querySelectorAll('var');
let references = [];
for (let i = 0; i < vars.length; i++) {
let $var = vars[i];
if ($var.innerHTML === name) {
references.push($var);
}
}
return references;
}
let REFERENCED_CLASSES = Array.from({ length: 7 }, (x, i) => `referenced${i}`);
function chooseHighlightIndex(parentClause) {
let counts = REFERENCED_CLASSES.map($class => parentClause.getElementsByClassName($class).length);
// Find the earliest index with the lowest count.
let minCount = Infinity;
let index = null;
for (let i = 0; i < counts.length; i++) {
if (counts[i] < minCount) {
minCount = counts[i];
index = i;
}
}
return index;
}
function toggleFindLocalReferences($elem) {
let parentClause = findContainer($elem);
let references = findLocalReferences(parentClause, $elem.innerHTML);
if ($elem.classList.contains('referenced')) {
references.forEach($reference => {
$reference.classList.remove('referenced', ...REFERENCED_CLASSES);
});
} else {
let index = chooseHighlightIndex(parentClause);
references.forEach($reference => {
$reference.classList.add('referenced', `referenced${index}`);
});
}
}
function installFindLocalReferences() {
document.addEventListener('click', e => {
if (e.target.nodeName === 'VAR') {
toggleFindLocalReferences(e.target);
}
});
}
document.addEventListener('DOMContentLoaded', installFindLocalReferences);
// The following license applies to the fuzzysearch function
// The MIT License (MIT)
// Copyright © 2015 Nicolas Bevacqua
// Copyright © 2016 Brian Terlson
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
function fuzzysearch(searchString, haystack, caseInsensitive) {
let tlen = haystack.length;
let qlen = searchString.length;
let chunks = 1;
let finding = false;
if (qlen > tlen) {
return false;
}
if (qlen === tlen) {
if (searchString === haystack) {
return { caseMatch: true, chunks: 1, prefix: true };
} else if (searchString.toLowerCase() === haystack.toLowerCase()) {
return { caseMatch: false, chunks: 1, prefix: true };
} else {
return false;
}
}
let j = 0;
outer: for (let i = 0; i < qlen; i++) {
let nch = searchString[i];
while (j < tlen) {
let targetChar = haystack[j++];
if (targetChar === nch) {
finding = true;
continue outer;
}
if (finding) {
chunks++;
finding = false;
}
}
if (caseInsensitive) {
return false;
}
return fuzzysearch(searchString.toLowerCase(), haystack.toLowerCase(), true);
}
return { caseMatch: !caseInsensitive, chunks, prefix: j <= qlen };
}
let referencePane = {
init() {
this.$container = document.createElement('div');
this.$container.setAttribute('id', 'references-pane-container');
let $spacer = document.createElement('div');
$spacer.setAttribute('id', 'references-pane-spacer');
$spacer.classList.add('menu-spacer');
this.$pane = document.createElement('div');
this.$pane.setAttribute('id', 'references-pane');
this.$container.appendChild($spacer);
this.$container.appendChild(this.$pane);
this.$header = document.createElement('div');
this.$header.classList.add('menu-pane-header');
this.$headerText = document.createElement('span');
this.$header.appendChild(this.$headerText);
this.$headerRefId = document.createElement('a');
this.$header.appendChild(this.$headerRefId);
this.$header.addEventListener('pointerdown', e => {
this.dragStart(e);
});
this.$closeButton = document.createElement('span');
this.$closeButton.setAttribute('id', 'references-pane-close');
this.$closeButton.addEventListener('click', () => {
this.deactivate();
});
this.$header.appendChild(this.$closeButton);
this.$pane.appendChild(this.$header);
this.$tableContainer = document.createElement('div');
this.$tableContainer.setAttribute('id', 'references-pane-table-container');
this.$table = document.createElement('table');
this.$table.setAttribute('id', 'references-pane-table');
this.$tableBody = this.$table.createTBody();
this.$tableContainer.appendChild(this.$table);
this.$pane.appendChild(this.$tableContainer);
if (menu != null) {
menu.$specContainer.appendChild(this.$container);
}
},
activate() {
this.$container.classList.add('active');
},
deactivate() {
this.$container.classList.remove('active');
this.state = null;
},
showReferencesFor(entry) {
this.activate();
this.state = { type: 'ref', id: entry.id };
this.$headerText.textContent = 'References to ';
let newBody = document.createElement('tbody');
let previousId;
let previousCell;
let dupCount = 0;
this.$headerRefId.innerHTML = getKey(entry);
this.$headerRefId.setAttribute('href', makeLinkToId(entry.id));
this.$headerRefId.style.display = 'inline';
(entry.referencingIds || [])
.map(id => {
let cid = menu.search.biblio.refParentClause[id];
let clause = menu.search.biblio.byId[cid];
if (clause == null) {
throw new Error('could not find clause for id ' + cid);
}
return { id, clause };
})
.sort((a, b) => sortByClauseNumber(a.clause, b.clause))
.forEach(record => {
if (previousId === record.clause.id) {
previousCell.innerHTML += ` (<a href="${makeLinkToId(record.id)}">${dupCount + 2}</a>)`;
dupCount++;
} else {
let row = newBody.insertRow();
let cell = row.insertCell();
cell.innerHTML = record.clause.number;
cell = row.insertCell();
cell.innerHTML = `<a href="${makeLinkToId(record.id)}">${record.clause.titleHTML}</a>`;
previousCell = cell;
previousId = record.clause.id;
dupCount = 0;
}
}, this);
this.$table.removeChild(this.$tableBody);
this.$tableBody = newBody;
this.$table.appendChild(this.$tableBody);
this.autoSize();
},
showSDOs(sdos, alternativeId) {
let rhs = document.getElementById(alternativeId);
let parentName = rhs.parentNode.getAttribute('name');
let colons = rhs.parentNode.querySelector('emu-geq');
rhs = rhs.cloneNode(true);
rhs.querySelectorAll('emu-params,emu-constraints').forEach(e => {
e.remove();
});
rhs.querySelectorAll('[id]').forEach(e => {
e.removeAttribute('id');
});
rhs.querySelectorAll('a').forEach(e => {
e.parentNode.replaceChild(document.createTextNode(e.textContent), e);
});
this.$headerText.innerHTML = `Syntax-Directed Operations for<br><a href="${makeLinkToId(alternativeId)}" class="menu-pane-header-production"><emu-nt>${parentName}</emu-nt> ${colons.outerHTML} </a>`;
this.$headerText.querySelector('a').append(rhs);
this.showSDOsBody(sdos, alternativeId);
},
showSDOsBody(sdos, alternativeId) {
this.activate();
this.state = { type: 'sdo', id: alternativeId, html: this.$headerText.innerHTML };
this.$headerRefId.style.display = 'none';
let newBody = document.createElement('tbody');
Object.keys(sdos).forEach(sdoName => {
let pair = sdos[sdoName];
let clause = pair.clause;
let ids = pair.ids;
let first = ids[0];
let row = newBody.insertRow();
let cell = row.insertCell();
cell.innerHTML = clause;
cell = row.insertCell();
let html = '<a href="' + makeLinkToId(first) + '">' + sdoName + '</a>';
for (let i = 1; i < ids.length; ++i) {
html += ' (<a href="' + makeLinkToId(ids[i]) + '">' + (i + 1) + '</a>)';
}
cell.innerHTML = html;
});
this.$table.removeChild(this.$tableBody);
this.$tableBody = newBody;
this.$table.appendChild(this.$tableBody);
this.autoSize();
},
autoSize() {
this.$tableContainer.style.height =
Math.min(250, this.$table.getBoundingClientRect().height) + 'px';
},
dragStart(pointerDownEvent) {
let startingMousePos = pointerDownEvent.clientY;
let startingHeight = this.$tableContainer.getBoundingClientRect().height;
let moveListener = pointerMoveEvent => {
if (pointerMoveEvent.buttons === 0) {
removeListeners();
return;
}
let desiredHeight = startingHeight - (pointerMoveEvent.clientY - startingMousePos);
this.$tableContainer.style.height = Math.max(0, desiredHeight) + 'px';
};
let listenerOptions = { capture: true, passive: true };
let removeListeners = () => {
document.removeEventListener('pointermove', moveListener, listenerOptions);
this.$header.removeEventListener('pointerup', removeListeners, listenerOptions);
this.$header.removeEventListener('pointercancel', removeListeners, listenerOptions);
};
document.addEventListener('pointermove', moveListener, listenerOptions);
this.$header.addEventListener('pointerup', removeListeners, listenerOptions);
this.$header.addEventListener('pointercancel', removeListeners, listenerOptions);
},
};
let Toolbox = {
init() {
this.$outer = document.createElement('div');
this.$outer.classList.add('toolbox-container');
this.$container = document.createElement('div');
this.$container.classList.add('toolbox');
this.$outer.appendChild(this.$container);
this.$permalink = document.createElement('a');
this.$permalink.textContent = 'Permalink';
this.$pinLink = document.createElement('a');
this.$pinLink.textContent = 'Pin';
this.$pinLink.setAttribute('href', '#');
this.$pinLink.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
menu.togglePinEntry(this.entry.id);
this.$pinLink.textContent = menu._pinnedIds[this.entry.id] ? 'Unpin' : 'Pin';
});
this.$refsLink = document.createElement('a');
this.$refsLink.setAttribute('href', '#');
this.$refsLink.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
referencePane.showReferencesFor(this.entry);
});
this.$container.appendChild(this.$permalink);
this.$container.appendChild(document.createTextNode(' '));
this.$container.appendChild(this.$pinLink);
this.$container.appendChild(document.createTextNode(' '));
this.$container.appendChild(this.$refsLink);
document.body.appendChild(this.$outer);
},
activate(el, entry, target) {
if (el === this._activeEl) return;
sdoBox.deactivate();
this.active = true;
this.entry = entry;
this.$pinLink.textContent = menu._pinnedIds[entry.id] ? 'Unpin' : 'Pin';
this.$outer.classList.add('active');
this.top = el.offsetTop - this.$outer.offsetHeight;
this.left = el.offsetLeft - 10;
this.$outer.setAttribute('style', 'left: ' + this.left + 'px; top: ' + this.top + 'px');
this.updatePermalink();
this.updateReferences();
this._activeEl = el;
if (this.top < document.body.scrollTop && el === target) {
// don't scroll unless it's a small thing (< 200px)
this.$outer.scrollIntoView();
}
},
updatePermalink() {
this.$permalink.setAttribute('href', makeLinkToId(this.entry.id));
},
updateReferences() {
this.$refsLink.textContent = `References (${(this.entry.referencingIds || []).length})`;
},
activateIfMouseOver(e) {
let ref = this.findReferenceUnder(e.target);
if (ref && (!this.active || e.pageY > this._activeEl.offsetTop)) {
let entry = menu.search.biblio.byId[ref.id];
this.activate(ref.element, entry, e.target);
} else if (
this.active &&
(e.pageY < this.top || e.pageY > this._activeEl.offsetTop + this._activeEl.offsetHeight)
) {
this.deactivate();
}
},
findReferenceUnder(el) {
while (el) {
let parent = el.parentNode;
if (el.nodeName === 'EMU-RHS' || el.nodeName === 'EMU-PRODUCTION') {
return null;
}
if (
el.nodeName === 'H1' &&
parent.nodeName.match(/EMU-CLAUSE|EMU-ANNEX|EMU-INTRO/) &&
parent.id
) {
return { element: el, id: parent.id };
} else if (el.nodeName === 'EMU-NT') {
if (
parent.nodeName === 'EMU-PRODUCTION' &&
parent.id &&
parent.id[0] !== '_' &&
parent.firstElementChild === el
) {
// return the LHS non-terminal element
return { element: el, id: parent.id };
}
return null;
} else if (
el.nodeName.match(/EMU-(?!CLAUSE|XREF|ANNEX|INTRO)|DFN/) &&
el.id &&
el.id[0] !== '_'
) {
if (
el.nodeName === 'EMU-FIGURE' ||
el.nodeName === 'EMU-TABLE' ||
el.nodeName === 'EMU-EXAMPLE'
) {
// return the figcaption element
return { element: el.children[0].children[0], id: el.id };
} else {
return { element: el, id: el.id };
}
}
el = parent;
}
},
deactivate() {
this.$outer.classList.remove('active');
this._activeEl = null;
this.active = false;
},
};
function sortByClauseNumber(clause1, clause2) {
let c1c = clause1.number.split('.');
let c2c = clause2.number.split('.');
for (let i = 0; i < c1c.length; i++) {
if (i >= c2c.length) {
return 1;
}
let c1 = c1c[i];
let c2 = c2c[i];
let c1cn = Number(c1);
let c2cn = Number(c2);
if (Number.isNaN(c1cn) && Number.isNaN(c2cn)) {
if (c1 > c2) {
return 1;
} else if (c1 < c2) {
return -1;
}
} else if (!Number.isNaN(c1cn) && Number.isNaN(c2cn)) {
return -1;
} else if (Number.isNaN(c1cn) && !Number.isNaN(c2cn)) {
return 1;
} else if (c1cn > c2cn) {
return 1;
} else if (c1cn < c2cn) {
return -1;
}
}
if (c1c.length === c2c.length) {
return 0;
}
return -1;
}
function makeLinkToId(id) {
let hash = '#' + id;
if (typeof idToSection === 'undefined' || !idToSection[id]) {
return hash;
}
let targetSec = idToSection[id];
return (targetSec === 'index' ? './' : targetSec + '.html') + hash;
}
function doShortcut(e) {
if (!(e.target instanceof HTMLElement)) {
return;
}
let target = e.target;
let name = target.nodeName.toLowerCase();
if (name === 'textarea' || name === 'input' || name === 'select' || target.isContentEditable) {
return;
}
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;
}
} else if (e.key === 'u') {
document.documentElement.classList.toggle('show-ao-annotations');
} else if (e.key === '?') {
document.getElementById('shortcuts-help').classList.toggle('active');
}
}
function init() {
if (document.getElementById('menu') == null) {
return;
}
menu = new Menu();
let $container = document.getElementById('spec-container');
$container.addEventListener(
'mouseover',
debounce(e => {
Toolbox.activateIfMouseOver(e);
}),
);
document.addEventListener(
'keydown',
debounce(e => {
if (e.code === 'Escape') {
if (Toolbox.active) {
Toolbox.deactivate();
}
document.getElementById('shortcuts-help').classList.remove('active');
}
}),
);
}
document.addEventListener('keypress', doShortcut);
document.addEventListener('DOMContentLoaded', () => {
Toolbox.init();
referencePane.init();
});
// preserve state during navigation
function getTocPath(li) {
let path = [];
let pointer = li;
while (true) {
let parent = pointer.parentElement;
if (parent == null) {
return null;
}
let index = [].indexOf.call(parent.children, pointer);
if (index == -1) {
return null;
}
path.unshift(index);
pointer = parent.parentElement;
if (pointer == null) {
return null;
}
if (pointer.id === 'menu-toc') {
break;
}
if (pointer.tagName !== 'LI') {
return null;
}
}
return path;
}
function activateTocPath(path) {
try {
let pointer = document.getElementById('menu-toc');
for (let index of path) {
pointer = pointer.querySelector('ol').children[index];
}
pointer.classList.add('active');
} catch (e) {
// pass
}
}
function getActiveTocPaths() {
return [...menu.$menu.querySelectorAll('.active')].map(getTocPath).filter(p => p != null);
}
function initTOCExpansion(visibleItemLimit) {
// Initialize to a reasonable amount of TOC expansion:
// * Expand any full-breadth nesting level up to visibleItemLimit.
// * Expand any *single-item* level while under visibleItemLimit (even if that pushes over it).
// Limit to initialization by bailing out if any parent item is already expanded.
const tocItems = Array.from(document.querySelectorAll('#menu-toc li'));
if (tocItems.some(li => li.classList.contains('active') && li.querySelector('li'))) {
return;
}
const selfAndSiblings = maybe => Array.from(maybe?.parentNode.children ?? []);
let currentLevelItems = selfAndSiblings(tocItems[0]);
let availableCount = visibleItemLimit - currentLevelItems.length;
while (availableCount > 0 && currentLevelItems.length) {
const nextLevelItems = currentLevelItems.flatMap(li => selfAndSiblings(li.querySelector('li')));
availableCount -= nextLevelItems.length;
if (availableCount > 0 || currentLevelItems.length === 1) {
// Expand parent items of the next level down (i.e., current-level items with children).
for (const ol of new Set(nextLevelItems.map(li => li.parentNode))) {
ol.closest('li').classList.add('active');
}
}
currentLevelItems = nextLevelItems;
}
}
function initState() {
if (typeof menu === 'undefined' || window.navigating) {
return;
}
const storage = typeof sessionStorage !== 'undefined' ? sessionStorage : Object.create(null);
if (storage.referencePaneState != null) {
let state = JSON.parse(storage.referencePaneState);
if (state != null) {
if (state.type === 'ref') {
let entry = menu.search.biblio.byId[state.id];
if (entry != null) {
referencePane.showReferencesFor(entry);
}
} else if (state.type === 'sdo') {
let sdos = sdoMap[state.id];
if (sdos != null) {
referencePane.$headerText.innerHTML = state.html;
referencePane.showSDOsBody(sdos, state.id);
}
}
delete storage.referencePaneState;
}
}
if (storage.activeTocPaths != null) {
document.querySelectorAll('#menu-toc li.active').forEach(li => li.classList.remove('active'));
let active = JSON.parse(storage.activeTocPaths);
active.forEach(activateTocPath);
delete storage.activeTocPaths;
} else {
initTOCExpansion(20);
}
if (storage.searchValue != null) {
let value = JSON.parse(storage.searchValue);
menu.search.$searchBox.value = value;
menu.search.search(value);
delete storage.searchValue;
}
if (storage.tocScroll != null) {
let tocScroll = JSON.parse(storage.tocScroll);
menu.$toc.scrollTop = tocScroll;
delete storage.tocScroll;
}
}
document.addEventListener('DOMContentLoaded', initState);
window.addEventListener('pageshow', initState);
window.addEventListener('beforeunload', () => {
if (!window.sessionStorage || typeof menu === 'undefined') {
return;
}
sessionStorage.referencePaneState = JSON.stringify(referencePane.state || null);
sessionStorage.activeTocPaths = JSON.stringify(getActiveTocPaths());
sessionStorage.searchValue = JSON.stringify(menu.search.$searchBox.value);
sessionStorage.tocScroll = JSON.stringify(menu.$toc.scrollTop);
});