From 4e52a8c53da43ac27dfeed9c3c2938249e04de6d Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 26 Aug 2024 16:20:52 -0400 Subject: [PATCH 1/8] Add indentation to hidden counter text --- css/elements.css | 1 + js/listNumbers.js | 7 +++++-- spec/index.html | 10 ++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/css/elements.css b/css/elements.css index 0a51f48b..94347e74 100644 --- a/css/elements.css +++ b/css/elements.css @@ -125,6 +125,7 @@ body.oldtoc { span[aria-hidden='true'] { font-size: 0; + white-space: pre; } a { diff --git a/js/listNumbers.js b/js/listNumbers.js index 81d43b80..e856b5b8 100644 --- a/js/listNumbers.js +++ b/js/listNumbers.js @@ -80,6 +80,7 @@ const counterByDepth = []; function addStepNumberText( ol, depth = 0, + indent = '', special = [...ol.classList].some(c => c.startsWith('nested-')), ) { let counter = !special && counterByDepth[depth]; @@ -103,7 +104,9 @@ function addStepNumberText( 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)}. `; + const markerText = i < cache.length ? cache[i] : getTextForIndex(i); + const extraIndent = ' '.repeat(markerText.length + 2); + marker.textContent = `${indent}${markerText}. `; marker.setAttribute('aria-hidden', 'true'); const attributesContainer = li.querySelector('.attributes-tag'); if (attributesContainer == null) { @@ -112,7 +115,7 @@ function addStepNumberText( attributesContainer.insertAdjacentElement('afterend', marker); } for (const sublist of li.querySelectorAll(':scope > ol')) { - addStepNumberText(sublist, depth + 1, special); + addStepNumberText(sublist, depth + 1, indent + extraIndent, special); } i++; } diff --git a/spec/index.html b/spec/index.html index ded4e569..8c0398d7 100644 --- a/spec/index.html +++ b/spec/index.html @@ -367,7 +367,10 @@

Example

1. Set _length_ to _length_ + 1. 1. Let _weight_ be GetWeight of _node_. 1. If _length_ = 10<sup>9</sup> or _weight_ > 2<sup>_length_ + 1</sup>, then - 1. Throw a *RangeError* exception. + 1. NOTE: This is an out-of-bounds state. + 1. If _ignoreErrors_ is not *true*, then + 1. Throw a *RangeError* exception. + 1. Set _hadError_ to *true*. </emu-alg> @@ -386,7 +389,10 @@

Result

1. Set _length_ to _length_ + 1. 1. Let _weight_ be GetWeight of _node_. 1. If _length_ = 109 or _weight_ > 2_length_ + 1, then - 1. Throw a *RangeError* exception. + 1. NOTE: This is an out-of-bounds state. + 1. If _ignoreErrors_ is not *true*, then + 1. Throw a *RangeError* exception. + 1. Set _hadError_ to *true*. From 2c58dda3ded41a3911935d62d13f995cf7889302 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 26 Aug 2024 16:23:49 -0400 Subject: [PATCH 2/8] Update fixture to include new JS and CSS --- test/baselines/generated-reference/assets-inline.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/baselines/generated-reference/assets-inline.html b/test/baselines/generated-reference/assets-inline.html index 302974cc..47d493c1 100644 --- a/test/baselines/generated-reference/assets-inline.html +++ b/test/baselines/generated-reference/assets-inline.html @@ -1439,6 +1439,7 @@ function addStepNumberText( ol, depth = 0, + indent = '', special = [...ol.classList].some(c => c.startsWith('nested-')), ) { let counter = !special && counterByDepth[depth]; @@ -1462,7 +1463,9 @@ 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)}. `; + const markerText = i < cache.length ? cache[i] : getTextForIndex(i); + const extraIndent = ' '.repeat(markerText.length + 2); + marker.textContent = `${indent}${markerText}. `; marker.setAttribute('aria-hidden', 'true'); const attributesContainer = li.querySelector('.attributes-tag'); if (attributesContainer == null) { @@ -1471,7 +1474,7 @@ attributesContainer.insertAdjacentElement('afterend', marker); } for (const sublist of li.querySelectorAll(':scope > ol')) { - addStepNumberText(sublist, depth + 1, special); + addStepNumberText(sublist, depth + 1, indent + extraIndent, special); } i++; } @@ -1684,6 +1687,7 @@ span[aria-hidden='true'] { font-size: 0; + white-space: pre; } a { From 2b206c7cd6cb974418c09483ac2c773bcd4fed56 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 26 Aug 2024 21:07:51 -0400 Subject: [PATCH 3/8] Omit indendation when copying a single algorithm step --- js/listNumbers.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/js/listNumbers.js b/js/listNumbers.js index e856b5b8..281de743 100644 --- a/js/listNumbers.js +++ b/js/listNumbers.js @@ -126,3 +126,22 @@ document.addEventListener('DOMContentLoaded', () => { addStepNumberText(ol); }); }); + +// Omit indendation when copying a single algorithm step. +document.addEventListener('copy', evt => { + const selection = getSelection(); + const singleRange = selection?.rangeCount === 1 && selection.getRangeAt(0); + const container = singleRange?.commonAncestorContainer; + if (!container?.querySelector("span[aria-hidden='true']")) { + return; + } + const clone = document.createElement('div'); + clone.append(singleRange.cloneContents()); + const lastHidden = [...clone.querySelectorAll("span[aria-hidden='true']")].at(-1); + if (lastHidden.previousSibling || lastHidden.parentNode !== clone) { + return; + } + evt.clipboardData.setData('text/plain', clone.textContent.trimStart()); + evt.clipboardData.setData('text/html', clone.innerHTML); + evt.preventDefault(); +}); From fe10a0b39dc239ee88b1694ec27e7c4f6a074f2e Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 26 Aug 2024 16:23:49 -0400 Subject: [PATCH 4/8] Update fixture to include new JS --- .../generated-reference/assets-inline.html | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/baselines/generated-reference/assets-inline.html b/test/baselines/generated-reference/assets-inline.html index 47d493c1..0acd1231 100644 --- a/test/baselines/generated-reference/assets-inline.html +++ b/test/baselines/generated-reference/assets-inline.html @@ -1486,6 +1486,25 @@ }); }); +// Omit indendation when copying a single algorithm step. +document.addEventListener('copy', evt => { + const selection = getSelection(); + const singleRange = selection?.rangeCount === 1 && selection.getRangeAt(0); + const container = singleRange?.commonAncestorContainer; + if (!container?.querySelector("span[aria-hidden='true']")) { + return; + } + const clone = document.createElement('div'); + clone.append(singleRange.cloneContents()); + const lastHidden = [...clone.querySelectorAll("span[aria-hidden='true']")].at(-1); + if (lastHidden.previousSibling || lastHidden.parentNode !== clone) { + return; + } + evt.clipboardData.setData('text/plain', clone.textContent.trimStart()); + evt.clipboardData.setData('text/html', clone.innerHTML); + evt.preventDefault(); +}); + 'use strict'; // Update superscripts to not suffer misinterpretation when copied and pasted as plain text. From ce68621a02fc10c5dbe9159540cae5e1aa07d706 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 27 Aug 2024 14:48:49 -0400 Subject: [PATCH 5/8] Handle more selection ranges in indentation-exclusion code --- js/listNumbers.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/js/listNumbers.js b/js/listNumbers.js index 281de743..1c652f0e 100644 --- a/js/listNumbers.js +++ b/js/listNumbers.js @@ -129,19 +129,30 @@ document.addEventListener('DOMContentLoaded', () => { // Omit indendation when copying a single algorithm step. document.addEventListener('copy', evt => { - const selection = getSelection(); - const singleRange = selection?.rangeCount === 1 && selection.getRangeAt(0); - const container = singleRange?.commonAncestorContainer; - if (!container?.querySelector("span[aria-hidden='true']")) { - return; + const doc = document.implementation.createHTMLDocument(''); + const domRoot = doc.createElement('div'); + const html = evt.clipboardData.getData('text/html'); + if (html) { + domRoot.innerHTML = html; + } else { + const selection = getSelection(); + const singleRange = selection?.rangeCount === 1 && selection.getRangeAt(0); + const container = singleRange?.commonAncestorContainer; + if (!container?.querySelector?.("span[aria-hidden='true']")) { + return; + } + domRoot.append(singleRange.cloneContents()); } - const clone = document.createElement('div'); - clone.append(singleRange.cloneContents()); - const lastHidden = [...clone.querySelectorAll("span[aria-hidden='true']")].at(-1); - if (lastHidden.previousSibling || lastHidden.parentNode !== clone) { + const hiddenElems = [...domRoot.querySelectorAll("span[aria-hidden='true']")]; + const lastHidden = hiddenElems.at(-1); + if (lastHidden?.parentNode !== domRoot || lastHidden.previousSibling) { + // Manipulation is not appropriate, either because there is more than one + // hidden element or because the only one is not at the beginning. return; } - evt.clipboardData.setData('text/plain', clone.textContent.trimStart()); - evt.clipboardData.setData('text/html', clone.innerHTML); + evt.clipboardData.setData('text/plain', domRoot.textContent.trimStart()); + if (!html) { + evt.clipboardData.setData('text/html', domRoot.innerHTML); + } evt.preventDefault(); }); From 3deecb157d431593c2fb6a16d09566480a0ae7fb Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 26 Aug 2024 16:23:49 -0400 Subject: [PATCH 6/8] Update fixture to include new JS --- .../generated-reference/assets-inline.html | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/test/baselines/generated-reference/assets-inline.html b/test/baselines/generated-reference/assets-inline.html index 0acd1231..469b7693 100644 --- a/test/baselines/generated-reference/assets-inline.html +++ b/test/baselines/generated-reference/assets-inline.html @@ -1488,20 +1488,31 @@ // Omit indendation when copying a single algorithm step. document.addEventListener('copy', evt => { - const selection = getSelection(); - const singleRange = selection?.rangeCount === 1 && selection.getRangeAt(0); - const container = singleRange?.commonAncestorContainer; - if (!container?.querySelector("span[aria-hidden='true']")) { - return; + const doc = document.implementation.createHTMLDocument(''); + const domRoot = doc.createElement('div'); + const html = evt.clipboardData.getData('text/html'); + if (html) { + domRoot.innerHTML = html; + } else { + const selection = getSelection(); + const singleRange = selection?.rangeCount === 1 && selection.getRangeAt(0); + const container = singleRange?.commonAncestorContainer; + if (!container?.querySelector?.("span[aria-hidden='true']")) { + return; + } + domRoot.append(singleRange.cloneContents()); } - const clone = document.createElement('div'); - clone.append(singleRange.cloneContents()); - const lastHidden = [...clone.querySelectorAll("span[aria-hidden='true']")].at(-1); - if (lastHidden.previousSibling || lastHidden.parentNode !== clone) { + const hiddenElems = [...domRoot.querySelectorAll("span[aria-hidden='true']")]; + const lastHidden = hiddenElems.at(-1); + if (lastHidden?.parentNode !== domRoot || lastHidden.previousSibling) { + // Manipulation is not appropriate, either because there is more than one + // hidden element or because the only one is not at the beginning. return; } - evt.clipboardData.setData('text/plain', clone.textContent.trimStart()); - evt.clipboardData.setData('text/html', clone.innerHTML); + evt.clipboardData.setData('text/plain', domRoot.textContent.trimStart()); + if (!html) { + evt.clipboardData.setData('text/html', domRoot.innerHTML); + } evt.preventDefault(); }); From db61a6ac4a9df67ade4f71ea9dbb904836eaae5a Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Tue, 27 Aug 2024 17:53:26 -0400 Subject: [PATCH 7/8] Handle arbitrarily many wrappers around a trimmable single-step selection --- js/listNumbers.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/js/listNumbers.js b/js/listNumbers.js index 1c652f0e..dec4aad1 100644 --- a/js/listNumbers.js +++ b/js/listNumbers.js @@ -108,6 +108,7 @@ function addStepNumberText( const extraIndent = ' '.repeat(markerText.length + 2); marker.textContent = `${indent}${markerText}. `; marker.setAttribute('aria-hidden', 'true'); + marker.setAttribute('class', 'list-marker'); const attributesContainer = li.querySelector('.attributes-tag'); if (attributesContainer == null) { li.prepend(marker); @@ -129,6 +130,7 @@ document.addEventListener('DOMContentLoaded', () => { // Omit indendation when copying a single algorithm step. document.addEventListener('copy', evt => { + // Construct a DOM from the selection. const doc = document.implementation.createHTMLDocument(''); const domRoot = doc.createElement('div'); const html = evt.clipboardData.getData('text/html'); @@ -138,18 +140,33 @@ document.addEventListener('copy', evt => { const selection = getSelection(); const singleRange = selection?.rangeCount === 1 && selection.getRangeAt(0); const container = singleRange?.commonAncestorContainer; - if (!container?.querySelector?.("span[aria-hidden='true']")) { + if (!container?.querySelector?.('.list-marker')) { return; } domRoot.append(singleRange.cloneContents()); } - const hiddenElems = [...domRoot.querySelectorAll("span[aria-hidden='true']")]; - const lastHidden = hiddenElems.at(-1); - if (lastHidden?.parentNode !== domRoot || lastHidden.previousSibling) { - // Manipulation is not appropriate, either because there is more than one - // hidden element or because the only one is not at the beginning. + + // Preserve the indentation if there is no hidden list marker, or if selection + // of more than one step is indicated by either multiple such markers or by + // visible text before the first one. + const listMarkers = domRoot.querySelectorAll('.list-marker'); + if (listMarkers.length !== 1) { return; } + const treeWalker = document.createTreeWalker(domRoot, undefined, { + acceptNode(node) { + return node.nodeType === Node.TEXT_NODE || node === listMarkers[0] + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_SKIP; + }, + }); + while (treeWalker.nextNode()) { + const node = treeWalker.currentNode; + if (node.nodeType === Node.ELEMENT_NODE) break; + if (/\S/u.test(node.data)) return; + } + + // Strip leading indentation from the plain text representation. evt.clipboardData.setData('text/plain', domRoot.textContent.trimStart()); if (!html) { evt.clipboardData.setData('text/html', domRoot.innerHTML); From 9e07a32a584cf1a080ad09b65c949a49e7ea3842 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 26 Aug 2024 16:23:49 -0400 Subject: [PATCH 8/8] Update fixture to include new JS --- .../generated-reference/assets-inline.html | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/test/baselines/generated-reference/assets-inline.html b/test/baselines/generated-reference/assets-inline.html index 469b7693..4f72c8ee 100644 --- a/test/baselines/generated-reference/assets-inline.html +++ b/test/baselines/generated-reference/assets-inline.html @@ -1467,6 +1467,7 @@ const extraIndent = ' '.repeat(markerText.length + 2); marker.textContent = `${indent}${markerText}. `; marker.setAttribute('aria-hidden', 'true'); + marker.setAttribute('class', 'list-marker'); const attributesContainer = li.querySelector('.attributes-tag'); if (attributesContainer == null) { li.prepend(marker); @@ -1488,6 +1489,7 @@ // Omit indendation when copying a single algorithm step. document.addEventListener('copy', evt => { + // Construct a DOM from the selection. const doc = document.implementation.createHTMLDocument(''); const domRoot = doc.createElement('div'); const html = evt.clipboardData.getData('text/html'); @@ -1497,18 +1499,33 @@ const selection = getSelection(); const singleRange = selection?.rangeCount === 1 && selection.getRangeAt(0); const container = singleRange?.commonAncestorContainer; - if (!container?.querySelector?.("span[aria-hidden='true']")) { + if (!container?.querySelector?.('.list-marker')) { return; } domRoot.append(singleRange.cloneContents()); } - const hiddenElems = [...domRoot.querySelectorAll("span[aria-hidden='true']")]; - const lastHidden = hiddenElems.at(-1); - if (lastHidden?.parentNode !== domRoot || lastHidden.previousSibling) { - // Manipulation is not appropriate, either because there is more than one - // hidden element or because the only one is not at the beginning. + + // Preserve the indentation if there is no hidden list marker, or if selection + // of more than one step is indicated by either multiple such markers or by + // visible text before the first one. + const listMarkers = domRoot.querySelectorAll('.list-marker'); + if (listMarkers.length !== 1) { return; } + const treeWalker = document.createTreeWalker(domRoot, undefined, { + acceptNode(node) { + return node.nodeType === Node.TEXT_NODE || node === listMarkers[0] + ? NodeFilter.FILTER_ACCEPT + : NodeFilter.FILTER_SKIP; + }, + }); + while (treeWalker.nextNode()) { + const node = treeWalker.currentNode; + if (node.nodeType === Node.ELEMENT_NODE) break; + if (/\S/u.test(node.data)) return; + } + + // Strip leading indentation from the plain text representation. evt.clipboardData.setData('text/plain', domRoot.textContent.trimStart()); if (!html) { evt.clipboardData.setData('text/html', domRoot.innerHTML);