Skip to content

Commit

Permalink
Dynamically add content to make <sup>s copy-paste safe (#517)
Browse files Browse the repository at this point in the history
  • Loading branch information
gibson042 committed Sep 15, 2023
1 parent 4712a9d commit 50e4036
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 20 deletions.
8 changes: 4 additions & 4 deletions css/elements.css
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ body.oldtoc {
margin: 0 auto;
}

span[aria-hidden='true'] {
font-size: 0;
}

a {
text-decoration: none;
color: #206ca7;
Expand Down Expand Up @@ -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;
Expand Down
71 changes: 71 additions & 0 deletions js/superscripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use strict';

// Update superscripts to not suffer misinterpretation when copied and pasted as plain text.
// For example,
// * Replace `10<sup>3</sup>` with
// `10<span aria-hidden="true">**</span><sup>3</sup>`
// so it gets pasted as `10**3` rather than `103`.
// * Replace `10<sup>-<var>x</var></sup>` with
// `10<span aria-hidden="true">**</span><sup>-<var>x</var></sup>`
// so it gets pasted as `10**-x` rather than `10-x`.
// * Replace `2<sup><var>a</var> + 1</sup>` with
// `2<span …>**(</span><sup><var>a</var> + 1</sup><span …>)</span>`
// so it gets pasted as `2**(a + 1)` rather than `2a + 1`.

function makeExponentPlainTextSafe(sup) {
// Change a <sup> only if it appears to be an exponent:
// * text-only and contains only mathematical content (not e.g. `1<sup>st</sup>`)
// * contains only <var>s and internal links (e.g.
// `2<sup><emu-xref><a href="#ℝ">ℝ</a></emu-xref>(_y_)</sup>`)
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', `<span aria-hidden="true">${prefix}</span>`);
if (suffix) {
sup.insertAdjacentHTML('afterend', `<span aria-hidden="true">${suffix}</span>`);
}
}

document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('sup:not(.text)').forEach(sup => {
makeExponentPlainTextSafe(sup);
});
});
37 changes: 22 additions & 15 deletions spec/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ <h1>Options</h1>
<tr><td></td><td>`title`</td><td>Document title, for example "ECMAScript 2016" or "Async Functions".</td></tr>
<tr><td></td><td>`status`</td><td>Document status. Can be "proposal", "draft", or "standard". Defaults to "proposal".</td></tr>
<tr><td></td><td>`stage`</td><td><a href="https://tc39.es/process-document/">TC39 proposal stage</a>. If present and `status` is "proposal", `version` defaults to "Stage <var>stage</var> Draft".</td></tr>
<tr><td></td><td>`version`</td><td>Document version, for example "6&lt;sup>th&lt;/sup> Edition" or "Draft 1".</td></tr>
<tr><td></td><td>`version`</td><td>Document version, for example "6&lt;sup>th&lt;/sup> Edition" (which renders like "6<sup>th</sup> Edition") or "Draft 1".</td></tr>
<tr><td></td><td>`date`</td><td>Timestamp of document rendering, used for various pieces of boilerplate. Defaults to the value of <a href="https://reproducible-builds.org/docs/source-date-epoch/">the `SOURCE_DATE_EPOCH` environment variable</a> (as a number of second since the POSIX epoch) if it exists, otherwise to the current date and time.</td></tr>
<tr><td></td><td>`shortname`</td><td>Document shortname, for example "ECMA-262". If present and `status` is "draft", `version` defaults to "Draft <var>shortname</var>".</td></tr>
<tr><td></td><td>`location`</td><td>URL of this document. Used in conjunction with the biblio file to support inbound references from other documents.</td></tr>
Expand Down Expand Up @@ -340,7 +340,7 @@ <h1>TransitiveEffect ( )</h1>
</aside>
</emu-clause>

<emu-clause id="emu-alg" aoid="EmuAlg">
<emu-clause id="emu-alg">
<h1>emu-alg</h1>
<p>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 <b>aoid</b> in the current spec, and afterwards checking any linked <a href="#emu-biblio">bibliography files</a>.</p>
<p>Algorithm steps can be annotated with additional metadata as summarized in <emu-xref href="#algorithm-step-annotation"></emu-xref>.</p>
Expand All @@ -350,33 +350,40 @@ <h2>Attributes</h2>
<p><b>replaces-step:</b> If present, references a step to replace by its <emu-xref href="#labeled-steps"><b>id</b></emu-xref> (whose numbering the algorithm adopts).</p>

<h2>Example</h2>
<emu-note>The <emu-xref href="#emu-alg" title></emu-xref> clause has an aoid of "EmuAlg".</emu-note>
<pre><code class="language-html">
&lt;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_.
&lt;/emu-alg>
&lt;p>The following is an alernative definition of step &lt;emu-xref href="#step-example-label">&lt;/emu-xref>.&lt;/p>
&lt;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&lt;sup>9&lt;/sup> or _weight_ &gt; 2&lt;sup>_length_ + 1&lt;/sup>, then
1. Throw a *RangeError* exception.
&lt;/emu-alg>
</code></pre>

<h3>Result</h3>
<aside class="result">
<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>
</aside>

Expand Down
2 changes: 1 addition & 1 deletion src/Spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down

0 comments on commit 50e4036

Please sign in to comment.