From 50e40366157958ecdb2e885559cc1061d9cbc727 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Fri, 15 Sep 2023 03:20:05 -0400 Subject: [PATCH] Dynamically add content to make s copy-paste safe (#517) --- css/elements.css | 8 +++--- js/superscripts.js | 71 ++++++++++++++++++++++++++++++++++++++++++++++ spec/index.html | 37 ++++++++++++++---------- src/Spec.ts | 2 +- 4 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 js/superscripts.js diff --git a/css/elements.css b/css/elements.css index ad2183a2..03e915b9 100644 --- a/css/elements.css +++ b/css/elements.css @@ -119,6 +119,10 @@ body.oldtoc { margin: 0 auto; } +span[aria-hidden='true'] { + font-size: 0; +} + a { text-decoration: none; color: #206ca7; @@ -284,10 +288,6 @@ emu-alg ol.nested-lots ol { list-style-type: lower-roman; } -emu-alg [aria-hidden='true'] { - font-size: 0; -} - emu-eqn { display: block; margin-left: 4em; diff --git a/js/superscripts.js b/js/superscripts.js new file mode 100644 index 00000000..98ca6085 --- /dev/null +++ b/js/superscripts.js @@ -0,0 +1,71 @@ +'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); + }); +}); diff --git a/spec/index.html b/spec/index.html index e12804b2..aa42c52f 100644 --- a/spec/index.html +++ b/spec/index.html @@ -66,7 +66,7 @@

Options

`title`Document title, for example "ECMAScript 2016" or "Async Functions". `status`Document status. Can be "proposal", "draft", or "standard". Defaults to "proposal". `stage`TC39 proposal stage. If present and `status` is "proposal", `version` defaults to "Stage stage Draft". - `version`Document version, for example "6<sup>th</sup> Edition" or "Draft 1". + `version`Document version, for example "6<sup>th</sup> Edition" (which renders like "6th Edition") or "Draft 1". `date`Timestamp of document rendering, used for various pieces of boilerplate. Defaults to the value of the `SOURCE_DATE_EPOCH` environment variable (as a number of second since the POSIX epoch) if it exists, otherwise to the current date and time. `shortname`Document shortname, for example "ECMA-262". If present and `status` is "draft", `version` defaults to "Draft shortname". `location`URL of this document. Used in conjunction with the biblio file to support inbound references from other documents. @@ -340,7 +340,7 @@

TransitiveEffect ( )

- +

emu-alg

Algorithm steps. Should not contain any HTML. The node's textContent is parsed as an Ecmarkdown document. Additionally, calls to abstract operations inside algorithm steps are automatically linked to their definitions by first checking for any clauses or algorithms with the appropriate aoid in the current spec, and afterwards checking any linked bibliography files.

Algorithm steps can be annotated with additional metadata as summarized in .

@@ -350,33 +350,40 @@

Attributes

replaces-step: If present, references a step to replace by its id (whose numbering the algorithm adopts).

Example

- The clause has an aoid of "EmuAlg".

     <emu-alg>
-      1. Let _clauseAbstractOp_ be the result of calling EmuAlg().
-      1. If *false* is *true*, then
-        1. [id="step-example-label"] Let _recurse_ be the result of calling EmuAlg(*true*).
-        1. Return the result of evaluating this |NonTerminalProduction|.
+      1. Let _length_ be 0.
+      1. Let _node_ be this |ListNode|.
+      1. Repeat, while _node_ is not *undefined*,
+        1. Set _node_ to NextNode of _node_.
+        1. [id="step-example-label"] Set _length_ to _length_ + 1.
+      1. Return _length_.
     </emu-alg>
     <p>The following is an alernative definition of step <emu-xref href="#step-example-label"></emu-xref>.</p>
     <emu-alg replaces-step="step-example-label">
-      1. Replacement.
-        1. Substep.
+      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.
     </emu-alg>
   

Result

diff --git a/src/Spec.ts b/src/Spec.ts index f1496129..ad8b42f2 100644 --- a/src/Spec.ts +++ b/src/Spec.ts @@ -2135,7 +2135,7 @@ async function walk(walker: TreeWalker, context: Context) { context.tagStack.pop(); } -const jsDependencies = ['sdoMap.js', 'menu.js', 'listNumbers.js']; +const jsDependencies = ['sdoMap.js', 'menu.js', 'listNumbers.js', 'superscripts.js']; async function concatJs(...extras: string[]) { let dependencies = await Promise.all( jsDependencies.map(dependency => utils.readFile(path.join(__dirname, '../js/' + dependency)))