mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-04 18:58:39 +00:00
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 <edemaine@mit.edu> * 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 <edemaine@mit.edu>
This commit is contained in:
committed by
GitHub
parent
023cc0342c
commit
8c2d852c4a
@@ -2,29 +2,27 @@
|
|||||||
|
|
||||||
This extension modifies the copy/paste behavior in any browser supporting the
|
This extension modifies the copy/paste behavior in any browser supporting the
|
||||||
[Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent)
|
[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
|
content of the resulting clipboard renders KaTeX elements as their LaTeX source
|
||||||
surrounded by specified delimiters. (The HTML content of the resulting
|
surrounded by specified delimiters. (The HTML content of the resulting
|
||||||
clipboard remains the selected HTML content, as it normally would.)
|
clipboard remains the selected HTML content, as it normally would.)
|
||||||
The default delimiters are `$...$` for inline math and `$$...$$` for display
|
The default delimiters are `$...$` for inline math and `$$...$$` for display
|
||||||
math, but you can easy switch them to e.g. `\(...\)` and `\[...\]` by
|
math, but you can easy switch them to e.g. `\(...\)` and `\[...\]` by
|
||||||
modifying `copyDelimiters` in [the source code](copy-tex.js).
|
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
|
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
|
included in the page.
|
||||||
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.
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link href="https://cdn.jsdelivr.net/npm/katex@0.15.6/dist/contrib/copy-tex.css" rel="stylesheet" type="text/css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.15.6/dist/contrib/copy-tex.min.js" integrity="sha384-Ep9Es0VCjVn9dFeaN2uQxgGcGmG+pfZ4eBaHxUpxXDORrrVACZVOpywyzvFRGbmv" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/katex@0.15.6/dist/contrib/copy-tex.min.js" integrity="sha384-Ep9Es0VCjVn9dFeaN2uQxgGcGmG+pfZ4eBaHxUpxXDORrrVACZVOpywyzvFRGbmv" crossorigin="anonymous"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(Note that, as of KaTeX 0.16.0, there is no longer a corresponding CSS file.)
|
||||||
|
|
||||||
See [index.html](index.html) for an example.
|
See [index.html](index.html) for an example.
|
||||||
(To run this example from a clone of the repository, run `yarn start`
|
(To run this example from a clone of the repository, run `yarn start`
|
||||||
in the root KaTeX directory, and then visit
|
in the root KaTeX directory, and then visit
|
||||||
@@ -39,11 +37,3 @@ ECMAScript module is also available:
|
|||||||
```html
|
```html
|
||||||
<script type="module" src="https://cdn.jsdelivr.net/npm/katex@0.15.6/dist/contrib/copy-tex.mjs" integrity="sha384-+gSYJ3yzY30+a6FGYJXOx9swmWs5oPKEi1AeCsAxsLexABlUXgHXkOkEZCj0Lz8U" crossorigin="anonymous"></script>
|
<script type="module" src="https://cdn.jsdelivr.net/npm/katex@0.15.6/dist/contrib/copy-tex.mjs" integrity="sha384-+gSYJ3yzY30+a6FGYJXOx9swmWs5oPKEi1AeCsAxsLexABlUXgHXkOkEZCj0Lz8U" crossorigin="anonymous"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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).
|
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
@@ -1,23 +1,50 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
import katexReplaceWithTex from './katex2tex';
|
import katexReplaceWithTex from './katex2tex';
|
||||||
|
|
||||||
// Global copy handler to modify behavior on .katex elements.
|
// Return <div class="katex"> element containing node, or null if not found.
|
||||||
document.addEventListener('copy', function(event) {
|
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();
|
const selection = window.getSelection();
|
||||||
if (selection.isCollapsed) {
|
if (selection.isCollapsed || !event.clipboardData) {
|
||||||
return; // default action OK if selection is empty
|
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')) {
|
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.
|
// Preserve usual HTML copy/paste behavior.
|
||||||
const html = [];
|
clipboardData.setData('text/html', htmlContents);
|
||||||
for (let i = 0; i < fragment.childNodes.length; i++) {
|
|
||||||
html.push(fragment.childNodes[i].outerHTML);
|
|
||||||
}
|
|
||||||
event.clipboardData.setData('text/html', html.join(''));
|
|
||||||
// Rewrite plain-text version.
|
// Rewrite plain-text version.
|
||||||
event.clipboardData.setData('text/plain',
|
clipboardData.setData('text/plain',
|
||||||
katexReplaceWithTex(fragment).textContent);
|
katexReplaceWithTex(fragment).textContent);
|
||||||
// Prevent normal copy handling.
|
// Prevent normal copy handling.
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@@ -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';
|
|
@@ -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.
|
// Set these to how you want inline and display math to be delimited.
|
||||||
export const defaultCopyDelimiters = {
|
export const defaultCopyDelimiters: CopyDelimiters = {
|
||||||
inline: ['$', '$'], // alternative: ['\(', '\)']
|
inline: ['$', '$'], // alternative: ['\(', '\)']
|
||||||
display: ['$$', '$$'], // alternative: ['\[', '\]']
|
display: ['$$', '$$'], // alternative: ['\[', '\]']
|
||||||
};
|
};
|
||||||
@@ -7,16 +14,18 @@ export const defaultCopyDelimiters = {
|
|||||||
// Replace .katex elements with their TeX source (<annotation> element).
|
// Replace .katex elements with their TeX source (<annotation> element).
|
||||||
// Modifies fragment in-place. Useful for writing your own 'copy' handler,
|
// Modifies fragment in-place. Useful for writing your own 'copy' handler,
|
||||||
// as in copy-tex.js.
|
// as in copy-tex.js.
|
||||||
export const katexReplaceWithTex = function(fragment,
|
export function katexReplaceWithTex(
|
||||||
copyDelimiters = defaultCopyDelimiters) {
|
fragment: DocumentFragment,
|
||||||
|
copyDelimiters: CopyDelimiters = defaultCopyDelimiters
|
||||||
|
): DocumentFragment {
|
||||||
// Remove .katex-html blocks that are preceded by .katex-mathml blocks
|
// Remove .katex-html blocks that are preceded by .katex-mathml blocks
|
||||||
// (which will get replaced below).
|
// (which will get replaced below).
|
||||||
const katexHtml = fragment.querySelectorAll('.katex-mathml + .katex-html');
|
const katexHtml = fragment.querySelectorAll('.katex-mathml + .katex-html');
|
||||||
for (let i = 0; i < katexHtml.length; i++) {
|
for (let i = 0; i < katexHtml.length; i++) {
|
||||||
const element = katexHtml[i];
|
const element = katexHtml[i];
|
||||||
if (element.remove) {
|
if (element.remove) {
|
||||||
element.remove(null);
|
element.remove();
|
||||||
} else {
|
} else if (element.parentNode) {
|
||||||
element.parentNode.removeChild(element);
|
element.parentNode.removeChild(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +38,7 @@ export const katexReplaceWithTex = function(fragment,
|
|||||||
if (texSource) {
|
if (texSource) {
|
||||||
if (element.replaceWith) {
|
if (element.replaceWith) {
|
||||||
element.replaceWith(texSource);
|
element.replaceWith(texSource);
|
||||||
} else {
|
} else if (element.parentNode) {
|
||||||
element.parentNode.replaceChild(texSource, element);
|
element.parentNode.replaceChild(texSource, element);
|
||||||
}
|
}
|
||||||
texSource.innerHTML = copyDelimiters.inline[0] +
|
texSource.innerHTML = copyDelimiters.inline[0] +
|
||||||
@@ -47,6 +56,6 @@ export const katexReplaceWithTex = function(fragment,
|
|||||||
+ copyDelimiters.display[1];
|
+ copyDelimiters.display[1];
|
||||||
}
|
}
|
||||||
return fragment;
|
return fragment;
|
||||||
};
|
}
|
||||||
|
|
||||||
export default katexReplaceWithTex;
|
export default katexReplaceWithTex;
|
||||||
|
@@ -127,7 +127,7 @@
|
|||||||
"test": "yarn test:lint && yarn test:flow && yarn test:jest",
|
"test": "yarn test:lint && yarn test:flow && yarn test:jest",
|
||||||
"test:lint": "yarn test:lint:js && yarn test:lint:css",
|
"test:lint": "yarn test:lint:js && yarn test:lint:css",
|
||||||
"test:lint:js": "eslint .",
|
"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:flow": "flow",
|
||||||
"test:jest": "jest",
|
"test:jest": "jest",
|
||||||
"test:jest:watch": "jest --watch",
|
"test:jest:watch": "jest --watch",
|
||||||
|
@@ -43,7 +43,7 @@ const targets /*: Array<Target> */ = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'contrib/copy-tex',
|
name: 'contrib/copy-tex',
|
||||||
entry: './contrib/copy-tex/copy-tex.webpack.js',
|
entry: './contrib/copy-tex/copy-tex.js',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'contrib/mathtex-script-type',
|
name: 'contrib/mathtex-script-type',
|
||||||
|
@@ -17,7 +17,6 @@ fs.copySync('../dist/katex.min.css', 'static/static/katex.min.css');
|
|||||||
fs.copySync('../dist/fonts', 'static/static/fonts');
|
fs.copySync('../dist/fonts', 'static/static/fonts');
|
||||||
|
|
||||||
// copy local built KaTeX extensions
|
// 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/copy-tex.min.js', 'static/static/copy-tex.min.js');
|
||||||
fs.copySync('../dist/contrib/mhchem.min.js', 'static/static/mhchem.min.js');
|
fs.copySync('../dist/contrib/mhchem.min.js', 'static/static/mhchem.min.js');
|
||||||
|
|
||||||
|
@@ -24,7 +24,6 @@
|
|||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:300,400,700,700i">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:300,400,700,700i">
|
||||||
<link rel="stylesheet" href="static/katex.min.css" crossorigin="anonymous">
|
<link rel="stylesheet" href="static/katex.min.css" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="static/index.css">
|
<link rel="stylesheet" href="static/index.css">
|
||||||
<link rel="stylesheet" href="static/copy-tex.min.css" crossorigin="anonymous">
|
|
||||||
|
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/clipboard@2.0.1/dist/clipboard.min.js" integrity="sha256-hIvIxeqhGZF+VVeM55k0mJvWpQ6gTkWk3Emc+NmowYA=" crossorigin="anonymous"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/clipboard@2.0.1/dist/clipboard.min.js" integrity="sha256-hIvIxeqhGZF+VVeM55k0mJvWpQ6gTkWk3Emc+NmowYA=" crossorigin="anonymous"></script>
|
||||||
<script defer src="static/katex.min.js" crossorigin="anonymous"></script>
|
<script defer src="static/katex.min.js" crossorigin="anonymous"></script>
|
||||||
|
Reference in New Issue
Block a user