From 8c2d852c4af213ba4e2f4a3b875498c367e9a119 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 6 Jun 2022 18:09:30 +0200 Subject: [PATCH] fix(copy-tex): Use JS (instead of CSS) to select full equation, solving display glitches (#3586) * copy-tex: Use JS to select full equation instead of CSS * remove CSS * Update webpack.common.js * more build tweaks * Update contrib/copy-tex/copy-tex.js Co-authored-by: Erik Demaine * Document new behavior BREAKING CHANGE: copy-tex extension no longer has (or requires) a CSS file. * Code cleanup, lint fixes, port to Flow * Rewrite to extend both start and end of range * Remove contrib/**/*.css linting Co-authored-by: Erik Demaine --- contrib/copy-tex/README.md | 24 ++++--------- contrib/copy-tex/copy-tex.css | 10 ------ contrib/copy-tex/copy-tex.js | 51 +++++++++++++++++++++------- contrib/copy-tex/copy-tex.webpack.js | 6 ---- contrib/copy-tex/katex2tex.js | 23 +++++++++---- package.json | 2 +- webpack.common.js | 2 +- website/lib/build.js | 1 - website/pages/index.html | 1 - 9 files changed, 64 insertions(+), 56 deletions(-) delete mode 100644 contrib/copy-tex/copy-tex.css delete mode 100644 contrib/copy-tex/copy-tex.webpack.js diff --git a/contrib/copy-tex/README.md b/contrib/copy-tex/README.md index 2c9a7d1e..dd1c28c0 100644 --- a/contrib/copy-tex/README.md +++ b/contrib/copy-tex/README.md @@ -2,29 +2,27 @@ This extension modifies the copy/paste behavior in any browser supporting the [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent) -so that, when selecting and copying whole KaTeX-rendered elements, the text +so that, when selecting and copying KaTeX-rendered elements, the text content of the resulting clipboard renders KaTeX elements as their LaTeX source surrounded by specified delimiters. (The HTML content of the resulting clipboard remains the selected HTML content, as it normally would.) The default delimiters are `$...$` for inline math and `$$...$$` for display math, but you can easy switch them to e.g. `\(...\)` and `\[...\]` by modifying `copyDelimiters` in [the source code](copy-tex.js). +Note that a selection containing part of a KaTeX formula gets extended to +include the entire KaTeX formula. -### Usage +## Usage This extension isn't part of KaTeX proper, so the script should be separately -included in the page. It also provides *optional* custom CSS that -defines KaTeX equations as -[`user-select: all`](https://developer.mozilla.org/en-US/docs/Web/CSS/user-select) -so that they get selected all-or-nothing (and thus trigger the good behavior -provided by this extension). Without this CSS, partially selected equations -will just get the usual HTML copy/paste behavior. +included in the page. ```html - ``` +(Note that, as of KaTeX 0.16.0, there is no longer a corresponding CSS file.) + See [index.html](index.html) for an example. (To run this example from a clone of the repository, run `yarn start` in the root KaTeX directory, and then visit @@ -39,11 +37,3 @@ ECMAScript module is also available: ```html ``` - -### Known Issues - -This extension has been tested on Chrome, Firefox, Edge, and Safari. - -Safari copies correctly, but the selection rectangle renders strangely -(too big) when interacting with display math -(because of the `user-select: all` CSS). diff --git a/contrib/copy-tex/copy-tex.css b/contrib/copy-tex/copy-tex.css deleted file mode 100644 index be015f1a..00000000 --- a/contrib/copy-tex/copy-tex.css +++ /dev/null @@ -1,10 +0,0 @@ -/* Force selection of entire .katex/.katex-display blocks, so that we can - * copy/paste the entire source code. If you omit this CSS, partial - * selections of a formula will work, but will copy the ugly HTML - * representation instead of the LaTeX source code. (Full selections will - * still produce the LaTeX source code.) - */ -.katex, -.katex-display { - user-select: all; -} diff --git a/contrib/copy-tex/copy-tex.js b/contrib/copy-tex/copy-tex.js index ca6e08ac..79c91d59 100644 --- a/contrib/copy-tex/copy-tex.js +++ b/contrib/copy-tex/copy-tex.js @@ -1,23 +1,50 @@ +// @flow + import katexReplaceWithTex from './katex2tex'; -// Global copy handler to modify behavior on .katex elements. -document.addEventListener('copy', function(event) { +// Return
element containing node, or null if not found. +function closestKatex(node: Node): ?Element { + // If node is a Text Node, for example, go up to containing Element, + // where we can apply the `closest` method. + const element: ?Element = + (node instanceof Element ? node : node.parentElement); + return element && element.closest('.katex'); +} + +// Global copy handler to modify behavior on/within .katex elements. +document.addEventListener('copy', function(event: ClipboardEvent) { const selection = window.getSelection(); - if (selection.isCollapsed) { - return; // default action OK if selection is empty + if (selection.isCollapsed || !event.clipboardData) { + return; // default action OK if selection is empty or unchangeable } - const fragment = selection.getRangeAt(0).cloneContents(); + const clipboardData = event.clipboardData; + const range = selection.getRangeAt(0); + + // When start point is within a formula, expand to entire formula. + const startKatex = closestKatex(range.startContainer); + if (startKatex) { + range.setStartBefore(startKatex); + } + + // Similarly, when end point is within a formula, expand to entire formula. + const endKatex = closestKatex(range.endContainer); + if (endKatex) { + range.setEndAfter(endKatex); + } + + const fragment = range.cloneContents(); if (!fragment.querySelector('.katex-mathml')) { - return; // default action OK if no .katex-mathml elements + return; // default action OK if no .katex-mathml elements } + + const htmlContents = Array.prototype.map.call(fragment.childNodes, + (el) => (el instanceof Text ? el.textContent : el.outerHTML) + ).join(''); + // Preserve usual HTML copy/paste behavior. - const html = []; - for (let i = 0; i < fragment.childNodes.length; i++) { - html.push(fragment.childNodes[i].outerHTML); - } - event.clipboardData.setData('text/html', html.join('')); + clipboardData.setData('text/html', htmlContents); // Rewrite plain-text version. - event.clipboardData.setData('text/plain', + clipboardData.setData('text/plain', katexReplaceWithTex(fragment).textContent); // Prevent normal copy handling. event.preventDefault(); diff --git a/contrib/copy-tex/copy-tex.webpack.js b/contrib/copy-tex/copy-tex.webpack.js deleted file mode 100644 index 9842a9e1..00000000 --- a/contrib/copy-tex/copy-tex.webpack.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * This is the webpack entry point for KaTeX. As ECMAScript doesn't support - * CSS modules natively, a separate entry point is used. - */ -import './copy-tex.css'; -import './copy-tex.js'; diff --git a/contrib/copy-tex/katex2tex.js b/contrib/copy-tex/katex2tex.js index 9b64c786..927003ca 100644 --- a/contrib/copy-tex/katex2tex.js +++ b/contrib/copy-tex/katex2tex.js @@ -1,5 +1,12 @@ +// @flow + +export interface CopyDelimiters { + inline: [string, string], + display: [string, string], +} + // Set these to how you want inline and display math to be delimited. -export const defaultCopyDelimiters = { +export const defaultCopyDelimiters: CopyDelimiters = { inline: ['$', '$'], // alternative: ['\(', '\)'] display: ['$$', '$$'], // alternative: ['\[', '\]'] }; @@ -7,16 +14,18 @@ export const defaultCopyDelimiters = { // Replace .katex elements with their TeX source ( element). // Modifies fragment in-place. Useful for writing your own 'copy' handler, // as in copy-tex.js. -export const katexReplaceWithTex = function(fragment, - copyDelimiters = defaultCopyDelimiters) { +export function katexReplaceWithTex( + fragment: DocumentFragment, + copyDelimiters: CopyDelimiters = defaultCopyDelimiters +): DocumentFragment { // Remove .katex-html blocks that are preceded by .katex-mathml blocks // (which will get replaced below). const katexHtml = fragment.querySelectorAll('.katex-mathml + .katex-html'); for (let i = 0; i < katexHtml.length; i++) { const element = katexHtml[i]; if (element.remove) { - element.remove(null); - } else { + element.remove(); + } else if (element.parentNode) { element.parentNode.removeChild(element); } } @@ -29,7 +38,7 @@ export const katexReplaceWithTex = function(fragment, if (texSource) { if (element.replaceWith) { element.replaceWith(texSource); - } else { + } else if (element.parentNode) { element.parentNode.replaceChild(texSource, element); } texSource.innerHTML = copyDelimiters.inline[0] + @@ -47,6 +56,6 @@ export const katexReplaceWithTex = function(fragment, + copyDelimiters.display[1]; } return fragment; -}; +} export default katexReplaceWithTex; diff --git a/package.json b/package.json index 6500317f..b2ac86d6 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "test": "yarn test:lint && yarn test:flow && yarn test:jest", "test:lint": "yarn test:lint:js && yarn test:lint:css", "test:lint:js": "eslint .", - "test:lint:css": "stylelint src/katex.less static/main.css contrib/**/*.css website/static/**/*.css", + "test:lint:css": "stylelint src/katex.less static/main.css website/static/**/*.css", "test:flow": "flow", "test:jest": "jest", "test:jest:watch": "jest --watch", diff --git a/webpack.common.js b/webpack.common.js index ac384358..4cd250eb 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -43,7 +43,7 @@ const targets /*: Array */ = [ }, { name: 'contrib/copy-tex', - entry: './contrib/copy-tex/copy-tex.webpack.js', + entry: './contrib/copy-tex/copy-tex.js', }, { name: 'contrib/mathtex-script-type', diff --git a/website/lib/build.js b/website/lib/build.js index 46620e6e..bc0915dc 100644 --- a/website/lib/build.js +++ b/website/lib/build.js @@ -17,7 +17,6 @@ fs.copySync('../dist/katex.min.css', 'static/static/katex.min.css'); fs.copySync('../dist/fonts', 'static/static/fonts'); // copy local built KaTeX extensions -fs.copySync('../dist/contrib/copy-tex.min.css', 'static/static/copy-tex.min.css'); fs.copySync('../dist/contrib/copy-tex.min.js', 'static/static/copy-tex.min.js'); fs.copySync('../dist/contrib/mhchem.min.js', 'static/static/mhchem.min.js'); diff --git a/website/pages/index.html b/website/pages/index.html index c37b23a1..cdac973c 100644 --- a/website/pages/index.html +++ b/website/pages/index.html @@ -24,7 +24,6 @@ -