@pie-lib/math-input-mathlive 1.0.0

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.
Files changed (110) hide show
  1. package/README.md +119 -0
  2. package/lib/__tests__/index.test.js +118 -0
  3. package/lib/__tests__/index.test.js.map +1 -0
  4. package/lib/__tests__/latex-bridge.test.js +186 -0
  5. package/lib/__tests__/latex-bridge.test.js.map +1 -0
  6. package/lib/__tests__/mathlive-instance.test.js +137 -0
  7. package/lib/__tests__/mathlive-instance.test.js.map +1 -0
  8. package/lib/horizontal-keypad.js +101 -0
  9. package/lib/horizontal-keypad.js.map +1 -0
  10. package/lib/index.js +143 -0
  11. package/lib/index.js.map +1 -0
  12. package/lib/keypad/index.js +365 -0
  13. package/lib/keypad/index.js.map +1 -0
  14. package/lib/keypad/keys-layout.js +22 -0
  15. package/lib/keypad/keys-layout.js.map +1 -0
  16. package/lib/keys/basic-operators.js +33 -0
  17. package/lib/keys/basic-operators.js.map +1 -0
  18. package/lib/keys/chars.js +12 -0
  19. package/lib/keys/chars.js.map +1 -0
  20. package/lib/keys/comparison.js +39 -0
  21. package/lib/keys/comparison.js.map +1 -0
  22. package/lib/keys/constants.js +37 -0
  23. package/lib/keys/constants.js.map +1 -0
  24. package/lib/keys/digits.js +46 -0
  25. package/lib/keys/digits.js.map +1 -0
  26. package/lib/keys/edit.js +14 -0
  27. package/lib/keys/edit.js.map +1 -0
  28. package/lib/keys/exponent.js +30 -0
  29. package/lib/keys/exponent.js.map +1 -0
  30. package/lib/keys/fractions.js +29 -0
  31. package/lib/keys/fractions.js.map +1 -0
  32. package/lib/keys/geometry.js +140 -0
  33. package/lib/keys/geometry.js.map +1 -0
  34. package/lib/keys/grades.js +259 -0
  35. package/lib/keys/grades.js.map +1 -0
  36. package/lib/keys/index.js +35 -0
  37. package/lib/keys/index.js.map +1 -0
  38. package/lib/keys/log.js +27 -0
  39. package/lib/keys/log.js.map +1 -0
  40. package/lib/keys/logic.js +19 -0
  41. package/lib/keys/logic.js.map +1 -0
  42. package/lib/keys/matrices.js +19 -0
  43. package/lib/keys/matrices.js.map +1 -0
  44. package/lib/keys/misc.js +62 -0
  45. package/lib/keys/misc.js.map +1 -0
  46. package/lib/keys/navigation.js +20 -0
  47. package/lib/keys/navigation.js.map +1 -0
  48. package/lib/keys/operators.js +15 -0
  49. package/lib/keys/operators.js.map +1 -0
  50. package/lib/keys/statistics.js +40 -0
  51. package/lib/keys/statistics.js.map +1 -0
  52. package/lib/keys/sub-sup.js +19 -0
  53. package/lib/keys/sub-sup.js.map +1 -0
  54. package/lib/keys/trigonometry.js +45 -0
  55. package/lib/keys/trigonometry.js.map +1 -0
  56. package/lib/keys/utils.js +87 -0
  57. package/lib/keys/utils.js.map +1 -0
  58. package/lib/keys/vars.js +24 -0
  59. package/lib/keys/vars.js.map +1 -0
  60. package/lib/latex-bridge.js +195 -0
  61. package/lib/latex-bridge.js.map +1 -0
  62. package/lib/math-input.js +143 -0
  63. package/lib/math-input.js.map +1 -0
  64. package/lib/mathlive-instance.js +319 -0
  65. package/lib/mathlive-instance.js.map +1 -0
  66. package/lib/mf/common-styles.js +111 -0
  67. package/lib/mf/common-styles.js.map +1 -0
  68. package/lib/mf/index.js +72 -0
  69. package/lib/mf/index.js.map +1 -0
  70. package/lib/mf/input.js +266 -0
  71. package/lib/mf/input.js.map +1 -0
  72. package/lib/mf/static.js +396 -0
  73. package/lib/mf/static.js.map +1 -0
  74. package/package.json +31 -0
  75. package/src/__tests__/index.test.jsx +109 -0
  76. package/src/__tests__/latex-bridge.test.js +151 -0
  77. package/src/__tests__/mathlive-instance.test.js +130 -0
  78. package/src/horizontal-keypad.jsx +73 -0
  79. package/src/index.jsx +63 -0
  80. package/src/keypad/index.jsx +335 -0
  81. package/src/keypad/keys-layout.js +16 -0
  82. package/src/keys/basic-operators.js +32 -0
  83. package/src/keys/chars.js +5 -0
  84. package/src/keys/comparison.js +28 -0
  85. package/src/keys/constants.js +35 -0
  86. package/src/keys/digits.js +40 -0
  87. package/src/keys/edit.js +3 -0
  88. package/src/keys/exponent.js +28 -0
  89. package/src/keys/fractions.js +26 -0
  90. package/src/keys/geometry.js +144 -0
  91. package/src/keys/grades.js +367 -0
  92. package/src/keys/index.js +20 -0
  93. package/src/keys/log.js +22 -0
  94. package/src/keys/logic.js +15 -0
  95. package/src/keys/matrices.js +15 -0
  96. package/src/keys/misc.js +65 -0
  97. package/src/keys/navigation.js +8 -0
  98. package/src/keys/operators.js +10 -0
  99. package/src/keys/statistics.js +38 -0
  100. package/src/keys/sub-sup.js +15 -0
  101. package/src/keys/trigonometry.js +15 -0
  102. package/src/keys/utils.js +66 -0
  103. package/src/keys/vars.js +19 -0
  104. package/src/latex-bridge.js +190 -0
  105. package/src/math-input.jsx +115 -0
  106. package/src/mathlive-instance.js +286 -0
  107. package/src/mf/common-styles.js +108 -0
  108. package/src/mf/index.js +26 -0
  109. package/src/mf/input.jsx +223 -0
  110. package/src/mf/static.jsx +358 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * LaTeX translation layer between the MathQuill-era content model and MathLive.
3
+ *
4
+ * Two independent concerns live here:
5
+ *
6
+ * 1. Newlines. MathQuill had no line break, so pie-lib registered a custom
7
+ * `newLine` embed that serialises to `\embed{newLine}[]`. MathLive has no
8
+ * embed system but supports real multiline environments, so we translate
9
+ * `\embed{newLine}[]` <-> `\\` inside `\displaylines{}`.
10
+ *
11
+ * Authored content keeps its `\embed{newLine}[]` on-disk form (that is what
12
+ * @pie-lib/math-rendering already normalises for MathJax), so each renderer
13
+ * normalises at its own boundary and stored items stay compatible.
14
+ *
15
+ * 2. Keypad key -> MathLive action. MathQuill keys carry `write`/`command`/
16
+ * `keystroke` built for `mf.write()` / `mf.cmd()` / `mf.keystroke()`.
17
+ * MathLive has `insert()` and `executeCommand()` instead.
18
+ */
19
+
20
+ export const NEWLINE_EMBED = '\\embed{newLine}[]';
21
+ const NEWLINE_EMBED_REGEX = /\\embed\{newLine\}\[\]/g;
22
+ const DISPLAYLINES_REGEX = /^\\displaylines\{([\s\S]*)\}$/;
23
+
24
+ /**
25
+ * MathQuill's editable sub-field syntax, e.g. `\MathQuillMathField[r1]{}`.
26
+ * These are the answer blocks in templated math items; MathLive expresses the
27
+ * same idea as `\placeholder[id]{}` and reads them back via the prompts API.
28
+ */
29
+ const MQ_FIELD_REGEX = /\\MathQuillMathField\[([^\]]*)\]\{([^{}]*)\}/g;
30
+ const PLACEHOLDER_REGEX = /\\placeholder\[([^\]]*)\]\{([^{}]*)\}/g;
31
+
32
+ /** Default id used when stored latex has an unnamed field. */
33
+ export const DEFAULT_FIELD_ID = 'r1';
34
+
35
+ /**
36
+ * Ids of the answer blocks in a piece of stored latex, in document order.
37
+ *
38
+ * @param {string} latex
39
+ * @returns {string[]}
40
+ */
41
+ export const fieldIds = (latex) => {
42
+ if (typeof latex !== 'string') {
43
+ return [];
44
+ }
45
+
46
+ const out = [];
47
+ let match;
48
+
49
+ MQ_FIELD_REGEX.lastIndex = 0;
50
+
51
+ while ((match = MQ_FIELD_REGEX.exec(latex)) !== null) {
52
+ out.push(match[1] || DEFAULT_FIELD_ID);
53
+ }
54
+
55
+ return out;
56
+ };
57
+
58
+ /**
59
+ * Convert stored latex (which may contain `\embed{newLine}[]`) into latex that
60
+ * MathLive can parse and render as multiple lines.
61
+ *
62
+ * @param {string} latex
63
+ * @returns {string}
64
+ */
65
+ export const toMathLive = (latex) => {
66
+ if (typeof latex !== 'string' || !latex) {
67
+ return latex || '';
68
+ }
69
+
70
+ // Answer blocks: \MathQuillMathField[id]{x} -> \placeholder[id]{x}
71
+ let out = latex.replace(MQ_FIELD_REGEX, (_m, id, content) => `\\placeholder[${id || DEFAULT_FIELD_ID}]{${content}}`);
72
+
73
+ // Newlines: \embed{newLine}[] -> a multiline environment
74
+ if (out.indexOf(NEWLINE_EMBED) !== -1) {
75
+ out = `\\displaylines{${out.split(NEWLINE_EMBED_REGEX).join(' \\\\ ')}}`;
76
+ }
77
+
78
+ return out;
79
+ };
80
+
81
+ /**
82
+ * Convert latex out of MathLive back into the stored form, so items authored
83
+ * with the MathLive editor remain readable by @pie-lib/math-rendering and by
84
+ * the existing MathQuill implementation.
85
+ *
86
+ * @param {string} latex
87
+ * @returns {string}
88
+ */
89
+ export const fromMathLive = (latex) => {
90
+ if (typeof latex !== 'string' || !latex) {
91
+ return latex || '';
92
+ }
93
+
94
+ const match = latex.trim().match(DISPLAYLINES_REGEX);
95
+ let out = match ? match[1] : latex;
96
+
97
+ // `\\` is the line separator inside a multiline environment.
98
+ if (out.indexOf('\\\\') !== -1) {
99
+ out = out
100
+ .split(/\s*\\\\\s*/)
101
+ .map((s) => s.trim())
102
+ .join(NEWLINE_EMBED);
103
+ }
104
+
105
+ // Answer blocks: \placeholder[id]{x} -> \MathQuillMathField[id]{x}
106
+ out = out.replace(
107
+ PLACEHOLDER_REGEX,
108
+ (_m, id, content) => `\\MathQuillMathField[${id || DEFAULT_FIELD_ID}]{${content}}`,
109
+ );
110
+
111
+ return out;
112
+ };
113
+
114
+ /**
115
+ * MathQuill `cmd()` semantics: commands that take arguments should land the
116
+ * cursor inside the first argument. MathLive expresses that with `#?`
117
+ * placeholders in the inserted latex.
118
+ */
119
+ const COMMANDS_WITH_ARGS = {
120
+ '\\frac': '\\frac{#?}{#?}',
121
+ '/': '\\frac{#?}{#?}',
122
+ '\\sqrt': '\\sqrt{#?}',
123
+ '\\nthroot': '\\sqrt[#?]{#?}',
124
+ '\\overline': '\\overline{#?}',
125
+ '\\overrightarrow': '\\overrightarrow{#?}',
126
+ '\\overleftrightarrow': '\\overleftrightarrow{#?}',
127
+ '\\overarc': '\\overarc{#?}',
128
+ '\\longdiv': '\\longdiv{#?}',
129
+ '^': '^{#?}',
130
+ _: '_{#?}',
131
+ '(': '\\left(#?\\right)',
132
+ '[': '\\left[#?\\right]',
133
+ '|': '\\left|#?\\right|',
134
+ };
135
+
136
+ /**
137
+ * MathQuill keystroke names -> MathLive command selectors.
138
+ */
139
+ const KEYSTROKES = {
140
+ Left: 'moveToPreviousChar',
141
+ Right: 'moveToNextChar',
142
+ Backspace: 'deleteBackward',
143
+ Delete: 'deleteForward',
144
+ Up: 'moveUp',
145
+ Down: 'moveDown',
146
+ };
147
+
148
+ /**
149
+ * Translate a keypad key definition into a MathLive action.
150
+ *
151
+ * Mirrors the precedence in @pie-lib/math-input's MathInput.keypadPress:
152
+ * latex (unless a command is present) -> write -> command -> keystroke.
153
+ *
154
+ * @param {object} key
155
+ * @returns {{type: 'insert'|'command', value: string}|undefined}
156
+ */
157
+ export const keyToAction = (key) => {
158
+ if (!key) {
159
+ return undefined;
160
+ }
161
+
162
+ if (key.latex && !key.command) {
163
+ return { type: 'insert', value: key.latex };
164
+ }
165
+
166
+ if (key.write) {
167
+ return { type: 'insert', value: key.write };
168
+ }
169
+
170
+ if (key.command) {
171
+ // Some keys carry an array of commands (e.g. Measured Angle is
172
+ // ['m', '\\angle'], log-base-n is ['\\log', '_']). MathQuill's `cmd()`
173
+ // applied them in sequence; MathLive inserts one latex string, so expand
174
+ // each part and concatenate.
175
+ const parts = Array.isArray(key.command) ? key.command : [key.command];
176
+ const value = parts.map((c) => COMMANDS_WITH_ARGS[c] || c).join('');
177
+
178
+ return { type: 'insert', value };
179
+ }
180
+
181
+ if (key.keystroke) {
182
+ const selector = KEYSTROKES[key.keystroke];
183
+
184
+ return selector ? { type: 'command', value: selector } : undefined;
185
+ }
186
+
187
+ return undefined;
188
+ };
189
+
190
+ export { COMMANDS_WITH_ARGS, KEYSTROKES };
@@ -0,0 +1,115 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { styled } from '@mui/material/styles';
4
+ import debug from 'debug';
5
+
6
+ import * as mf from './mf';
7
+ import { baseSet } from './keys';
8
+ import KeyPad from './keypad';
9
+ import { keyToAction } from './latex-bridge';
10
+
11
+ const log = debug('pie-lib:math-input-mathlive');
12
+
13
+ const grey = 'rgba(0, 0, 0, 0.23)';
14
+
15
+ const MathInputContainer = styled('div')(({ theme, focused }) => ({
16
+ borderRadius: '4px',
17
+ border: `solid 1px ${focused ? theme.palette.primary.main : grey}`,
18
+ marginTop: theme.spacing(1),
19
+ marginBottom: theme.spacing(1),
20
+ transition: 'border 200ms linear',
21
+ }));
22
+
23
+ const PadContainer = styled('div')({
24
+ width: '100%',
25
+ display: 'flex',
26
+ });
27
+
28
+ const StyledInput = styled(mf.Input)(({ theme }) => ({
29
+ width: '100%',
30
+ transition: 'border 200ms linear',
31
+ padding: theme.spacing(1),
32
+ '& math-field:focus-within': {
33
+ outline: 'none',
34
+ boxShadow: 'none',
35
+ },
36
+ }));
37
+
38
+ /**
39
+ * MathLive-backed equivalent of @pie-lib/math-input's `MathInput`.
40
+ * Same props (`keyset`, `latex`, `onChange`, `displayMode`).
41
+ */
42
+ export class MathInput extends React.Component {
43
+ static propTypes = {
44
+ className: PropTypes.string,
45
+ keyset: PropTypes.array,
46
+ displayMode: PropTypes.oneOf(['block', 'block-on-focus']),
47
+ latex: PropTypes.string,
48
+ onChange: PropTypes.func,
49
+ };
50
+
51
+ static defaultProps = {
52
+ keyset: [],
53
+ displayMode: 'block',
54
+ };
55
+
56
+ constructor(props) {
57
+ super(props);
58
+ this.state = { focused: false };
59
+ }
60
+
61
+ keypadPress = (key) => {
62
+ log('[keypadPress] key:', key);
63
+
64
+ if (!this.input) {
65
+ return;
66
+ }
67
+
68
+ const action = keyToAction(key);
69
+
70
+ if (!action) {
71
+ return;
72
+ }
73
+
74
+ if (action.type === 'command') {
75
+ this.input.keystroke(action.value);
76
+ } else {
77
+ this.input.write(action.value);
78
+ }
79
+ };
80
+
81
+ inputFocus = () => this.setState({ focused: true });
82
+
83
+ inputBlur = () => this.setState({ focused: false });
84
+
85
+ changeLatex = (l) => {
86
+ const { onChange } = this.props;
87
+
88
+ if (onChange && l !== this.props.latex) {
89
+ log('[changeLatex]', l, this.props.latex);
90
+ onChange(l);
91
+ }
92
+ };
93
+
94
+ render() {
95
+ const { className, keyset, latex } = this.props;
96
+ const { focused } = this.state;
97
+
98
+ return (
99
+ <MathInputContainer className={className} focused={focused}>
100
+ <StyledInput
101
+ innerRef={(r) => (this.input = r)}
102
+ onFocus={this.inputFocus}
103
+ onBlur={this.inputBlur}
104
+ latex={latex}
105
+ onChange={this.changeLatex}
106
+ />
107
+ <PadContainer>
108
+ <KeyPad baseSet={baseSet} additionalKeys={keyset} onPress={this.keypadPress} />
109
+ </PadContainer>
110
+ </MathInputContainer>
111
+ );
112
+ }
113
+ }
114
+
115
+ export default MathInput;
@@ -0,0 +1,286 @@
1
+ import debug from 'debug';
2
+ import { NEWLINE_EMBED, toMathLive } from './latex-bridge';
3
+
4
+ const log = debug('pie-lib:math-input-mathlive:instance');
5
+
6
+ /**
7
+ * MathLive bootstrap + compatibility shims for @pie-lib/math-input's public API.
8
+ *
9
+ * MathLive is loaded lazily and only in the browser: the editor bundle is
10
+ * ~227KB gzip and registers custom elements, so it must never run during SSR.
11
+ */
12
+
13
+ let mathlive;
14
+
15
+ /**
16
+ * Where the KaTeX woff2 fonts are served from, as configured by the host app.
17
+ * `undefined` means "not configured"; `null` means "explicitly disabled".
18
+ */
19
+ let fontsDirectory;
20
+
21
+ /**
22
+ * Turn a fonts path into a fully-qualified URL.
23
+ *
24
+ * MathLive's default `fontsDirectory` is the relative `'./fonts/'`, which it
25
+ * resolves against its own script URL. Under a bundler (webpack/Next) that
26
+ * script URL is empty, so `new URL('./fonts/', '')` throws
27
+ * "Failed to construct 'URL': Invalid base URL" from `loadFonts`.
28
+ * Resolving against `document.baseURI` avoids that entirely and honours any
29
+ * <base href> / asset prefix the host app uses.
30
+ */
31
+ const absoluteFontsDirectory = (dir) => {
32
+ if (!dir) {
33
+ return dir;
34
+ }
35
+
36
+ // Already absolute (http://, https://, //cdn...) - MathLive handles these.
37
+ if (/^(?:[a-z+]+:)?\/\//i.test(dir)) {
38
+ return dir;
39
+ }
40
+
41
+ try {
42
+ return new URL(dir, document.baseURI).href;
43
+ } catch (e) {
44
+ log('could not resolve fontsDirectory "%s": %s', dir, e && e.message);
45
+ return null;
46
+ }
47
+ };
48
+
49
+ /**
50
+ * Tell the package where the MathLive fonts are served from.
51
+ *
52
+ * Call before `loadMathLive()`. Accepts an absolute URL, a root-relative path
53
+ * (`/mathlive-fonts`) or a relative one; pass `null` to skip font loading and
54
+ * fall back to system fonts.
55
+ *
56
+ * The fonts ship inside the `mathlive` package (`node_modules/mathlive/fonts`)
57
+ * and must be copied into whatever the host app serves statically.
58
+ *
59
+ * @param {string|null} dir
60
+ */
61
+ export const configureFonts = (dir) => {
62
+ fontsDirectory = dir;
63
+
64
+ if (mathlive) {
65
+ mathlive.MathfieldElement.fontsDirectory = absoluteFontsDirectory(dir);
66
+ }
67
+ };
68
+
69
+ /**
70
+ * Custom macros for the pie-specific commands that the MathQuill fork
71
+ * understood but stock LaTeX does not.
72
+ *
73
+ * Kept deliberately in sync with the MathJax macros in
74
+ * @pie-lib/math-rendering (`render-math.js`) so the same latex renders the same
75
+ * way in the editor and in the static renderer.
76
+ */
77
+ export const PIE_MACROS = {
78
+ parallelogram: '\\htmlData{pie-macro=parallelogram}{\\unicode{"25B1}}',
79
+ overarc: '\\overparen{#1}',
80
+ napprox: '\\not\\approx',
81
+ nsim: '\\not\\sim',
82
+ ncong: '\\not\\cong',
83
+ nparallel: '\\not\\parallel',
84
+ perpendicular: '\\perp',
85
+ divide: '\\div',
86
+ degree: '^{\\circ}',
87
+ square: '\\unicode{"25A1}',
88
+ longdiv: '\\htmlData{pie-macro=longdiv}{#1}',
89
+ abs: '\\left|#1\\right|',
90
+ };
91
+
92
+ /**
93
+ * Registry for `registerEmbed`.
94
+ *
95
+ * MathLive has no embed plugin system, so an embed is expressed as a macro that
96
+ * expands to real LaTeX. `\htmlData{embed=<name>}` tags the rendered output so
97
+ * consumers can target it with CSS exactly as they targeted the old
98
+ * `htmlString` markup.
99
+ *
100
+ * Known embeds map to native constructs:
101
+ * - `newLine` -> structural, handled by latex-bridge (`\displaylines` + `\\`)
102
+ * - `answerBlock`-> `\placeholder{}` (interactive, read via the prompts API)
103
+ *
104
+ * @type {Map<string, {factory: Function, latex: string}>}
105
+ */
106
+ const embeds = new Map();
107
+
108
+ const KNOWN_EMBED_LATEX = {
109
+ // newLine is structural: it is translated by latex-bridge before it ever
110
+ // reaches MathLive, so the macro only needs to be harmless.
111
+ newLine: '',
112
+ answerBlock: '\\placeholder{}',
113
+ };
114
+
115
+ /**
116
+ * Load MathLive. Resolves to undefined during SSR.
117
+ * @returns {Promise<object|undefined>}
118
+ */
119
+ export const loadMathLive = async () => {
120
+ if (typeof window === 'undefined') {
121
+ return undefined;
122
+ }
123
+
124
+ if (!mathlive) {
125
+ try {
126
+ mathlive = await import('mathlive');
127
+ } catch (e) {
128
+ // MathLive is browser-only and ships an ESM-conditional exports map, so
129
+ // the import can fail in test/SSR-ish runtimes. Fail soft: callers all
130
+ // handle a missing instance (static rendering falls back to raw latex).
131
+ log('MathLive could not be loaded: %s', e && e.message);
132
+ return undefined;
133
+ }
134
+
135
+ // Always replace MathLive's relative `'./fonts/'` default: it throws
136
+ // "Invalid base URL" when bundled. Either point at the host app's copy or
137
+ // disable font loading, both of which are safe.
138
+ if (fontsDirectory === undefined) {
139
+ log(
140
+ 'no fontsDirectory configured - MathLive will use fallback fonts. ' +
141
+ 'Call configureFonts("/path/to/fonts") with a copy of node_modules/mathlive/fonts to enable them.',
142
+ );
143
+ }
144
+
145
+ mathlive.MathfieldElement.fontsDirectory =
146
+ fontsDirectory === undefined ? null : absoluteFontsDirectory(fontsDirectory);
147
+ }
148
+
149
+ return mathlive;
150
+ };
151
+
152
+ /**
153
+ * Synchronous accessor for an already-loaded MathLive.
154
+ * @returns {object|undefined}
155
+ */
156
+ export const getMathLive = () => mathlive;
157
+
158
+ /**
159
+ * The macro dictionary to hand to a mathfield: pie macros plus anything
160
+ * registered through `registerEmbed`.
161
+ *
162
+ * @returns {object}
163
+ */
164
+ export const getMacros = () => {
165
+ const out = { ...PIE_MACROS };
166
+
167
+ embeds.forEach(({ latex }, name) => {
168
+ out[name] = `\\htmlData{embed=${name}}{${latex}}`;
169
+ });
170
+
171
+ return out;
172
+ };
173
+
174
+ /**
175
+ * Register an embed. API-compatible with @pie-lib/math-input's `registerEmbed`
176
+ * so existing callers do not change.
177
+ *
178
+ * MathQuill's factory returns `{ htmlString, text, latex }`. Only the
179
+ * declarative parts survive: `latex` round-trips through a macro and the
180
+ * styling hook becomes a `data-embed` attribute. Arbitrary DOM injection via
181
+ * `htmlString` has no MathLive equivalent and is ignored.
182
+ *
183
+ * @param {string} name
184
+ * @param {Function} factory
185
+ */
186
+ export function registerEmbed(name, factory) {
187
+ if (!name || typeof factory !== 'function') {
188
+ return;
189
+ }
190
+
191
+ let definition = {};
192
+
193
+ try {
194
+ definition = factory() || {};
195
+ } catch (e) {
196
+ log('embed factory threw for "%s": %s', name, e.message);
197
+ }
198
+
199
+ const latex = Object.prototype.hasOwnProperty.call(KNOWN_EMBED_LATEX, name)
200
+ ? KNOWN_EMBED_LATEX[name]
201
+ : definition.mathliveLatex || '\\placeholder{}';
202
+
203
+ if (!Object.prototype.hasOwnProperty.call(KNOWN_EMBED_LATEX, name) && !definition.mathliveLatex) {
204
+ // Unknown third-party embed: degrade instead of throwing. `htmlString`
205
+ // cannot be reproduced, so it renders as an editable slot.
206
+ log(
207
+ 'embed "%s" is not natively supported by MathLive; rendering as a placeholder. ' +
208
+ 'Provide `mathliveLatex` in the factory result to control this.',
209
+ name,
210
+ );
211
+ }
212
+
213
+ embeds.set(name, { factory, latex });
214
+ }
215
+
216
+ /**
217
+ * Embeds registered so far. Exposed for tests/diagnostics.
218
+ * @returns {string[]}
219
+ */
220
+ export const registeredEmbeds = () => Array.from(embeds.keys());
221
+
222
+ /** Reset the embed registry. Test helper. */
223
+ export const __resetEmbeds = () => embeds.clear();
224
+
225
+ /** Inject a stub MathLive instance. Test helper. */
226
+ export const __setMathLiveForTest = (stub) => {
227
+ mathlive = stub;
228
+ };
229
+
230
+ /**
231
+ * Render static math into an element. API-compatible with
232
+ * @pie-lib/math-input's `applyStaticMath`.
233
+ *
234
+ * @param {HTMLElement} element
235
+ * @param {string} [latex] assigned to the element before rendering
236
+ * @param {object} [options] `{ macros }` overrides
237
+ * @returns {HTMLElement|undefined} the element, for parity with the old return
238
+ */
239
+ export function applyStaticMath(element, latex, options = {}) {
240
+ if (!element || typeof window === 'undefined') {
241
+ return undefined;
242
+ }
243
+
244
+ const ml = getMathLive();
245
+
246
+ if (!ml) {
247
+ log('applyStaticMath called before MathLive loaded - call loadMathLive() first');
248
+ return undefined;
249
+ }
250
+
251
+ const source = latex === undefined || latex === null ? element.textContent : latex;
252
+
253
+ element.innerHTML = ml.convertLatexToMarkup(toMathLive(source), {
254
+ macros: { ...getMacros(), ...(options.macros || {}) },
255
+ });
256
+
257
+ return element;
258
+ }
259
+
260
+ /**
261
+ * Convert latex to markup using the pie macro set. Used for keypad labels and
262
+ * any read-only rendering that does not need a live mathfield.
263
+ *
264
+ * @param {string} latex
265
+ * @param {object} [options]
266
+ * @returns {string} HTML markup, or '' if MathLive is not loaded
267
+ */
268
+ export function latexToMarkup(latex, options = {}) {
269
+ const ml = getMathLive();
270
+
271
+ if (!ml || !latex) {
272
+ return '';
273
+ }
274
+
275
+ try {
276
+ return ml.convertLatexToMarkup(toMathLive(latex), {
277
+ macros: { ...getMacros(), ...(options.macros || {}) },
278
+ ...options,
279
+ });
280
+ } catch (e) {
281
+ log('convertLatexToMarkup failed for "%s": %s', latex, e.message);
282
+ return '';
283
+ }
284
+ }
285
+
286
+ export { NEWLINE_EMBED };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared MathLive styling.
3
+ *
4
+ * Replaces @pie-lib/math-input's `common-mq-styles.js`. That file was built
5
+ * around MathQuill's `.mq-*` class names, none of which exist in MathLive
6
+ * output, so the selectors here target MathLive's own structure instead.
7
+ *
8
+ * The four named groups are kept because consumers destructure them:
9
+ *
10
+ * const { commonMqFontStyles, commonMqKeyboardStyles, longdivStyles, supsubStyles }
11
+ * = mq.CommonMqStyles; // math-toolbar/src/editor-and-pad.jsx, math-preview.jsx
12
+ *
13
+ * Most are intentionally near-empty: MathLive sizes scripts, radicals and
14
+ * fences correctly on its own, so the MathQuill-era corrections are not needed.
15
+ * They stay as documented extension points rather than being deleted, so the
16
+ * destructuring above keeps working.
17
+ */
18
+
19
+ /**
20
+ * Font styling. MathQuill needed explicit MathJax font families to match the
21
+ * static renderer; MathLive ships the KaTeX fonts it renders with, so this only
22
+ * smooths rendering and lets the host override the family.
23
+ */
24
+ export const commonMqFontStyles = {
25
+ '-webkit-font-smoothing': 'antialiased',
26
+ '& math-field': {
27
+ fontFamily: 'inherit',
28
+ },
29
+ };
30
+
31
+ /**
32
+ * Long division. The MathQuill fork drew this with nested `.mq-longdiv-*`
33
+ * elements that needed manual offsets; here it comes from the `\longdiv` macro
34
+ * (see `PIE_MACROS`), tagged with `data-pie-macro="longdiv"`.
35
+ */
36
+ export const longdivStyles = {
37
+ '& [data-pie-macro="longdiv"]': {
38
+ display: 'inline-flex',
39
+ },
40
+ };
41
+
42
+ /**
43
+ * Superscript/subscript sizing. MathLive follows TeX metrics, so no overrides
44
+ * are required.
45
+ */
46
+ export const supsubStyles = {};
47
+
48
+ /** Empty placeholder boxes (answer blocks / open slots). */
49
+ export const placeholderStyles = {
50
+ '& .ML__placeholder': {
51
+ borderBottom: '1px solid currentColor',
52
+ minWidth: '1em',
53
+ },
54
+ };
55
+
56
+ /**
57
+ * Styling for the on-screen keyboard and any container rendering math labels.
58
+ * Also hides MathLive's own affordances, since pie supplies its own keypad.
59
+ */
60
+ export const commonMqKeyboardStyles = {
61
+ ...commonMqFontStyles,
62
+ ...longdivStyles,
63
+ ...placeholderStyles,
64
+ touchAction: 'manipulation',
65
+ '& math-field': {
66
+ fontSize: 'inherit',
67
+ outline: 'none',
68
+ },
69
+ '& math-field::part(virtual-keyboard-toggle)': {
70
+ display: 'none',
71
+ },
72
+ '& math-field::part(menu-toggle)': {
73
+ display: 'none',
74
+ },
75
+ };
76
+
77
+ /** Everything combined - the common case for a container of math. */
78
+ export const commonMathLiveStyles = {
79
+ ...commonMqFontStyles,
80
+ ...longdivStyles,
81
+ ...supsubStyles,
82
+ ...placeholderStyles,
83
+ '& math-field': {
84
+ fontSize: 'inherit',
85
+ outline: 'none',
86
+ },
87
+ '& math-field::part(virtual-keyboard-toggle)': {
88
+ display: 'none',
89
+ },
90
+ '& math-field::part(menu-toggle)': {
91
+ display: 'none',
92
+ },
93
+ };
94
+
95
+ export const commonKeyboardStyles = commonMqKeyboardStyles;
96
+
97
+ /**
98
+ * Default export mirrors @pie-lib/math-input's `CommonMqStyles` object so
99
+ * existing destructuring keeps working.
100
+ */
101
+ export default {
102
+ commonMqFontStyles,
103
+ longdivStyles,
104
+ supsubStyles,
105
+ commonMqKeyboardStyles,
106
+ placeholderStyles,
107
+ commonMathLiveStyles,
108
+ };