@barocss/math-editor 0.1.0 → 0.2.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 (99) hide show
  1. package/ADAPTERS.md +31 -2
  2. package/API-JAVASCRIPT.md +157 -0
  3. package/API-REACT.md +202 -0
  4. package/API-SESSION.md +163 -0
  5. package/API-SOLID.md +90 -0
  6. package/API-SVELTE.md +83 -0
  7. package/API-VUE.md +104 -0
  8. package/API-WEB-COMPONENT.md +128 -0
  9. package/CHANGELOG.md +16 -0
  10. package/EMBEDDING.md +7 -0
  11. package/IMPLEMENTATION.md +132 -1
  12. package/JSON-MODEL.md +441 -0
  13. package/LATEX-GUIDE.md +248 -0
  14. package/LATEX-MODEL.md +290 -0
  15. package/LATEX-SCOPE.md +260 -4
  16. package/LOCALIZATION.md +7 -1
  17. package/README.md +91 -4
  18. package/RELEASING.md +1 -1
  19. package/ROADMAP.md +107 -16
  20. package/SUPPORT.md +128 -13
  21. package/SYMBOLS.md +33 -1
  22. package/VALIDATION.md +171 -0
  23. package/dist/core.d.ts +2 -0
  24. package/dist/core.d.ts.map +1 -1
  25. package/dist/core.js +2 -0
  26. package/dist/core.js.map +1 -1
  27. package/dist/document-codec.d.ts +4 -0
  28. package/dist/document-codec.d.ts.map +1 -0
  29. package/dist/document-codec.js +37 -0
  30. package/dist/document-codec.js.map +1 -0
  31. package/dist/dom/menu-position.d.ts +3 -0
  32. package/dist/dom/menu-position.d.ts.map +1 -0
  33. package/dist/dom/menu-position.js +44 -0
  34. package/dist/dom/menu-position.js.map +1 -0
  35. package/dist/dom/toolbar.d.ts +2 -0
  36. package/dist/dom/toolbar.d.ts.map +1 -1
  37. package/dist/dom/toolbar.js +16 -1
  38. package/dist/dom/toolbar.js.map +1 -1
  39. package/dist/dom.d.ts +1 -0
  40. package/dist/dom.d.ts.map +1 -1
  41. package/dist/dom.js +134 -24
  42. package/dist/dom.js.map +1 -1
  43. package/dist/editor-labels.d.ts.map +1 -1
  44. package/dist/editor-labels.js +42 -0
  45. package/dist/editor-labels.js.map +1 -1
  46. package/dist/fences.d.ts +11 -0
  47. package/dist/fences.d.ts.map +1 -0
  48. package/dist/fences.js +21 -0
  49. package/dist/fences.js.map +1 -0
  50. package/dist/index.d.ts +3 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +2 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/latex.d.ts +26 -0
  55. package/dist/latex.d.ts.map +1 -0
  56. package/dist/latex.js +689 -0
  57. package/dist/latex.js.map +1 -0
  58. package/dist/locales/en.js +108 -1
  59. package/dist/locales/en.json +108 -1
  60. package/dist/locales/ko.js +120 -2
  61. package/dist/locales/ko.json +120 -2
  62. package/dist/math-editor-toolbar.d.ts +5 -2
  63. package/dist/math-editor-toolbar.d.ts.map +1 -1
  64. package/dist/math-editor-toolbar.js +10 -6
  65. package/dist/math-editor-toolbar.js.map +1 -1
  66. package/dist/math-editor.d.ts +24 -4
  67. package/dist/math-editor.d.ts.map +1 -1
  68. package/dist/math-editor.js +218 -39
  69. package/dist/math-editor.js.map +1 -1
  70. package/dist/model.d.ts +10 -2
  71. package/dist/model.d.ts.map +1 -1
  72. package/dist/model.js +262 -9
  73. package/dist/model.js.map +1 -1
  74. package/dist/range.d.ts +1 -1
  75. package/dist/range.d.ts.map +1 -1
  76. package/dist/range.js +116 -6
  77. package/dist/range.js.map +1 -1
  78. package/dist/react.d.ts +1 -1
  79. package/dist/react.d.ts.map +1 -1
  80. package/dist/session.d.ts +3 -0
  81. package/dist/session.d.ts.map +1 -1
  82. package/dist/session.js +9 -1
  83. package/dist/session.js.map +1 -1
  84. package/dist/suggestions.d.ts +4 -0
  85. package/dist/suggestions.d.ts.map +1 -1
  86. package/dist/suggestions.js +158 -21
  87. package/dist/suggestions.js.map +1 -1
  88. package/dist/symbols.d.ts +1 -1
  89. package/dist/symbols.d.ts.map +1 -1
  90. package/dist/symbols.js +56 -5
  91. package/dist/symbols.js.map +1 -1
  92. package/package.json +3 -2
  93. package/src/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  94. package/src/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  95. package/src/fonts/KaTeX_Main-Bold.woff2 +0 -0
  96. package/src/fonts/KaTeX_Main-Regular.woff2 +0 -0
  97. package/src/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  98. package/src/fonts/LICENSE-KaTeX.txt +21 -0
  99. package/src/style.css +868 -45
package/ADAPTERS.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Framework adapters and embedding
2
2
 
3
+ ## Framework-specific API guides
4
+
5
+ [Pure JavaScript](API-JAVASCRIPT.md) · [React](API-REACT.md) · [Web Component](API-WEB-COMPONENT.md) · [Vue 3](API-VUE.md) · [Svelte](API-SVELTE.md) · [Solid](API-SOLID.md) · [Session and DOM API](API-SESSION.md)
6
+
7
+ Each guide covers installation, a complete integration, document replacement, saving, lifecycle cleanup and host limitations.
8
+
9
+
3
10
  ## Distribution architecture
4
11
 
5
12
  One ESM npm package exposes independent entry points. This keeps document types, commands and clipboard formats on one version. Six separate npm packages are unnecessary until independently versioned adapters are useful.
@@ -147,7 +154,7 @@ The directive reads its accessor in a reactive effect and cleans up with `onClea
147
154
  | Composition candidate preview | Disabled choices while composing | Menu hidden while composing |
148
155
  | Keyboard model ranges / OS IME matrix / full accessibility audit | Pending | Pending |
149
156
 
150
- The native renderer is an integration preview, not a replacement of the rich React editor. Completing parity before switching the main entry is the next release gate. Trusted document loading still needs schema/ID validation. LaTeX is export-only; plain-text paste is not a LaTeX parser.
157
+ The native renderer is an integration preview, not a replacement of the rich React editor. Completing parity before switching the main entry is the next release gate. Use `parseMathDocument` to validate schema and IDs before trusted document loading. Bounded LaTeX import is available through `importLatex`; ordinary plain-text paste remains literal.
151
158
 
152
159
  ## Local verification and packaging
153
160
 
@@ -160,7 +167,7 @@ pnpm --filter @barocss/math-demo test:e2e
160
167
  pnpm --filter @barocss/math-demo build
161
168
  # Inspect a tarball locally; this does not publish:
162
169
  pnpm --dir packages/math-editor pack --pack-destination /tmp/math-editor-package
163
- node packages/math-editor/scripts/check-package.mjs /tmp/math-editor-package/barocss-math-editor-0.1.0.tgz
170
+ node packages/math-editor/scripts/check-package.mjs /tmp/math-editor-package/barocss-math-editor-0.2.0.tgz
164
171
  ```
165
172
 
166
173
  Release work still includes package ownership/versioning, an explicit release decision, supported-browser and framework-version CI, bundle budgets and renderer parity. No registry publication has been performed.
@@ -168,3 +175,25 @@ Release work still includes package ownership/versioning, an explicit release de
168
175
  ## Independent outputs and host completion
169
176
 
170
177
  The DOM entry also exports `mountMathLatex` and `mountMathPreview`. See [Embedding](EMBEDDING.md) for the four-surface composition contract, native `enterBehavior`/`onCommit`/`onCancel`, popup draft handling and Note-style next-block creation. The rich React editor adds `toolbar`, `toolbarEnd` and `showTokenLegend`; its dedicated single-line/host-completion API remains the native `MathEditorSurface`.
178
+
179
+ ## Compact and filtered toolbars (workspace)
180
+
181
+ Toolbars initially show up to eight structure buttons. More tools / Fewer tools toggles the remainder without changing the formula or history. Rich React also places matrix presets, templates and symbol shortcuts in the expanded section; Undo, Redo and `toolbarEnd` stay visible. Native toolbars contain structure buttons and history only and show a toggle when needed. The layout wraps naturally on narrow screens; this is not a guaranteed single-row toolbar.
182
+
183
+ ```tsx
184
+ <MathEditor toolbar={['fraction', 'root', 'superscript', 'matrix']}
185
+ toolbarMaxItems={3} />
186
+ ```
187
+
188
+ ```js
189
+ mountMathEditor(host, session, {
190
+ toolbar: ['fraction', 'root', 'norm'],
191
+ toolbarMaxItems: 2,
192
+ });
193
+ mountMathToolbar(toolbarHost, session, {
194
+ kinds: ['fraction', 'root', 'norm'],
195
+ maxItems: 2,
196
+ });
197
+ ```
198
+
199
+ `toolbar: false` hides the toolbar. A structure array filters its structure buttons only: it does not disable those structures in suggestions, parsing or the model, and does not filter React's auxiliary template/symbol controls. `toolbarMaxItems` (independent toolbar: `maxItems`) is a nonnegative count; zero initially hides all structure buttons behind More. A sufficiently large count shows all selected structure buttons initially. Expanded state belongs to the mounted toolbar and is not saved in the math document. The rich React `toolbar` array and compact behavior are available in 0.2.0.
@@ -0,0 +1,157 @@
1
+ # Pure JavaScript API guide
2
+
3
+ Use this integration for vanilla JavaScript, custom application shells and imperative editor hosts. Neither `/core` nor `/dom` imports React. Examples assume an ESM bundler such as Vite; the package does not provide a global script/IIFE build.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @barocss/math-editor@0.2.0
9
+ ```
10
+
11
+ ## Mount a complete editor
12
+
13
+ Create `<div id="formula"></div><pre id="latex"></pre>` in your page, then run this module after those elements exist:
14
+
15
+ ```js
16
+ import { createMathSession } from '@barocss/math-editor/core';
17
+ import { mountMathEditor } from '@barocss/math-editor/dom';
18
+ import '@barocss/math-editor/style.css';
19
+
20
+ const host = document.querySelector('#formula');
21
+ const output = document.querySelector('#latex');
22
+ if (!(host instanceof HTMLElement) || !output) throw new Error('Missing editor host');
23
+
24
+ const session = createMathSession({ locale: 'en', mode: 'block' });
25
+ const editor = mountMathEditor(host, {
26
+ session,
27
+ toolbar: true,
28
+ onChange(document, latex) {
29
+ localStorage.setItem('formula', JSON.stringify(document));
30
+ output.textContent = latex;
31
+ },
32
+ });
33
+ editor.focus();
34
+
35
+ // Call when your route/component is removed.
36
+ function dispose() {
37
+ editor.destroy();
38
+ session.destroy();
39
+ }
40
+ ```
41
+
42
+ Creating a session is optional: without `session`, the renderer creates and owns one, accessible as `editor.session`. In that case `editor.destroy()` also releases the owned session.
43
+
44
+ ## Open, update and execute
45
+
46
+ ```js
47
+ // savedDocument must have been validated by your host.
48
+ session.load(savedDocument); // resets undo history
49
+ editor.update({ locale: 'ko', toolbar: false });
50
+ session.execute({ type: 'text', value: 'α' });
51
+ session.execute({ type: 'structure', kind: 'fraction' });
52
+ session.execute({ type: 'undo' });
53
+ const snapshot = session.getSnapshot();
54
+ console.log(snapshot.latex, snapshot.canUndo);
55
+ ```
56
+
57
+ These snippets continue the mounted example. `defaultValue`, `session` and `menuHost` are read at mount; use `load()` rather than changing `defaultValue`. `update()` merges options: pass `false` explicitly to hide a toolbar.
58
+
59
+ ## External toolbar
60
+
61
+ Create a separate `#tools` element. It can sit beside the document or in a floating panel:
62
+
63
+ ```js
64
+ import { mountMathToolbar } from '@barocss/math-editor/dom';
65
+ const toolsHost = document.querySelector('#tools');
66
+ if (!(toolsHost instanceof HTMLElement)) throw new Error('Missing toolbar host');
67
+ const tools = mountMathToolbar(toolsHost, session, {
68
+ kinds: ['fraction', 'root', 'superscript'],
69
+ onExecute: () => editor.focus(),
70
+ });
71
+ // Include this in dispose(), before destroying editor/session:
72
+ // tools.destroy();
73
+ ```
74
+
75
+ A toolbar kind list controls the visible buttons, not which structures may exist in a document. Validate the final model if your product supports only a subset.
76
+
77
+ ## Inline and popup hosting
78
+
79
+ Set `mode: 'inline'` for a single top-level row and `toolbar: false` for an editor-only surface. Inline equations may still be tall, for example fractions or matrices. Inline Enter first accepts a suggestion; otherwise it invokes `onCommit(snapshot)` or `onExit(1)`. `onExit(-1 | 1)` asks your host to restore its caret before/after the equation.
80
+
81
+ For a popup, create a draft session from a copy of the original document. Commit `session.getSnapshot().state.document` only after Apply. Cancel destroys the draft without modifying the original. Set `menuHost` to your modal element if your modal's focus trap needs suggestion menus inside it. See the complete [popup composition](EMBEDDING.md).
82
+
83
+ ## Common mistakes
84
+
85
+ - Importing the package root in a React-free application: use `/core` and `/dom` instead.
86
+ - Mounting before an element exists or re-mounting on each keystroke.
87
+ - Saving every `subscribe` callback: caret and locale changes also emit; check `documentChanged`.
88
+ - Treating LaTeX output as a source document or a computation API.
89
+
90
+ ## Complete modal: Apply, Cancel and focus restoration
91
+
92
+ This function creates its own native dialog and draft. Call it from a user action with a trusted document. It resolves to a new document on Apply, or `undefined` on Cancel. No host document changes while typing. Block mode permits multiline formulas; use inline mode only when the source is single-row.
93
+
94
+ ```js
95
+ import { createMathSession } from '@barocss/math-editor/core';
96
+ import { mountMathEditor } from '@barocss/math-editor/dom';
97
+ import '@barocss/math-editor/style.css';
98
+
99
+ export function editFormula(original) {
100
+ const previousFocus = document.activeElement;
101
+ const dialog = document.createElement('dialog');
102
+ const title = document.createElement('h2');
103
+ title.textContent = 'Edit formula';
104
+ const titleId = `formula-${crypto.randomUUID()}`;
105
+ title.id = titleId;
106
+ dialog.setAttribute('aria-labelledby', titleId);
107
+ const host = document.createElement('div');
108
+ const apply = document.createElement('button');
109
+ const cancel = document.createElement('button');
110
+ apply.textContent = 'Apply';
111
+ cancel.textContent = 'Cancel';
112
+ apply.type = cancel.type = 'button';
113
+ dialog.append(title, host, cancel, apply);
114
+ document.body.append(dialog);
115
+ const session = createMathSession({ document: original, locale: 'en' });
116
+
117
+ return new Promise(resolve => {
118
+ let finished = false;
119
+ const finish = accepted => {
120
+ if (finished) return;
121
+ finished = true;
122
+ const result = accepted ? session.getSnapshot().state.document : undefined;
123
+ editor.destroy();
124
+ session.destroy();
125
+ dialog.close();
126
+ dialog.remove();
127
+ if (previousFocus instanceof HTMLElement && previousFocus.isConnected)
128
+ previousFocus.focus();
129
+ resolve(result);
130
+ };
131
+ const editor = mountMathEditor(host, {
132
+ session,
133
+ menuHost: dialog,
134
+ onCancel: () => finish(false),
135
+ });
136
+ apply.addEventListener('click', () => finish(true));
137
+ cancel.addEventListener('click', () => finish(false));
138
+ dialog.addEventListener('cancel', event => {
139
+ event.preventDefault();
140
+ finish(false);
141
+ });
142
+ dialog.addEventListener('close', () => finish(false));
143
+ dialog.showModal();
144
+ editor.focus();
145
+ });
146
+ }
147
+ ```
148
+
149
+ Use `const next = await editFormula(currentDocument); if (next) commitToHost(next);` in your host's click handler. `currentDocument` and `commitToHost` belong to your application. Commit as one host undo transaction. For route teardown while a modal is open, close the dialog before removing its DOM so the `close` handler releases the draft.
150
+
151
+ ## Persistence and ownership
152
+
153
+ Persist the `MathDocument` passed to `onChange`; LaTeX is derived output and is not an editable round-trip format. The package does not parse arbitrary LaTeX. Validate externally supplied JSON before loading it: loading assumes a trusted, structurally valid document with unique IDs. There is no server save or collaboration transport built in.
154
+
155
+ Keep the mount host empty. The editor owns its descendants; framework rendering into the same host can destroy caret and composition state. Use one editing surface per session. Destroy the renderer on teardown; a session supplied by the host remains the host's responsibility.
156
+
157
+ See [session and DOM API](API-SESSION.md) for exact options, commands, events and cleanup, and [renderer differences](ADAPTERS.md#current-renderer-parity) before choosing a native wrapper over the rich React editor.
package/API-REACT.md ADDED
@@ -0,0 +1,202 @@
1
+ # React API guide
2
+
3
+ React offers two components. Choose intentionally: they share document structure, but do not share a live session or renderer.
4
+
5
+ | Component | Use it for | State ownership |
6
+ |---|---|---|
7
+ | `MathEditor` | Rich symbol browser, token editing and selection tools | Internal React history; `defaultValue` is mount-only |
8
+ | `MathEditorSurface` | Native inline mode, external session/tools, host commit/cancel | DOM renderer with optional host-owned `MathSession` |
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @barocss/math-editor@0.2.0 react react-dom
14
+ ```
15
+
16
+ ## Rich editor
17
+
18
+ ```tsx
19
+ import { MathEditor } from '@barocss/math-editor/react';
20
+ import { createMathDocument, type MathDocument } from '@barocss/math-editor/core';
21
+ import '@barocss/math-editor/style.css';
22
+
23
+ export function Formula({ id, saved, onSave }: {
24
+ id: string;
25
+ saved?: MathDocument;
26
+ onSave: (document: MathDocument, latex: string) => void;
27
+ }) {
28
+ return <MathEditor
29
+ key={id}
30
+ locale="en"
31
+ defaultValue={saved ?? createMathDocument()}
32
+ onChange={onSave}
33
+ showTokenLegend={false}
34
+ label="Formula editor"
35
+ />;
36
+ }
37
+ ```
38
+
39
+ Changing `saved` alone does not replace the document. Change `key` when opening a different document; do not change it on every save. There is no controlled `value` prop or `session` prop on this component. `onChange` reports committed model edits, not IME drafts.
40
+
41
+ ## Rich component props in 0.2.0
42
+
43
+ | Prop | Default / behavior |
44
+ |---|---|
45
+ | `defaultValue` | Optional `MathDocument`, read once |
46
+ | `locale` | `ko`; locale changes preserve history |
47
+ | `toolbar` | `true`; hides/shows all rich toolbar panels |
48
+ | `toolbarEnd` | Optional React content appended to tools |
49
+ | `showTokenLegend` | `true` |
50
+ | `showLineNumbers` | `true`; UI-only multiline gutter |
51
+ | `label` | Localized default accessible group label |
52
+ | `onChange(document, latex)` | Save the document; LaTeX is derived |
53
+ | `onExit(direction)` | Host focus restoration request, `-1` or `1` |
54
+
55
+ Version 0.2.0 supports `excludedStructures` and `multiline` on the rich component, plus configurable `toolbar` kinds and `toolbarMaxItems`.
56
+
57
+ ## Native surface
58
+
59
+ ```tsx
60
+ import { MathEditorSurface } from '@barocss/math-editor/react';
61
+ import type { MathDocument } from '@barocss/math-editor/core';
62
+ import '@barocss/math-editor/style.css';
63
+
64
+ export function InlineFormula({ onSave, onExit }: {
65
+ onSave: (document: MathDocument, latex: string) => void;
66
+ onExit: (direction: -1 | 1) => void;
67
+ }) {
68
+ return <MathEditorSurface
69
+ locale="en"
70
+ mode="inline"
71
+ toolbar={false}
72
+ onChange={onSave}
73
+ onCommit={() => onExit(1)}
74
+ onExit={onExit}
75
+ />;
76
+ }
77
+ ```
78
+
79
+ `MathEditorSurface` accepts [DOM options](API-SESSION.md), plus `className`. It mounts/destroys through React effects and updates mutable options. `defaultValue`, `session` and `menuHost` remain mount-only. For external commands, provide one stable session; `session.load(document)` opens a different trusted document and resets history. The host must clean up its own subscriptions and external toolbar/session. Avoid sharing a session across simultaneous surfaces.
80
+
81
+ ## SSR and popup behavior
82
+
83
+ The native surface mounts on the client through an effect. Keep browser storage and DOM access in client lifecycle code. In server-component frameworks, put the integration in a client component; package import safety does not imply server-rendered editable content or hydration support.
84
+
85
+ For a popup, hold a draft in component state or a draft session. `onChange` should update that draft, not persist to the host document. Apply commits it; Cancel unmounts it. Use a fresh `key` each time a new original is opened. Keep host undo separate and create one host transaction on Apply.
86
+
87
+ ## Common mistakes
88
+
89
+ - Passing `value`, `onCommit`, `mode` or `session` to rich `MathEditor`: use the native surface for those native options.
90
+ - Mirroring each `onChange` into a new component key, destroying undo and IME state.
91
+ - Assuming rich and native components have identical symbol/selection tools.
92
+
93
+ ## Complete React modal with an isolated rich-editor draft
94
+
95
+ The parent supplies a trusted document and receives one Apply callback. Typing and Cancel never call `onApply`. Render a fresh instance for each editing request (for example, a monotonically increasing request key). This uses the supported component API.
96
+
97
+ ```tsx
98
+ import { useEffect, useRef } from 'react';
99
+ import { MathEditor } from '@barocss/math-editor/react';
100
+ import type { MathDocument } from '@barocss/math-editor/core';
101
+ import '@barocss/math-editor/style.css';
102
+
103
+ export function FormulaDialog({ original, onApply, onClose }: {
104
+ original: MathDocument;
105
+ onApply: (document: MathDocument) => void;
106
+ onClose: () => void;
107
+ }) {
108
+ const dialog = useRef<HTMLDialogElement>(null);
109
+ const draft = useRef(structuredClone(original));
110
+ const finished = useRef(false);
111
+ useEffect(() => {
112
+ const element = dialog.current!;
113
+ const previous = document.activeElement;
114
+ element.showModal();
115
+ return () => {
116
+ element.close();
117
+ if (previous instanceof HTMLElement && previous.isConnected) previous.focus();
118
+ };
119
+ }, []);
120
+ function finish(apply: boolean) {
121
+ if (finished.current) return;
122
+ finished.current = true;
123
+ if (apply) onApply(structuredClone(draft.current));
124
+ onClose(); // Parent unmounts this component.
125
+ }
126
+ return <dialog ref={dialog} aria-label="Edit formula"
127
+ onCancel={event => { event.preventDefault(); finish(false); }}>
128
+ <MathEditor defaultValue={original} locale="en" showTokenLegend={false}
129
+ onChange={document => { draft.current = document; }} />
130
+ <button type="button" onClick={() => finish(false)}>Cancel</button>
131
+ <button type="button" onClick={() => finish(true)}>Apply</button>
132
+ </dialog>;
133
+ }
134
+ ```
135
+
136
+ `onApply` is a synchronous host-state commit in this example. If saving requires a server request, keep a pending/error state, disable duplicate Apply, and close only after success. Do not replace `defaultValue` on every keystroke.
137
+
138
+ ## Native React surface with Undo, Redo and document switching
139
+
140
+ This is an alternative to the rich modal. The stable session is created once per mounted component; loading a different trusted `source` resets its history. Keep `source` stable while saving: do not feed each emitted document back into it.
141
+
142
+ ```tsx
143
+ import { useEffect, useState } from 'react';
144
+ import { MathEditorSurface } from '@barocss/math-editor/react';
145
+ import { createMathSession, type MathDocument } from '@barocss/math-editor/core';
146
+ import '@barocss/math-editor/style.css';
147
+
148
+ export function SessionFormula({ source }: { source: MathDocument }) {
149
+ const [session] = useState(() => createMathSession({ document: source, locale: 'en' }));
150
+ const [snapshot, setSnapshot] = useState(() => session.getSnapshot());
151
+ useEffect(() => {
152
+ const unsubscribe = session.subscribe(next => setSnapshot(next));
153
+ setSnapshot(session.getSnapshot());
154
+ return () => { unsubscribe(); session.destroy(); };
155
+ }, [session]);
156
+ useEffect(() => { session.load(source); }, [session, source]);
157
+ return <>
158
+ <button disabled={!snapshot.canUndo}
159
+ onClick={() => session.execute({ type: 'undo' })}>Undo</button>
160
+ <button disabled={!snapshot.canRedo}
161
+ onClick={() => session.execute({ type: 'redo' })}>Redo</button>
162
+ <MathEditorSurface session={session} locale="en" />
163
+ <pre>{snapshot.latex}</pre>
164
+ </>;
165
+ }
166
+ ```
167
+
168
+ ## Persistence and ownership
169
+
170
+ Persist the `MathDocument` passed to `onChange`; LaTeX is derived output; the bounded loader supports the documented round-trip subset. The package does not parse arbitrary LaTeX. Validate externally supplied JSON before loading it: loading assumes a trusted, structurally valid document with unique IDs. There is no server save or collaboration transport built in.
171
+
172
+ Keep the mount host empty. The editor owns its descendants; framework rendering into the same host can destroy caret and composition state. Use one editing surface per session. Destroy the renderer on teardown; a session supplied by the host remains the host's responsibility.
173
+
174
+ See [session and DOM API](API-SESSION.md) for exact options, commands, events and cleanup, and [renderer differences](ADAPTERS.md#current-renderer-parity) before choosing a native wrapper over the rich React editor.
175
+
176
+
177
+ ## Undoable LaTeX import (0.2.0)
178
+
179
+ ```tsx
180
+ import { useRef } from 'react';
181
+ import { MathEditor, type MathEditorHandle } from '@barocss/math-editor';
182
+
183
+ function Editor() {
184
+ const api = useRef<MathEditorHandle>(null);
185
+ return <>
186
+ <button onClick={() => {
187
+ const result = api.current?.importLatex(String.raw`\frac{a}{b}`);
188
+ if (result && !result.ok) console.log(result.diagnostics);
189
+ }}>Load example</button>
190
+ <MathEditor apiRef={api} />
191
+ </>;
192
+ }
193
+ ```
194
+
195
+ Import validates the whole expression and host `multiline`/`excludedStructures` restrictions before committing. Success fires `onChange` and creates one undo step; failure leaves content and history unchanged. It refuses replacement during active composition. `MathEditorSurface` uses `session.importLatex` instead. See [LaTeX scope](LATEX-SCOPE.md) for supported grammar and limits.
196
+
197
+
198
+ ## Rich editor embedded in a document (workspace)
199
+
200
+ Use `autoFocus`, `toolbar={false}`, `showTokenLegend={false}`, `showLineNumbers={false}` and `enterBehavior="commit"` for an in-place rich editor. Keep `onChange` in a local draft, commit that draft in `onCommit`, and discard it in `onCancel`. Suggestions and selection wrapping take priority over Enter; Escape closes suggestions before requesting cancellation. IME composition reserves its keys. `Shift+Enter` retains normal line/grid behavior subject to `multiline`.
201
+
202
+ Wrap the editor in a non-editable island when embedding inside contenteditable. The host must exclude nested events, selections and renderer mutations from its own text pipeline. Do not save a host transaction for every math keystroke. Set `--me-font-size` on the wrapper to scale token previews and inputs together; its default is22px.
package/API-SESSION.md ADDED
@@ -0,0 +1,163 @@
1
+ # Session and native DOM API reference
2
+
3
+ This reference applies to pure JavaScript, `MathEditorSurface`, Web Component, Vue, Svelte and Solid native integrations in 0.2.0. Rich React `MathEditor` owns separate history and has its own [props](API-REACT.md).
4
+
5
+ ## Imports
6
+
7
+ ```ts
8
+ import { createMathSession, createMathDocument } from '@barocss/math-editor/core';
9
+ import { mountMathEditor, mountMathToolbar, mountMathLatex, mountMathPreview }
10
+ from '@barocss/math-editor/dom';
11
+ import '@barocss/math-editor/style.css';
12
+ ```
13
+
14
+ ## createMathSession(options)
15
+
16
+ | Option | Type | Default |
17
+ |---|---|---|
18
+ | `document` | Trusted `MathDocument` | Empty document |
19
+ | `locale` | Registered locale string | `ko` |
20
+ | `mode` | `block` or `inline` | `block` |
21
+
22
+ | Method | Contract |
23
+ |---|---|
24
+ | `getSnapshot()` | Copy containing `state`, optional `range`, `locale`, `mode`, `canUndo`, `canRedo`, `latex` |
25
+ | `subscribe(listener)` | Listener receives `(snapshot, documentChanged)`; returns unsubscribe |
26
+ | `execute(command)` | Boolean result; text/structure/template/newline/undo/redo |
27
+ | `apply(state)` | Apply `MathState`; returns false for invalid inline multiline state |
28
+ | `select(caret)` | Set `MathCaret`, clear structural range, emit non-document change |
29
+ | `selectRange(range?)` | Set/clear `MathRange`, emit non-document change |
30
+ | `configure({ locale?, mode? })` | Keep document/history; throws on multiline-to-inline change |
31
+ | `load(document)` | Open trusted document and reset history/range; emits document change |
32
+ | `copy()` | `MathFragment` for structural range, or undefined |
33
+ | `paste(fragment)` | Boolean result; rejects multiline in inline mode |
34
+ | `destroy()` | Release subscribers; host must also destroy renderers/tools |
35
+
36
+ A snapshot's `state` contains `document` and `caret`. Never mutate a snapshot expecting it to affect the session. Use a command or `apply` instead. `false` is also returned for undo/redo when no history change occurs; it is not always an error.
37
+
38
+ ## Commands
39
+
40
+ ```ts
41
+ session.execute({ type: 'text', value: 'x + α' });
42
+ session.execute({ type: 'structure', kind: 'fraction' });
43
+ session.execute({ type: 'template', id: 'quadratic' });
44
+ session.execute({ type: 'newline' });
45
+ session.execute({ type: 'undo' });
46
+ session.execute({ type: 'redo' });
47
+ ```
48
+
49
+ These examples assume an existing session. Structure kinds: `fraction`, `root`, `superscript`, `subscript`, `parentheses`, `brackets`, `absolute`, `sum`, `product`, `integral`, `matrix`, `aligned`, `cases`. A selected structural range can only wrap in supported wrapping kinds. Use exported grid helpers plus `apply()` for grid resizing; there is no `execute({ type: 'resizeMatrix' })` command.
50
+
51
+ ## mountMathEditor(host, options)
52
+
53
+ `host` must be a real HTMLElement. Mount on the client, once per surface.
54
+
55
+ | Option | Contract |
56
+ |---|---|
57
+ | `session` | Host session; mount-only; survives renderer destruction |
58
+ | `defaultValue` | Trusted document for an internally created session; mount-only |
59
+ | `menuHost` | Suggestion portal HTMLElement; mount-only; nearest native dialog or body by default |
60
+ | `locale`, `mode` | Configure locale/mode; same defaults as session |
61
+ | `toolbar` | Boolean or readonly structure kind list; block defaults on, inline off |
62
+ | `showLineNumbers` | Whether UI-only multiline gutter is displayed |
63
+ | `enterBehavior` | `newline` or `commit`; block defaults newline, inline commit |
64
+ | `onChange(document, latex)` | Committed document changes |
65
+ | `onCommit(snapshot)` | Host completion request; no automatic save or block creation |
66
+ | `onCancel()` | Escape after dismissing suggestions/grid state; host decides whether to close |
67
+ | `onExit(direction)` | Host caret handoff, `-1` before or `1` after |
68
+
69
+ Return value: `{ session, focus(), update(options), destroy() }`. `update` accepts mutable options only and merges them. Locale updates preserve history. Do not pass a fresh session to `update`; recreate the renderer if changing ownership.
70
+
71
+ Inline mode means one top-level row, not a fixed visual height. It rejects newline commands and multiline paste and throws when loading multiline documents. Enter accepts suggestions first; grid keyboard operations have their own priority. CSS alone cannot establish these model constraints.
72
+
73
+ ## Saving without caret-only writes
74
+
75
+ ```ts
76
+ const unsubscribe = session.subscribe((snapshot, documentChanged) => {
77
+ if (!documentChanged) return;
78
+ localStorage.setItem('formula', JSON.stringify(snapshot.state.document));
79
+ });
80
+ // On teardown: unsubscribe(); editor.destroy(); session.destroy();
81
+ ```
82
+
83
+ If a server persists changes asynchronously, debounce/serialize writes and handle failed saves in your host. The library does not resolve write races or validate arbitrary documents returned by a server.
84
+
85
+ ## Independent toolbar and outputs
86
+
87
+ `mountMathToolbar(host, session, { kinds?, onExecute? })` exposes `destroy()`. Use `onExecute: () => editor.focus()` to return keyboard focus after choosing a tool. `mountMathLatex(host, session)` renders derived LaTeX and returns `destroy()`.
88
+
89
+ `mountMathPreview(host, session, { render(host, snapshot) })` delegates rendering to your application. `render` may return a cleanup function. The math-editor package does not ship KaTeX as a runtime requirement; import your chosen renderer separately. All output handles must be destroyed when their hosts are removed.
90
+
91
+ ## Reusable browser-storage adapter with restore and error reporting
92
+
93
+ This adapter avoids writes for caret changes, debounces edits, flushes on page hide, and removes its listeners on teardown. Restore runs before subscribing so opening saved content does not immediately write it back. It does not validate documents itself: supply your application's schema validator as `decode`, which must throw for invalid or unsupported JSON. Use a different key per document. Call `dispose()` before changing the session's document/key.
94
+
95
+ ```ts
96
+ import type { MathDocument } from '@barocss/math-editor/core';
97
+ import { createMathSession } from '@barocss/math-editor/core';
98
+
99
+ type Session = ReturnType<typeof createMathSession>;
100
+ export function attachBrowserStorage(
101
+ session: Session,
102
+ key: string,
103
+ decode: (json: unknown) => MathDocument,
104
+ onError: (error: unknown) => void,
105
+ ) {
106
+ // Invoke on the client only. Storage may be unavailable or full.
107
+ try {
108
+ const stored = localStorage.getItem(key);
109
+ if (stored !== null) session.load(decode(JSON.parse(stored)));
110
+ } catch (error) { onError(error); }
111
+ let timer: ReturnType<typeof setTimeout> | undefined;
112
+ let pending: MathDocument | undefined;
113
+ function flush() {
114
+ clearTimeout(timer);
115
+ timer = undefined;
116
+ if (!pending) return;
117
+ try {
118
+ localStorage.setItem(key, JSON.stringify(pending));
119
+ pending = undefined;
120
+ } catch (error) { onError(error); }
121
+ }
122
+ const unsubscribe = session.subscribe((snapshot, documentChanged) => {
123
+ if (!documentChanged) return;
124
+ pending = snapshot.state.document;
125
+ clearTimeout(timer);
126
+ timer = setTimeout(flush, 300);
127
+ });
128
+ window.addEventListener('pagehide', flush);
129
+ return {
130
+ flush,
131
+ dispose() {
132
+ unsubscribe();
133
+ window.removeEventListener('pagehide', flush);
134
+ flush();
135
+ },
136
+ };
137
+ }
138
+ ```
139
+
140
+ The host owns validation because it knows supported structures, size limits and schema versions. Do not replace `decode` with a TypeScript cast. Page-hide flushing is best effort, not a guarantee against crashes; localStorage is not cross-device persistence or multi-tab conflict resolution. Display `onError` failures and offer a retry via `flush()`.
141
+
142
+ ## Failure and compatibility boundaries
143
+
144
+ - All adapters are ESM; no CommonJS/global-script API is promised.
145
+ - Loading expects trusted structured JSON; arbitrary LaTeX parsing and evaluation are not supported.
146
+ - Native and rich React editing UX differ; see [the parity table](ADAPTERS.md#current-renderer-parity).
147
+ - One session is not a concurrent multi-user collaboration engine.
148
+ - Host UI, permissions, persistence, clipboard environment restrictions and SSR boundaries remain the integrator's responsibility.
149
+
150
+
151
+ ## LaTeX import (0.2.0)
152
+
153
+ ```ts
154
+ const result = session.importLatex(String.raw`\frac{a}{b}`);
155
+ if (!result.ok) {
156
+ // No mutation or subscription notification. Retain the caller's source.
157
+ console.log(result.diagnostics);
158
+ }
159
+ ```
160
+
161
+ This operation replaces the current formula in one undo step; redo restores the imported formula. Inline sessions reject multiple top-level lines before modifying state. All framework adapters using a session share this API; Web Components expose it as `element.session.importLatex(source)`.
162
+
163
+ For state-independent inspection, use `parseLatex(source, { multiline?, excludedStructures? })` from `/core`. For opening a different saved document, validate JSON with `parseMathDocument` and call `session.load`. Loading deliberately resets history, importing does not. See [the exact syntax contract](LATEX-SCOPE.md); arbitrary LaTeX macros are not supported. These APIs are available in 0.2.0.
package/API-SOLID.md ADDED
@@ -0,0 +1,90 @@
1
+ # Solid API guide
2
+
3
+ The `mathEditor` directive mounts the native editor, updates options in a reactive effect and tears down through Solid's owner cleanup.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @barocss/math-editor@0.2.0 solid-js
9
+ ```
10
+
11
+ ## Complete component
12
+
13
+ ```tsx
14
+ import { createSignal } from 'solid-js';
15
+ import { mathEditor } from '@barocss/math-editor/solid';
16
+ import '@barocss/math-editor/style.css';
17
+
18
+ // Preserve the value import used by the JSX directive transform.
19
+ void mathEditor;
20
+ export function Formula() {
21
+ const [locale, setLocale] = createSignal('en');
22
+ const [latex, setLatex] = createSignal('');
23
+ return <>
24
+ <button onClick={() => setLocale(locale() === 'en' ? 'ko' : 'en')}>Language</button>
25
+ <div use:mathEditor={{
26
+ locale: locale(),
27
+ mode: 'inline',
28
+ toolbar: false,
29
+ onChange(document, output) {
30
+ setLatex(output);
31
+ // Persist document through your application here.
32
+ },
33
+ }} />
34
+ <pre>{latex()}</pre>
35
+ </>;
36
+ }
37
+ ```
38
+
39
+ The adapter augments Solid's JSX `Directives` type. Keep the directive import as a runtime value, and compile this with Solid's JSX transform, not the React transform.
40
+
41
+ ## Reactive options and sessions
42
+
43
+ The directive reads an accessor in `createEffect`, so a signal read in its options updates the renderer. It mounts once; `defaultValue`, `session` and `menuHost` stay mount-only. There is no controlled document prop.
44
+
45
+ Create an external `createMathSession()` once in the component body when you need commands, undo state or an external toolbar. Pass it as `session`, use `session.load(validatedDocument)` to open another document, and register your own subscription/session cleanup with `onCleanup`. Do not create sessions in reactive option expressions.
46
+
47
+ ## Host lifecycle
48
+
49
+ The directive calls `editor.destroy()` on cleanup. It cannot clean up arbitrary host event listeners or save subscriptions. Its host must remain an empty DOM element in your JSX because the editor owns descendants. DOM mounting is client-only; use the framework's client-only boundary when integrating into an SSR route.
50
+
51
+ For inline or popup integration, use the native `mode`, `enterBehavior`, `onCommit`, `onCancel` and `onExit` options described in the shared API. Those notifications do not automatically create the next prose block or close a modal.
52
+
53
+ ## History, templates and saved-document restoration
54
+
55
+ This complete component keeps the session outside reactive effects, subscribes to snapshots, and releases the subscription on teardown. Save is in-memory; Restore opens that snapshot and clears undo history.
56
+
57
+ ```tsx
58
+ import { createSignal, onCleanup } from 'solid-js';
59
+ import { mathEditor } from '@barocss/math-editor/solid';
60
+ import { createMathSession, createMathDocument } from '@barocss/math-editor/core';
61
+ import '@barocss/math-editor/style.css';
62
+
63
+ void mathEditor;
64
+ export function FormulaWithHistory() {
65
+ let saved = createMathDocument('x');
66
+ const session = createMathSession({ document: saved, locale: 'en' });
67
+ const [snapshot, setSnapshot] = createSignal(session.getSnapshot());
68
+ const unsubscribe = session.subscribe(next => setSnapshot(next));
69
+ onCleanup(() => { unsubscribe(); session.destroy(); });
70
+ return <>
71
+ <button disabled={!snapshot().canUndo}
72
+ onClick={() => session.execute({ type: 'undo' })}>Undo</button>
73
+ <button disabled={!snapshot().canRedo}
74
+ onClick={() => session.execute({ type: 'redo' })}>Redo</button>
75
+ <button onClick={() => session.execute({ type: 'template', id: 'quadratic' })}>Quadratic formula</button>
76
+ <button onClick={() => { saved = session.getSnapshot().state.document; }}>Save</button>
77
+ <button onClick={() => session.load(saved)}>Restore saved</button>
78
+ <div use:mathEditor={{ session, locale: 'en' }} />
79
+ <pre>{snapshot().latex}</pre>
80
+ </>;
81
+ }
82
+ ```
83
+
84
+ ## Persistence and ownership
85
+
86
+ Persist the `MathDocument` passed to `onChange`; LaTeX is derived output and is not an editable round-trip format. The package does not parse arbitrary LaTeX. Validate externally supplied JSON before loading it: loading assumes a trusted, structurally valid document with unique IDs. There is no server save or collaboration transport built in.
87
+
88
+ Keep the mount host empty. The editor owns its descendants; framework rendering into the same host can destroy caret and composition state. Use one editing surface per session. Destroy the renderer on teardown; a session supplied by the host remains the host's responsibility.
89
+
90
+ See [session and DOM API](API-SESSION.md) for exact options, commands, events and cleanup, and [renderer differences](ADAPTERS.md#current-renderer-parity) before choosing a native wrapper over the rich React editor.