From 3e399736832544029bd84eaf6c3b9295af4fc17f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:48:10 +0000 Subject: [PATCH 1/2] chore(deps): update dependency ecmarkup to v18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bdb44fa..3de0d68 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,6 @@ "repository": "github:DerekNonGeneric/proposal-assertion-error", "license": "MIT", "devDependencies": { - "ecmarkup": "16.0.0" + "ecmarkup": "18.0.0" } } From d568766a5b2cfc98e776026b012392f163ce6a0b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" Date: Mon, 18 Sep 2023 19:48:32 +0000 Subject: [PATCH 2/2] fixup: [spec] `npm run build` --- index.html | 620 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 502 insertions(+), 118 deletions(-) diff --git a/index.html b/index.html index 0bd1628..b91a6ab 100644 --- a/index.html +++ b/index.html @@ -455,25 +455,68 @@ }; function findActiveClause(root, path) { - let clauses = getChildClauses(root); path = path || []; - for (let $clause of clauses) { - let rect = $clause.getBoundingClientRect(); + 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( - parseInt(getComputedStyle($clause)['margin-top']), + 0, + parseInt(clauseStyles['margin-top']), parseInt(getComputedStyle($header)['margin-top']) ); - - if (rect.top - marginTop <= 1 && rect.bottom > 0) { - return findActiveClause($clause, path.concat($clause)) || path; + 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) { @@ -768,6 +811,10 @@ 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', () => { @@ -776,16 +823,16 @@ this.$header.appendChild(this.$closeButton); this.$pane.appendChild(this.$header); - let tableContainer = document.createElement('div'); - tableContainer.setAttribute('id', 'references-pane-table-container'); + 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(); - tableContainer.appendChild(this.$table); - this.$pane.appendChild(tableContainer); + this.$tableContainer.appendChild(this.$table); + this.$pane.appendChild(this.$tableContainer); menu.$specContainer.appendChild(this.$container); }, @@ -807,7 +854,7 @@ let previousId; let previousCell; let dupCount = 0; - this.$headerRefId.textContent = '#' + entry.id; + this.$headerRefId.innerHTML = getKey(entry); this.$headerRefId.setAttribute('href', makeLinkToId(entry.id)); this.$headerRefId.style.display = 'inline'; (entry.referencingIds || []) @@ -838,6 +885,7 @@ this.$table.removeChild(this.$tableBody); this.$tableBody = newBody; this.$table.appendChild(this.$tableBody); + this.autoSize(); }, showSDOs(sdos, alternativeId) { @@ -884,6 +932,34 @@ 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); }, }; @@ -914,7 +990,9 @@ 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); }, @@ -1165,12 +1243,40 @@ return [...menu.$menu.querySelectorAll('.active')].map(getTocPath).filter(p => p != null); } -function loadStateFromSessionStorage() { - if (!window.sessionStorage || typeof menu === 'undefined' || window.navigating) { +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; } - if (sessionStorage.referencePaneState != null) { - let state = JSON.parse(sessionStorage.referencePaneState); + + 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]; @@ -1184,39 +1290,36 @@ referencePane.showSDOsBody(sdos, state.id); } } - delete sessionStorage.referencePaneState; + delete storage.referencePaneState; } } - if (sessionStorage.activeTocPaths != null) { - document - .getElementById('menu-toc') - .querySelectorAll('.active') - .forEach(e => { - e.classList.remove('active'); - }); - let active = JSON.parse(sessionStorage.activeTocPaths); + 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 sessionStorage.activeTocPaths; + delete storage.activeTocPaths; + } else { + initTOCExpansion(20); } - if (sessionStorage.searchValue != null) { - let value = JSON.parse(sessionStorage.searchValue); + if (storage.searchValue != null) { + let value = JSON.parse(storage.searchValue); menu.search.$searchBox.value = value; menu.search.search(value); - delete sessionStorage.searchValue; + delete storage.searchValue; } - if (sessionStorage.tocScroll != null) { - let tocScroll = JSON.parse(sessionStorage.tocScroll); + if (storage.tocScroll != null) { + let tocScroll = JSON.parse(storage.tocScroll); menu.$toc.scrollTop = tocScroll; - delete sessionStorage.tocScroll; + delete storage.tocScroll; } } -document.addEventListener('DOMContentLoaded', loadStateFromSessionStorage); +document.addEventListener('DOMContentLoaded', initState); -window.addEventListener('pageshow', loadStateFromSessionStorage); +window.addEventListener('pageshow', initState); window.addEventListener('beforeunload', () => { if (!window.sessionStorage || typeof menu === 'undefined') { @@ -1229,44 +1332,308 @@ }); 'use strict'; -let decimalBullet = Array.from({ length: 100 }, (a, i) => '' + (i + 1)); -let alphaBullet = Array.from({ length: 26 }, (a, i) => String.fromCharCode('a'.charCodeAt(0) + i)); - -// prettier-ignore -let romanBullet = ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix', 'x', 'xi', 'xii', 'xiii', 'xiv', 'xv', 'xvi', 'xvii', 'xviii', 'xix', 'xx', 'xxi', 'xxii', 'xxiii', 'xxiv', 'xxv']; -// prettier-ignore -let bullets = [decimalBullet, alphaBullet, romanBullet, decimalBullet, alphaBullet, romanBullet]; - -function addStepNumberText(ol, parentIndex) { - for (let i = 0; i < ol.children.length; ++i) { - let child = ol.children[i]; - let index = parentIndex.concat([i]); - let applicable = bullets[Math.min(index.length - 1, 5)]; - let span = document.createElement('span'); - span.textContent = (applicable[i] || '?') + '. '; - span.style.fontSize = '0'; - span.setAttribute('aria-hidden', 'true'); - child.prepend(span); - let sublist = child.querySelector('ol'); - if (sublist != null) { - addStepNumberText(sublist, index); + +// Manually prefix algorithm step list items with hidden counter representations +// corresponding with their markers so they get selected and copied with content. +// We read list-style-type to avoid divergence with the style sheet, but +// for efficiency assume that all lists at the same nesting depth use the same +// style (except for those associated with replacement steps). +// We also precompute some initial items for each supported style type. +// https://w3c.github.io/csswg-drafts/css-counter-styles/ + +const lowerLetters = Array.from({ length: 26 }, (_, i) => + String.fromCharCode('a'.charCodeAt(0) + i) +); +// Implement the lower-alpha 'alphabetic' algorithm, +// adjusting for indexing from 0 rather than 1. +// https://w3c.github.io/csswg-drafts/css-counter-styles/#simple-alphabetic +// https://w3c.github.io/csswg-drafts/css-counter-styles/#alphabetic-system +const lowerAlphaTextForIndex = i => { + let S = ''; + for (const N = lowerLetters.length; i >= 0; i--) { + S = lowerLetters[i % N] + S; + i = Math.floor(i / N); + } + return S; +}; + +const weightedLowerRomanSymbols = Object.entries({ + m: 1000, + cm: 900, + d: 500, + cd: 400, + c: 100, + xc: 90, + l: 50, + xl: 40, + x: 10, + ix: 9, + v: 5, + iv: 4, + i: 1, +}); +// Implement the lower-roman 'additive' algorithm, +// adjusting for indexing from 0 rather than 1. +// https://w3c.github.io/csswg-drafts/css-counter-styles/#simple-numeric +// https://w3c.github.io/csswg-drafts/css-counter-styles/#additive-system +const lowerRomanTextForIndex = i => { + let value = i + 1; + let S = ''; + for (const [symbol, weight] of weightedLowerRomanSymbols) { + if (!value) break; + if (weight > value) continue; + const reps = Math.floor(value / weight); + S += symbol.repeat(reps); + value -= weight * reps; + } + return S; +}; + +// Memoize pure index-to-text functions with an exposed cache for fast retrieval. +const makeCounter = (pureGetTextForIndex, precomputeCount = 30) => { + const cache = Array.from({ length: precomputeCount }, (_, i) => pureGetTextForIndex(i)); + const getTextForIndex = i => { + if (i >= cache.length) cache[i] = pureGetTextForIndex(i); + return cache[i]; + }; + return { getTextForIndex, cache }; +}; + +const counterByStyle = { + __proto__: null, + decimal: makeCounter(i => String(i + 1)), + 'lower-alpha': makeCounter(lowerAlphaTextForIndex), + 'upper-alpha': makeCounter(i => lowerAlphaTextForIndex(i).toUpperCase()), + 'lower-roman': makeCounter(lowerRomanTextForIndex), + 'upper-roman': makeCounter(i => lowerRomanTextForIndex(i).toUpperCase()), +}; +const fallbackCounter = makeCounter(() => '?'); +const counterByDepth = []; + +function addStepNumberText( + ol, + depth = 0, + special = [...ol.classList].some(c => c.startsWith('nested-')) +) { + let counter = !special && counterByDepth[depth]; + if (!counter) { + const counterStyle = getComputedStyle(ol)['list-style-type']; + counter = counterByStyle[counterStyle]; + if (!counter) { + console.warn('unsupported list-style-type', { + ol, + counterStyle, + id: ol.closest('[id]')?.getAttribute('id'), + }); + counterByStyle[counterStyle] = fallbackCounter; + counter = fallbackCounter; + } + if (!special) { + counterByDepth[depth] = counter; } } + const { cache, getTextForIndex } = counter; + let i = (Number(ol.getAttribute('start')) || 1) - 1; + for (const li of ol.children) { + const marker = document.createElement('span'); + marker.textContent = `${i < cache.length ? cache[i] : getTextForIndex(i)}. `; + marker.setAttribute('aria-hidden', 'true'); + const attributesContainer = li.querySelector('.attributes-tag'); + if (attributesContainer == null) { + li.prepend(marker); + } else { + attributesContainer.insertAdjacentElement('afterend', marker); + } + for (const sublist of li.querySelectorAll(':scope > ol')) { + addStepNumberText(sublist, depth + 1, special); + } + i++; + } } + document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('emu-alg > ol').forEach(ol => { - addStepNumberText(ol, []); + addStepNumberText(ol); + }); +}); + +'use strict'; + +// Update superscripts to not suffer misinterpretation when copied and pasted as plain text. +// For example, +// * Replace `103` with +// `103` +// so it gets pasted as `10**3` rather than `103`. +// * Replace `10-x` with +// `10-x` +// so it gets pasted as `10**-x` rather than `10-x`. +// * Replace `2a + 1` with +// `2**(a + 1)` +// so it gets pasted as `2**(a + 1)` rather than `2a + 1`. + +function makeExponentPlainTextSafe(sup) { + // Change a only if it appears to be an exponent: + // * text-only and contains only mathematical content (not e.g. `1st`) + // * contains only s and internal links (e.g. + // `2(_y_)`) + const isText = [...sup.childNodes].every(node => node.nodeType === 3); + const text = sup.textContent; + if (isText) { + if (!/^[0-9. 𝔽ℝℤ()=*×/÷±+\u2212-]+$/u.test(text)) { + return; + } + } else { + if (sup.querySelector('*:not(var, emu-xref, :scope emu-xref a)')) { + return; + } + } + + let prefix = '**'; + let suffix = ''; + + // Add wrapping parentheses unless they are already present + // or this is a simple (possibly signed) integer or single-variable exponent. + const skipParens = + /^[±+\u2212-]?(?:[0-9]+|\p{ID_Start}\p{ID_Continue}*)$/u.test(text) || + // Split on parentheses and remember them; the resulting parts must + // start and end empty (i.e., with open/close parentheses) + // and increase depth to 1 only at the first parenthesis + // to e.g. wrap `(a+1)*(b+1)` but not `((a+1)*(b+1))`. + text + .trim() + .split(/([()])/g) + .reduce((depth, s, i, parts) => { + if (s === '(') { + return depth > 0 || i === 1 ? depth + 1 : NaN; + } else if (s === ')') { + return depth > 0 ? depth - 1 : NaN; + } else if (s === '' || (i > 0 && i < parts.length - 1)) { + return depth; + } + return NaN; + }, 0) === 0; + if (!skipParens) { + prefix += '('; + suffix += ')'; + } + + sup.insertAdjacentHTML('beforebegin', ``); + if (suffix) { + sup.insertAdjacentHTML('afterend', ``); + } +} + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('sup:not(.text)').forEach(sup => { + makeExponentPlainTextSafe(sup); }); }); let sdoMap = JSON.parse(`{}`); let biblio = JSON.parse(`{"refsByClause":{},"entries":[{"type":"clause","id":"sec-demo-clause","titleHTML":"This is an emu-clause","number":"1"},{"type":"clause","id":"sec-copyright-and-software-license","title":"Copyright & Software License","titleHTML":"Copyright & Software License","number":"A"}]}`); -;let usesMultipage = false