@liqvid/katex 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,3 @@
1
1
  # @liqvid/katex
2
2
 
3
- [KaTeX](https://katex.org/) integration for [Liqvid](https://liqvidjs.org).
4
-
5
- ## Usage
6
-
7
- ```tsx
8
- import {KTX} from "@liqvid/katex";
9
-
10
- function Quadratic() {
11
- return (
12
- <div>
13
- The value of <KTX>x</KTX> is given by the quadratic formula
14
- <KTX display>{String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`}</KTX>
15
- </div>
16
- );
17
- }
18
- ```
19
-
20
- ## Macros
21
-
22
- For convenience, this module supports loading macro definitions from a file. Simply include a `<script type="math/tex">` tag in the `<head>` of your html, pointing to a tex file containing `\newcommand`s.
23
-
24
- ```html
25
- <!-- this has to go in <head> -->
26
- <script src="./macros.tex" type="math/tex"></script>
27
- ```
28
- ```tex
29
- % macros.tex
30
- \newcommand{\C}{\mathbb C}
31
- ```
3
+ [KaTeX](https://katex.org/) integration for [Liqvid](https://liqvidjs.org). See https://liqvidjs.org/docs/integrations/katex/ for documentation.
package/dist/index.cjs ADDED
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var jsxRuntime_js = require('react/jsx-runtime.js');
6
+ var react$1 = require('@liqvid/utils/react');
7
+ var liqvid = require('liqvid');
8
+ var react = require('react');
9
+
10
+ // option of loading KaTeX asynchronously
11
+ const KaTeXLoad = new Promise((resolve) => {
12
+ const script = document.querySelector("script[src*=\"katex.js\"], script[src*=\"katex.min.js\"]");
13
+ if (!script)
14
+ return;
15
+ if (window.hasOwnProperty("katex")) {
16
+ resolve(katex);
17
+ }
18
+ else {
19
+ script.addEventListener("load", () => resolve(katex));
20
+ }
21
+ });
22
+ // load macros from <head>
23
+ const KaTeXMacros = new Promise((resolve) => {
24
+ const macros = {};
25
+ const scripts = Array.from(document.querySelectorAll("head > script[type='math/tex']"));
26
+ return Promise.all(scripts.map(script => fetch(script.src)
27
+ .then(res => {
28
+ if (res.ok)
29
+ return res.text();
30
+ throw new Error(`${res.status} ${res.statusText}: ${script.src}`);
31
+ })
32
+ .then(tex => {
33
+ Object.assign(macros, parseMacros(tex));
34
+ }))).then(() => resolve(macros));
35
+ });
36
+ /**
37
+ * Ready Promise
38
+ */
39
+ const KaTeXReady = Promise.all([KaTeXLoad, KaTeXMacros]);
40
+ /**
41
+ * Parse \newcommand macros in a file.
42
+ * Also supports \ktxnewcommand (for use in conjunction with MathJax).
43
+ * @param file TeX file to parse
44
+ */
45
+ function parseMacros(file) {
46
+ const macros = {};
47
+ const rgx = /\\(?:ktx)?newcommand\{(.+?)\}(?:\[\d+\])?\{/g;
48
+ let match;
49
+ while (match = rgx.exec(file)) {
50
+ let body = "";
51
+ const macro = match[1];
52
+ let braceCount = 1;
53
+ for (let i = match.index + match[0].length; (braceCount > 0) && (i < file.length); ++i) {
54
+ const char = file[i];
55
+ if (char === "{") {
56
+ braceCount++;
57
+ }
58
+ else if (char === "}") {
59
+ braceCount--;
60
+ if (braceCount === 0)
61
+ break;
62
+ }
63
+ else if (char === "\\") {
64
+ body += file.slice(i, i + 2);
65
+ ++i;
66
+ continue;
67
+ }
68
+ body += char;
69
+ }
70
+ macros[macro] = body;
71
+ }
72
+ return macros;
73
+ }
74
+
75
+ /** Component for KaTeX code */
76
+ const KTX$1 = react.forwardRef(function KTX(props, ref) {
77
+ const spanRef = react.useRef();
78
+ const { children, display = false, ...attrs } = props;
79
+ const [ready, resolve] = react$1.usePromise();
80
+ // handle
81
+ react.useImperativeHandle(ref, () => ({
82
+ domElement: spanRef.current,
83
+ ready
84
+ }));
85
+ react.useEffect(() => {
86
+ KaTeXReady.then(([katex, macros]) => {
87
+ katex.render(children.toString(), spanRef.current, {
88
+ displayMode: !!display,
89
+ macros,
90
+ strict: "ignore",
91
+ throwOnError: false,
92
+ trust: true
93
+ });
94
+ /* move katex into placeholder element */
95
+ const child = spanRef.current.firstElementChild;
96
+ // copy classes
97
+ for (let i = 0, len = child.classList.length; i < len; ++i) {
98
+ spanRef.current.classList.add(child.classList.item(i));
99
+ }
100
+ // move children
101
+ while (child.childNodes.length > 0) {
102
+ spanRef.current.appendChild(child.firstChild);
103
+ }
104
+ // delete child
105
+ child.remove();
106
+ // resolve promise
107
+ resolve();
108
+ });
109
+ }, [children]);
110
+ // Google Chrome fails without this
111
+ if (display) {
112
+ if (!attrs.style)
113
+ attrs.style = {};
114
+ attrs.style.display = "block";
115
+ }
116
+ return (jsxRuntime_js.jsx("span", { ...attrs, ref: spanRef }));
117
+ });
118
+
119
+ /** Component for KaTeX code */
120
+ const KTX = react.forwardRef(function KTX(props, ref) {
121
+ const { obstruct = "canplay canplaythrough", reparse = false, ...attrs } = props;
122
+ const plain = react.useRef();
123
+ const combined = react$1.combineRefs(plain, ref);
124
+ const player = liqvid.usePlayer();
125
+ react.useEffect(() => {
126
+ // obstruction
127
+ if (obstruct.match(/\bcanplay\b/)) {
128
+ player.obstruct("canplay", plain.current.ready);
129
+ }
130
+ if (obstruct.match("canplaythrough")) {
131
+ player.obstruct("canplaythrough", plain.current.ready);
132
+ }
133
+ // reparsing
134
+ if (reparse) {
135
+ plain.current.ready.then(() => player.reparseTree(plain.current.domElement));
136
+ }
137
+ }, []);
138
+ return (jsxRuntime_js.jsx(KTX$1, { ref: combined, ...attrs }));
139
+ });
140
+
141
+ /**
142
+ * Wait for several things to be rendered
143
+ */
144
+ const RenderGroup = react.forwardRef(function RenderGroup(props, ref) {
145
+ const [ready, resolve] = react$1.usePromise();
146
+ // handle
147
+ react.useImperativeHandle(ref, () => ({ ready }));
148
+ const elements = react.useRef([]);
149
+ const promises = react.useRef([]);
150
+ // reparsing
151
+ const player = liqvid.usePlayer();
152
+ react.useEffect(() => {
153
+ // promises
154
+ Promise.all(promises.current).then(() => {
155
+ // reparse
156
+ if (props.reparse) {
157
+ player.reparseTree(leastCommonAncestor(elements.current));
158
+ }
159
+ // ready()
160
+ resolve();
161
+ });
162
+ }, []);
163
+ return react$1.recursiveMap(props.children, node => {
164
+ if (shouldInspect(node)) {
165
+ const originalRef = node.ref;
166
+ return react.cloneElement(node, {
167
+ ref: (ref) => {
168
+ if (!ref)
169
+ return;
170
+ elements.current.push(ref.domElement);
171
+ promises.current.push(ref.ready);
172
+ // pass along original ref
173
+ if (typeof originalRef === "function") {
174
+ originalRef(ref);
175
+ }
176
+ else if (originalRef && typeof originalRef === "object") {
177
+ originalRef.current = ref;
178
+ }
179
+ }
180
+ });
181
+ }
182
+ return node;
183
+ });
184
+ });
185
+ /**
186
+ * Determine whether the node is a <KTX> element
187
+ * @param node Element to check
188
+ */
189
+ function shouldInspect(node) {
190
+ return react.isValidElement(node) && typeof node.type === "object" && (node.type === KTX || node.type === KTX$1);
191
+ }
192
+ /**
193
+ * Find least common ancestor of an array of elements
194
+ * @param elements Elements
195
+ * @returns Deepest node containing all passed elements
196
+ */
197
+ function leastCommonAncestor(elements) {
198
+ if (elements.length === 0) {
199
+ throw new Error("Must pass at least one element");
200
+ }
201
+ let ancestor = elements[0];
202
+ let failing = elements.slice(1);
203
+ while (failing.length > 0) {
204
+ ancestor = ancestor.parentElement;
205
+ failing = failing.filter(node => !ancestor.contains(node));
206
+ }
207
+ return ancestor;
208
+ }
209
+
210
+ exports.KTX = KTX;
211
+ exports.KaTeXReady = KaTeXReady;
212
+ exports.RenderGroup = RenderGroup;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,67 @@
1
- export { KTXBlocking as KTX, KTXBlocking } from "./Blocking";
2
- export { KaTeXReady } from "./loading";
3
- export { Handle, KTXNonBlocking } from "./NonBlocking";
4
- export { RenderGroup } from "./RenderGroup";
1
+ /// <reference types="react" />
2
+ import * as react from 'react';
3
+ import * as katex from 'katex';
4
+
5
+ /**
6
+ * KTX element API
7
+ */
8
+ interface Handle$1 {
9
+ /** The underlying <span> element */
10
+ domElement: HTMLSpanElement;
11
+ /** Promise that resolves once typesetting is finished */
12
+ ready: Promise<void>;
13
+ }
14
+ interface Props$2 extends React.HTMLAttributes<HTMLSpanElement> {
15
+ /**
16
+ * Whether to render in display style
17
+ * @default false
18
+ */
19
+ display?: boolean;
20
+ }
21
+ /** Component for KaTeX code */
22
+ declare const KTX$1: react.ForwardRefExoticComponent<Props$2 & react.RefAttributes<Handle$1>>;
23
+
24
+ interface Props$1 extends React.ComponentProps<typeof KTX$1> {
25
+ /**
26
+ * Player events to obstruct
27
+ * @default "canplay canplaythrough"
28
+ */
29
+ obstruct?: string;
30
+ /**
31
+ * Whether to reparse descendants for `during()` and `from()`
32
+ * @default false
33
+ */
34
+ reparse?: boolean;
35
+ }
36
+ /** Component for KaTeX code */
37
+ declare const KTX: react.ForwardRefExoticComponent<Pick<Props$1, "hidden" | "color" | "style" | "display" | "translate" | "slot" | "title" | "children" | "key" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "className" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "id" | "lang" | "placeholder" | "spellCheck" | "tabIndex" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "obstruct" | "reparse"> & react.RefAttributes<Handle$1>>;
38
+
39
+ /**
40
+ * Ready Promise
41
+ */
42
+ declare const KaTeXReady: Promise<[typeof katex, {
43
+ [key: string]: string;
44
+ }]>;
45
+
46
+ /** RenderGroup element API */
47
+ interface Handle {
48
+ /** Promise that resolves once all KTX descendants have finished typesetting */
49
+ ready: Promise<void>;
50
+ }
51
+ interface Props {
52
+ /**
53
+ * Whether to reparse descendants for `during()` and `from()`
54
+ * @default false
55
+ */
56
+ reparse?: boolean;
57
+ }
58
+ /**
59
+ * Wait for several things to be rendered
60
+ */
61
+ declare const RenderGroup: react.ForwardRefExoticComponent<Props & react.RefAttributes<Handle>>;
62
+
5
63
  declare global {
6
64
  const katex: typeof katex;
7
65
  }
66
+
67
+ export { Handle$1 as Handle, KTX, KaTeXReady, RenderGroup };
package/dist/index.mjs ADDED
@@ -0,0 +1,206 @@
1
+ import { jsx } from 'react/jsx-runtime.js';
2
+ import { usePromise, combineRefs, recursiveMap } from '@liqvid/utils/react';
3
+ import { usePlayer } from 'liqvid';
4
+ import { forwardRef, useRef, useImperativeHandle, useEffect, isValidElement, cloneElement } from 'react';
5
+
6
+ // option of loading KaTeX asynchronously
7
+ const KaTeXLoad = new Promise((resolve) => {
8
+ const script = document.querySelector("script[src*=\"katex.js\"], script[src*=\"katex.min.js\"]");
9
+ if (!script)
10
+ return;
11
+ if (window.hasOwnProperty("katex")) {
12
+ resolve(katex);
13
+ }
14
+ else {
15
+ script.addEventListener("load", () => resolve(katex));
16
+ }
17
+ });
18
+ // load macros from <head>
19
+ const KaTeXMacros = new Promise((resolve) => {
20
+ const macros = {};
21
+ const scripts = Array.from(document.querySelectorAll("head > script[type='math/tex']"));
22
+ return Promise.all(scripts.map(script => fetch(script.src)
23
+ .then(res => {
24
+ if (res.ok)
25
+ return res.text();
26
+ throw new Error(`${res.status} ${res.statusText}: ${script.src}`);
27
+ })
28
+ .then(tex => {
29
+ Object.assign(macros, parseMacros(tex));
30
+ }))).then(() => resolve(macros));
31
+ });
32
+ /**
33
+ * Ready Promise
34
+ */
35
+ const KaTeXReady = Promise.all([KaTeXLoad, KaTeXMacros]);
36
+ /**
37
+ * Parse \newcommand macros in a file.
38
+ * Also supports \ktxnewcommand (for use in conjunction with MathJax).
39
+ * @param file TeX file to parse
40
+ */
41
+ function parseMacros(file) {
42
+ const macros = {};
43
+ const rgx = /\\(?:ktx)?newcommand\{(.+?)\}(?:\[\d+\])?\{/g;
44
+ let match;
45
+ while (match = rgx.exec(file)) {
46
+ let body = "";
47
+ const macro = match[1];
48
+ let braceCount = 1;
49
+ for (let i = match.index + match[0].length; (braceCount > 0) && (i < file.length); ++i) {
50
+ const char = file[i];
51
+ if (char === "{") {
52
+ braceCount++;
53
+ }
54
+ else if (char === "}") {
55
+ braceCount--;
56
+ if (braceCount === 0)
57
+ break;
58
+ }
59
+ else if (char === "\\") {
60
+ body += file.slice(i, i + 2);
61
+ ++i;
62
+ continue;
63
+ }
64
+ body += char;
65
+ }
66
+ macros[macro] = body;
67
+ }
68
+ return macros;
69
+ }
70
+
71
+ /** Component for KaTeX code */
72
+ const KTX$1 = forwardRef(function KTX(props, ref) {
73
+ const spanRef = useRef();
74
+ const { children, display = false, ...attrs } = props;
75
+ const [ready, resolve] = usePromise();
76
+ // handle
77
+ useImperativeHandle(ref, () => ({
78
+ domElement: spanRef.current,
79
+ ready
80
+ }));
81
+ useEffect(() => {
82
+ KaTeXReady.then(([katex, macros]) => {
83
+ katex.render(children.toString(), spanRef.current, {
84
+ displayMode: !!display,
85
+ macros,
86
+ strict: "ignore",
87
+ throwOnError: false,
88
+ trust: true
89
+ });
90
+ /* move katex into placeholder element */
91
+ const child = spanRef.current.firstElementChild;
92
+ // copy classes
93
+ for (let i = 0, len = child.classList.length; i < len; ++i) {
94
+ spanRef.current.classList.add(child.classList.item(i));
95
+ }
96
+ // move children
97
+ while (child.childNodes.length > 0) {
98
+ spanRef.current.appendChild(child.firstChild);
99
+ }
100
+ // delete child
101
+ child.remove();
102
+ // resolve promise
103
+ resolve();
104
+ });
105
+ }, [children]);
106
+ // Google Chrome fails without this
107
+ if (display) {
108
+ if (!attrs.style)
109
+ attrs.style = {};
110
+ attrs.style.display = "block";
111
+ }
112
+ return (jsx("span", { ...attrs, ref: spanRef }));
113
+ });
114
+
115
+ /** Component for KaTeX code */
116
+ const KTX = forwardRef(function KTX(props, ref) {
117
+ const { obstruct = "canplay canplaythrough", reparse = false, ...attrs } = props;
118
+ const plain = useRef();
119
+ const combined = combineRefs(plain, ref);
120
+ const player = usePlayer();
121
+ useEffect(() => {
122
+ // obstruction
123
+ if (obstruct.match(/\bcanplay\b/)) {
124
+ player.obstruct("canplay", plain.current.ready);
125
+ }
126
+ if (obstruct.match("canplaythrough")) {
127
+ player.obstruct("canplaythrough", plain.current.ready);
128
+ }
129
+ // reparsing
130
+ if (reparse) {
131
+ plain.current.ready.then(() => player.reparseTree(plain.current.domElement));
132
+ }
133
+ }, []);
134
+ return (jsx(KTX$1, { ref: combined, ...attrs }));
135
+ });
136
+
137
+ /**
138
+ * Wait for several things to be rendered
139
+ */
140
+ const RenderGroup = forwardRef(function RenderGroup(props, ref) {
141
+ const [ready, resolve] = usePromise();
142
+ // handle
143
+ useImperativeHandle(ref, () => ({ ready }));
144
+ const elements = useRef([]);
145
+ const promises = useRef([]);
146
+ // reparsing
147
+ const player = usePlayer();
148
+ useEffect(() => {
149
+ // promises
150
+ Promise.all(promises.current).then(() => {
151
+ // reparse
152
+ if (props.reparse) {
153
+ player.reparseTree(leastCommonAncestor(elements.current));
154
+ }
155
+ // ready()
156
+ resolve();
157
+ });
158
+ }, []);
159
+ return recursiveMap(props.children, node => {
160
+ if (shouldInspect(node)) {
161
+ const originalRef = node.ref;
162
+ return cloneElement(node, {
163
+ ref: (ref) => {
164
+ if (!ref)
165
+ return;
166
+ elements.current.push(ref.domElement);
167
+ promises.current.push(ref.ready);
168
+ // pass along original ref
169
+ if (typeof originalRef === "function") {
170
+ originalRef(ref);
171
+ }
172
+ else if (originalRef && typeof originalRef === "object") {
173
+ originalRef.current = ref;
174
+ }
175
+ }
176
+ });
177
+ }
178
+ return node;
179
+ });
180
+ });
181
+ /**
182
+ * Determine whether the node is a <KTX> element
183
+ * @param node Element to check
184
+ */
185
+ function shouldInspect(node) {
186
+ return isValidElement(node) && typeof node.type === "object" && (node.type === KTX || node.type === KTX$1);
187
+ }
188
+ /**
189
+ * Find least common ancestor of an array of elements
190
+ * @param elements Elements
191
+ * @returns Deepest node containing all passed elements
192
+ */
193
+ function leastCommonAncestor(elements) {
194
+ if (elements.length === 0) {
195
+ throw new Error("Must pass at least one element");
196
+ }
197
+ let ancestor = elements[0];
198
+ let failing = elements.slice(1);
199
+ while (failing.length > 0) {
200
+ ancestor = ancestor.parentElement;
201
+ failing = failing.filter(node => !ancestor.contains(node));
202
+ }
203
+ return ancestor;
204
+ }
205
+
206
+ export { KTX, KaTeXReady, RenderGroup };
package/dist/plain.cjs ADDED
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var jsxRuntime_js = require('react/jsx-runtime.js');
6
+ var react$1 = require('@liqvid/utils/react');
7
+ var react = require('react');
8
+
9
+ // option of loading KaTeX asynchronously
10
+ const KaTeXLoad = new Promise((resolve) => {
11
+ const script = document.querySelector("script[src*=\"katex.js\"], script[src*=\"katex.min.js\"]");
12
+ if (!script)
13
+ return;
14
+ if (window.hasOwnProperty("katex")) {
15
+ resolve(katex);
16
+ }
17
+ else {
18
+ script.addEventListener("load", () => resolve(katex));
19
+ }
20
+ });
21
+ // load macros from <head>
22
+ const KaTeXMacros = new Promise((resolve) => {
23
+ const macros = {};
24
+ const scripts = Array.from(document.querySelectorAll("head > script[type='math/tex']"));
25
+ return Promise.all(scripts.map(script => fetch(script.src)
26
+ .then(res => {
27
+ if (res.ok)
28
+ return res.text();
29
+ throw new Error(`${res.status} ${res.statusText}: ${script.src}`);
30
+ })
31
+ .then(tex => {
32
+ Object.assign(macros, parseMacros(tex));
33
+ }))).then(() => resolve(macros));
34
+ });
35
+ /**
36
+ * Ready Promise
37
+ */
38
+ const KaTeXReady = Promise.all([KaTeXLoad, KaTeXMacros]);
39
+ /**
40
+ * Parse \newcommand macros in a file.
41
+ * Also supports \ktxnewcommand (for use in conjunction with MathJax).
42
+ * @param file TeX file to parse
43
+ */
44
+ function parseMacros(file) {
45
+ const macros = {};
46
+ const rgx = /\\(?:ktx)?newcommand\{(.+?)\}(?:\[\d+\])?\{/g;
47
+ let match;
48
+ while (match = rgx.exec(file)) {
49
+ let body = "";
50
+ const macro = match[1];
51
+ let braceCount = 1;
52
+ for (let i = match.index + match[0].length; (braceCount > 0) && (i < file.length); ++i) {
53
+ const char = file[i];
54
+ if (char === "{") {
55
+ braceCount++;
56
+ }
57
+ else if (char === "}") {
58
+ braceCount--;
59
+ if (braceCount === 0)
60
+ break;
61
+ }
62
+ else if (char === "\\") {
63
+ body += file.slice(i, i + 2);
64
+ ++i;
65
+ continue;
66
+ }
67
+ body += char;
68
+ }
69
+ macros[macro] = body;
70
+ }
71
+ return macros;
72
+ }
73
+
74
+ /** Component for KaTeX code */
75
+ const KTX = react.forwardRef(function KTX(props, ref) {
76
+ const spanRef = react.useRef();
77
+ const { children, display = false, ...attrs } = props;
78
+ const [ready, resolve] = react$1.usePromise();
79
+ // handle
80
+ react.useImperativeHandle(ref, () => ({
81
+ domElement: spanRef.current,
82
+ ready
83
+ }));
84
+ react.useEffect(() => {
85
+ KaTeXReady.then(([katex, macros]) => {
86
+ katex.render(children.toString(), spanRef.current, {
87
+ displayMode: !!display,
88
+ macros,
89
+ strict: "ignore",
90
+ throwOnError: false,
91
+ trust: true
92
+ });
93
+ /* move katex into placeholder element */
94
+ const child = spanRef.current.firstElementChild;
95
+ // copy classes
96
+ for (let i = 0, len = child.classList.length; i < len; ++i) {
97
+ spanRef.current.classList.add(child.classList.item(i));
98
+ }
99
+ // move children
100
+ while (child.childNodes.length > 0) {
101
+ spanRef.current.appendChild(child.firstChild);
102
+ }
103
+ // delete child
104
+ child.remove();
105
+ // resolve promise
106
+ resolve();
107
+ });
108
+ }, [children]);
109
+ // Google Chrome fails without this
110
+ if (display) {
111
+ if (!attrs.style)
112
+ attrs.style = {};
113
+ attrs.style.display = "block";
114
+ }
115
+ return (jsxRuntime_js.jsx("span", { ...attrs, ref: spanRef }));
116
+ });
117
+
118
+ exports.KTX = KTX;
@@ -0,0 +1,23 @@
1
+ /// <reference types="react" />
2
+ import * as react from 'react';
3
+
4
+ /**
5
+ * KTX element API
6
+ */
7
+ interface Handle {
8
+ /** The underlying <span> element */
9
+ domElement: HTMLSpanElement;
10
+ /** Promise that resolves once typesetting is finished */
11
+ ready: Promise<void>;
12
+ }
13
+ interface Props extends React.HTMLAttributes<HTMLSpanElement> {
14
+ /**
15
+ * Whether to render in display style
16
+ * @default false
17
+ */
18
+ display?: boolean;
19
+ }
20
+ /** Component for KaTeX code */
21
+ declare const KTX: react.ForwardRefExoticComponent<Props & react.RefAttributes<Handle>>;
22
+
23
+ export { Handle, KTX };
package/dist/plain.mjs ADDED
@@ -0,0 +1,114 @@
1
+ import { jsx } from 'react/jsx-runtime.js';
2
+ import { usePromise } from '@liqvid/utils/react';
3
+ import { forwardRef, useRef, useImperativeHandle, useEffect } from 'react';
4
+
5
+ // option of loading KaTeX asynchronously
6
+ const KaTeXLoad = new Promise((resolve) => {
7
+ const script = document.querySelector("script[src*=\"katex.js\"], script[src*=\"katex.min.js\"]");
8
+ if (!script)
9
+ return;
10
+ if (window.hasOwnProperty("katex")) {
11
+ resolve(katex);
12
+ }
13
+ else {
14
+ script.addEventListener("load", () => resolve(katex));
15
+ }
16
+ });
17
+ // load macros from <head>
18
+ const KaTeXMacros = new Promise((resolve) => {
19
+ const macros = {};
20
+ const scripts = Array.from(document.querySelectorAll("head > script[type='math/tex']"));
21
+ return Promise.all(scripts.map(script => fetch(script.src)
22
+ .then(res => {
23
+ if (res.ok)
24
+ return res.text();
25
+ throw new Error(`${res.status} ${res.statusText}: ${script.src}`);
26
+ })
27
+ .then(tex => {
28
+ Object.assign(macros, parseMacros(tex));
29
+ }))).then(() => resolve(macros));
30
+ });
31
+ /**
32
+ * Ready Promise
33
+ */
34
+ const KaTeXReady = Promise.all([KaTeXLoad, KaTeXMacros]);
35
+ /**
36
+ * Parse \newcommand macros in a file.
37
+ * Also supports \ktxnewcommand (for use in conjunction with MathJax).
38
+ * @param file TeX file to parse
39
+ */
40
+ function parseMacros(file) {
41
+ const macros = {};
42
+ const rgx = /\\(?:ktx)?newcommand\{(.+?)\}(?:\[\d+\])?\{/g;
43
+ let match;
44
+ while (match = rgx.exec(file)) {
45
+ let body = "";
46
+ const macro = match[1];
47
+ let braceCount = 1;
48
+ for (let i = match.index + match[0].length; (braceCount > 0) && (i < file.length); ++i) {
49
+ const char = file[i];
50
+ if (char === "{") {
51
+ braceCount++;
52
+ }
53
+ else if (char === "}") {
54
+ braceCount--;
55
+ if (braceCount === 0)
56
+ break;
57
+ }
58
+ else if (char === "\\") {
59
+ body += file.slice(i, i + 2);
60
+ ++i;
61
+ continue;
62
+ }
63
+ body += char;
64
+ }
65
+ macros[macro] = body;
66
+ }
67
+ return macros;
68
+ }
69
+
70
+ /** Component for KaTeX code */
71
+ const KTX = forwardRef(function KTX(props, ref) {
72
+ const spanRef = useRef();
73
+ const { children, display = false, ...attrs } = props;
74
+ const [ready, resolve] = usePromise();
75
+ // handle
76
+ useImperativeHandle(ref, () => ({
77
+ domElement: spanRef.current,
78
+ ready
79
+ }));
80
+ useEffect(() => {
81
+ KaTeXReady.then(([katex, macros]) => {
82
+ katex.render(children.toString(), spanRef.current, {
83
+ displayMode: !!display,
84
+ macros,
85
+ strict: "ignore",
86
+ throwOnError: false,
87
+ trust: true
88
+ });
89
+ /* move katex into placeholder element */
90
+ const child = spanRef.current.firstElementChild;
91
+ // copy classes
92
+ for (let i = 0, len = child.classList.length; i < len; ++i) {
93
+ spanRef.current.classList.add(child.classList.item(i));
94
+ }
95
+ // move children
96
+ while (child.childNodes.length > 0) {
97
+ spanRef.current.appendChild(child.firstChild);
98
+ }
99
+ // delete child
100
+ child.remove();
101
+ // resolve promise
102
+ resolve();
103
+ });
104
+ }, [children]);
105
+ // Google Chrome fails without this
106
+ if (display) {
107
+ if (!attrs.style)
108
+ attrs.style = {};
109
+ attrs.style.display = "block";
110
+ }
111
+ return (jsx("span", { ...attrs, ref: spanRef }));
112
+ });
113
+
114
+ export { KTX };
package/package.json CHANGED
@@ -1,17 +1,24 @@
1
1
  {
2
2
  "name": "@liqvid/katex",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "KaTeX integration for Liqvid",
5
5
  "files": [
6
6
  "dist/*"
7
7
  ],
8
8
  "exports": {
9
- ".": "./dist/index.js"
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.cjs"
12
+ },
13
+ "./plain": {
14
+ "import": "./dist/plain.mjs",
15
+ "require": "./dist/plain.cjs"
16
+ }
10
17
  },
11
18
  "typesVersions": {
12
19
  "*": {
13
20
  "*": [
14
- "./dist/*"
21
+ "./dist/types/*"
15
22
  ]
16
23
  }
17
24
  },
@@ -30,9 +37,10 @@
30
37
  "homepage": "https://github.com/liqvidjs/liqvid/tree/main/packages/katex",
31
38
  "license": "MIT",
32
39
  "peerDependencies": {
33
- "@types/react": "^17.0.39",
34
- "liqvid": "2.1.0-beta.3",
35
- "react": "^17.0.2"
40
+ "@types/katex": "^0.11.1",
41
+ "@types/react": ">=17.0.0",
42
+ "liqvid": "^2.1.4",
43
+ "react": ">=17.0.0"
36
44
  },
37
45
  "peerDependenciesMeta": {
38
46
  "liqvid": {
@@ -40,18 +48,17 @@
40
48
  }
41
49
  },
42
50
  "devDependencies": {
43
- "@types/react": "^17.0.11",
44
- "@typescript-eslint/eslint-plugin": "^5.13.0",
45
- "@typescript-eslint/parser": "^5.13.0",
46
- "@yuri/eslint-config": "^1.0.1",
47
- "eslint": "^8.10.0",
48
- "eslint-plugin-react": "^7.29.3",
49
- "liqvid": "2.1.0-beta.3",
50
- "react": "^17.0.2",
51
- "typescript": "^4.6.2"
51
+ "liqvid": "^2.1.4"
52
52
  },
53
53
  "dependencies": {
54
- "@types/events": "^3.0.0",
55
- "@types/katex": "^0.11.1"
56
- }
57
- }
54
+ "@liqvid/utils": "^1.3.0"
55
+ },
56
+ "scripts": {
57
+ "build": "pnpm build:clean && pnpm build:js && pnpm build:postclean",
58
+ "build:clean": "rm -rf dist",
59
+ "build:js": "tsc && node ../../build.mjs && rollup -c",
60
+ "build:postclean": "rm -rf dist/esm dist/types dist/tsconfig.tsbuildinfo",
61
+ "lint": "eslint --ext ts,tsx --fix src"
62
+ },
63
+ "readme": "# @liqvid/katex\n\n[KaTeX](https://katex.org/) integration for [Liqvid](https://liqvidjs.org). See https://liqvidjs.org/docs/integrations/katex/ for documentation.\n"
64
+ }
@@ -1,2 +0,0 @@
1
- /// <reference types="react" />
2
- export declare const KTXBlocking: import("react").ForwardRefExoticComponent<Pick<Props, string | number | symbol> & import("react").RefAttributes<Handle>>;
package/dist/Blocking.js DELETED
@@ -1,32 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { usePlayer } from "liqvid";
3
- import { forwardRef, useEffect, useMemo, useRef } from "react";
4
- import { KTXNonBlocking } from "./NonBlocking";
5
- export const KTXBlocking = forwardRef(function KTX(props, ref) {
6
- const player = usePlayer();
7
- const innerRef = useRef();
8
- if (typeof ref === "function") {
9
- ref(innerRef.current);
10
- }
11
- else if (ref) {
12
- ref.current = innerRef.current;
13
- }
14
- const resolve = useRef(null);
15
- useMemo(() => {
16
- const promise = new Promise((res) => {
17
- resolve.current = res;
18
- });
19
- player.obstruct("canplay", promise);
20
- player.obstruct("canplaythrough", promise);
21
- }, []);
22
- useEffect(() => {
23
- if (typeof ref === "function") {
24
- ref(innerRef.current);
25
- }
26
- else if (ref) {
27
- ref.current = innerRef.current;
28
- }
29
- innerRef.current.ready.then(() => resolve.current());
30
- }, []);
31
- return (_jsx(KTXNonBlocking, Object.assign({ ref: innerRef }, props)));
32
- });
@@ -1,9 +0,0 @@
1
- /// <reference types="react" />
2
- export interface Handle {
3
- domElement: HTMLSpanElement;
4
- ready: Promise<void>;
5
- }
6
- export declare const KTXNonBlocking: import("react").ForwardRefExoticComponent<{
7
- display?: boolean;
8
- reparse?: boolean;
9
- } & import("react").HTMLAttributes<HTMLSpanElement> & import("react").RefAttributes<Handle>>;
@@ -1,60 +0,0 @@
1
- var __rest = (this && this.__rest) || function (s, e) {
2
- var t = {};
3
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
- t[p] = s[p];
5
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
- t[p[i]] = s[p[i]];
9
- }
10
- return t;
11
- };
12
- import { jsx as _jsx } from "react/jsx-runtime";
13
- import { usePlayer } from "liqvid";
14
- import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from "react";
15
- import { KaTeXReady } from "./loading";
16
- const implementation = function KTX(props, ref) {
17
- const spanRef = useRef();
18
- const { children, display, reparse } = props, attrs = __rest(props, ["children", "display", "reparse"]);
19
- const resolveRef = useRef();
20
- const player = usePlayer();
21
- const ready = useMemo(() => {
22
- return new Promise((resolve) => {
23
- resolveRef.current = resolve;
24
- });
25
- }, []);
26
- useImperativeHandle(ref, () => ({
27
- domElement: spanRef.current,
28
- ready
29
- }));
30
- useEffect(() => {
31
- KaTeXReady.then(([katex, macros]) => {
32
- katex.render(children.toString(), spanRef.current, {
33
- displayMode: !!display,
34
- macros,
35
- strict: "ignore",
36
- throwOnError: false,
37
- trust: true
38
- });
39
- const child = spanRef.current.firstElementChild;
40
- for (let i = 0, len = child.classList.length; i < len; ++i) {
41
- spanRef.current.classList.add(child.classList.item(i));
42
- }
43
- while (child.childNodes.length > 0) {
44
- spanRef.current.appendChild(child.firstChild);
45
- }
46
- child.remove();
47
- if (reparse) {
48
- player.reparseTree(spanRef.current);
49
- }
50
- resolveRef.current();
51
- });
52
- }, [children]);
53
- if (display) {
54
- if (!attrs.style)
55
- attrs.style = {};
56
- attrs.style.display = "block";
57
- }
58
- return (_jsx("span", Object.assign({}, attrs, { ref: spanRef })));
59
- };
60
- export const KTXNonBlocking = forwardRef(implementation);
@@ -1,10 +0,0 @@
1
- /// <reference types="react" />
2
- interface Handle {
3
- ready: Promise<void>;
4
- }
5
- interface Props {
6
- reparse?: boolean;
7
- }
8
- export declare const RenderGroup: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<Handle>>;
9
- export declare function recursiveMap(children: React.ReactNode, fn: (child: React.ReactNode) => React.ReactNode): React.ReactNode;
10
- export {};
@@ -1,76 +0,0 @@
1
- import { Children, cloneElement, forwardRef, isValidElement, useEffect, useMemo, useRef, useImperativeHandle } from "react";
2
- import { usePlayer } from "liqvid";
3
- import { KTXNonBlocking } from "./NonBlocking";
4
- import { KTXBlocking } from "./Blocking";
5
- export const RenderGroup = forwardRef(function RenderGroup(props, ref) {
6
- const [ready, resolve] = usePromise();
7
- useImperativeHandle(ref, () => ({ ready }));
8
- const elements = useRef([]);
9
- const promises = useRef([]);
10
- const player = usePlayer();
11
- useEffect(() => {
12
- Promise.all(promises.current).then(() => {
13
- if (props.reparse) {
14
- player.reparseTree(leastCommonAncestor(elements.current));
15
- }
16
- resolve();
17
- });
18
- }, []);
19
- return recursiveMap(props.children, node => {
20
- if (shouldInspect(node)) {
21
- const originalRef = node.ref;
22
- return cloneElement(node, {
23
- ref: (ref) => {
24
- if (!ref)
25
- return;
26
- elements.current.push(ref.domElement);
27
- promises.current.push(ref.ready);
28
- if (typeof originalRef === "function") {
29
- originalRef(ref);
30
- }
31
- else if (originalRef && typeof originalRef === "object") {
32
- originalRef.current = ref;
33
- }
34
- }
35
- });
36
- }
37
- return node;
38
- });
39
- });
40
- function shouldInspect(node) {
41
- return isValidElement(node) && typeof node.type === "object" && (node.type === KTXBlocking || node.type === KTXNonBlocking);
42
- }
43
- export function recursiveMap(children, fn) {
44
- return Children.map(children, (child) => {
45
- if (!isValidElement(child)) {
46
- return child;
47
- }
48
- if ("children" in child.props) {
49
- child = cloneElement(child, {
50
- children: recursiveMap(child.props.children, fn)
51
- });
52
- }
53
- return fn(child);
54
- });
55
- }
56
- function usePromise(deps = []) {
57
- const resolve = useRef();
58
- const reject = useRef();
59
- const promise = useMemo(() => new Promise((res, rej) => {
60
- resolve.current = res;
61
- reject.current = rej;
62
- }), deps);
63
- return [promise, resolve.current, reject.current];
64
- }
65
- function leastCommonAncestor(elements) {
66
- if (elements.length === 0) {
67
- throw new Error("Must pass at least one element");
68
- }
69
- let ancestor = elements[0];
70
- let failing = elements.slice(1);
71
- while (failing.length > 0) {
72
- ancestor = ancestor.parentElement;
73
- failing = failing.filter(node => !ancestor.contains(node));
74
- }
75
- return ancestor;
76
- }
package/dist/index.js DELETED
@@ -1,4 +0,0 @@
1
- export { KTXBlocking as KTX, KTXBlocking } from "./Blocking";
2
- export { KaTeXReady } from "./loading";
3
- export { KTXNonBlocking } from "./NonBlocking";
4
- export { RenderGroup } from "./RenderGroup";
package/dist/loading.d.ts DELETED
@@ -1,3 +0,0 @@
1
- export declare const KaTeXReady: Promise<[typeof import("katex"), {
2
- [key: string]: string;
3
- }]>;
package/dist/loading.js DELETED
@@ -1,54 +0,0 @@
1
- const KaTeXLoad = new Promise((resolve) => {
2
- const script = document.querySelector(`script[src*="katex.js"], script[src*="katex.min.js"]`);
3
- if (!script)
4
- return;
5
- if (window.hasOwnProperty("katex")) {
6
- resolve(katex);
7
- }
8
- else {
9
- script.addEventListener("load", () => resolve(katex));
10
- }
11
- });
12
- const KaTeXMacros = new Promise((resolve) => {
13
- const macros = {};
14
- const scripts = Array.from(document.querySelectorAll("head > script[type='math/tex']"));
15
- return Promise.all(scripts.map(script => fetch(script.src)
16
- .then(res => {
17
- if (res.ok)
18
- return res.text();
19
- throw new Error(`${res.status} ${res.statusText}: ${script.src}`);
20
- })
21
- .then(tex => {
22
- Object.assign(macros, parseMacros(tex));
23
- }))).then(() => resolve(macros));
24
- });
25
- export const KaTeXReady = Promise.all([KaTeXLoad, KaTeXMacros]);
26
- function parseMacros(file) {
27
- const macros = {};
28
- const rgx = /\\(?:ktx)?newcommand\{(.+?)\}(?:\[\d+\])?\{/g;
29
- let match;
30
- while (match = rgx.exec(file)) {
31
- let body = "";
32
- const macro = match[1];
33
- let braceCount = 1;
34
- for (let i = match.index + match[0].length; (braceCount > 0) && (i < file.length); ++i) {
35
- const char = file[i];
36
- if (char === "{") {
37
- braceCount++;
38
- }
39
- else if (char === "}") {
40
- braceCount--;
41
- if (braceCount === 0)
42
- break;
43
- }
44
- else if (char === "\\") {
45
- body += file.slice(i, i + 2);
46
- ++i;
47
- continue;
48
- }
49
- body += char;
50
- }
51
- macros[macro] = body;
52
- }
53
- return macros;
54
- }