barocss / math

Session and native DOM API reference

This reference applies to pure JavaScript, MathEditorSurface, Web Component, Vue, Svelte and Solid native integrations. The base session/DOM API is available in 0.2.0; the native discovery/context toolbar, setDisabled and precise pointer-selection changes below are additions in 0.4.0. Rich React MathEditor owns separate history and has its own props.

Imports

import { createMathSession, createMathDocument } from '@barocss/math-editor/core';
import { mountMathEditor, mountMathToolbar, mountMathLatex, mountMathPreview }
  from '@barocss/math-editor/dom';
import '@barocss/math-editor/style.css';

createMathSession(options)

Option Type Default
document Trusted MathDocument Empty document
locale Registered locale string ko
mode block or inline block
Method Contract
getSnapshot() Copy containing state, optional range / matrixRange, locale, mode, canUndo, canRedo, latex
subscribe(listener) Listener receives (snapshot, documentChanged); returns unsubscribe
execute(command) Boolean result; text/structure/template/newline/undo/redo and matrix selection/clear/transpose
apply(state) Apply MathState; returns false for invalid inline multiline state
select(caret) Set MathCaret, clear structural range, emit non-document change
selectRange(range?) Set/clear MathRange, emit non-document change
selectMatrixRange(range?) Validate and set/clear a cell rectangle; clear structural range, emit non-document change; false leaves state unchanged
copyMatrix() Copy selected cells as MathMatrixFragment, or undefined
pasteMatrix(fragment) Paste at the selected rectangle or active matrix cell as one edit; false leaves document, selection and history unchanged
configure({ locale?, mode? }) Keep document/history; throws on multiline-to-inline change
load(document) Open trusted document and reset history/range; emits document change
copy() MathFragment for structural range; matrix rectangle becomes a single-matrix fragment; otherwise undefined
paste(fragment) Boolean result; rejects multiline in inline mode
destroy() Release subscribers; host must also destroy renderers/tools

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.

Matrix cell selections

Available in 0.4.0. MathRange and MathMatrixRange are mutually exclusive. select(caret), document changes, load and Undo/Redo clear cell selection. Selecting cells does not emit a document change or create history.

import { createMathSession } from '@barocss/math-editor/core';

const session = createMathSession({ document: savedDocument });
// Assume a loaded matrix has three columns; select rows 1–2, columns 2–3.
session.selectMatrixRange({ matrixId, anchor: 1, focus: 5 });
const cells = session.copyMatrix();
// Move to another matrix/cell or select its destination rectangle first.
if (cells && !session.pasteMatrix(cells)) {
  // The destination shape or 20 × 20 bound rejected this paste; no partial write.
}
session.execute({ type: 'matrix-select' }); // Current cell, ready for Shift+arrows.
session.execute({ type: 'matrix-select', all: true });
session.execute({ type: 'matrix-clear' });
session.execute({ type: 'matrix-transpose' }); // Entire selected/active matrix.

MathMatrixRange is { matrixId: string, anchor: number, focus: number }. Endpoints are zero-based row-major cell indices. Reversed endpoints work; bounds are derived from row and column coordinates, not the linear span between indices. MathMatrixFragment is { version: 1, columns, cells: MathRow[], environment }. Its cells are row-major and may contain any supported nested formula.

Destination Paste behavior
Caret in a matrix or one selected cell Replace whole cells from that anchor; grow/pad the matrix up to 20 × 20
Larger selected rectangle Require matching dimensions; a single copied cell fills every selected cell
Different rectangle dimensions, invalid payload or size overflow Return false with no write, selection change or history entry
Outside a matrix in the browser UI Insert the copied rectangle as a new matrix through its normal math-fragment fallback

Pasting keeps the destination delimiter, preserves unselected cells, and assigns fresh IDs recursively to inserted content. Typing over a cell selection clears its contents and inserts text into the upper-left cell. Delete/Cut clear contents without deleting rows or columns. Enter/Escape resumes editing; Shift+arrows extends the rectangle. Transpose reorders the entire matrix, retaining existing cell IDs, delimiter and nested caret. Each content operation takes one Undo.

Browser clipboard handlers write MATH_MATRIX_CLIPBOARD_TYPE (application/x-barocss-math-matrix+json), the existing math fragment MIME, and plain LaTeX. Read external matrix payloads with parseMatrixFragment; normal parseFragment remains the structural clipboard parser. paste(fragment) does not overwrite a cell rectangle: use pasteMatrix for that operation. Tab-separated spreadsheet data inside a matrix is treated as cell text, not interpreted LaTeX. Clipboard formats can be stripped by other applications; OS interoperability is a separate check. Neither selection nor clipboard metadata changes MathDocument v1.

Commands

session.execute({ type: 'text', value: 'x + α' });
session.execute({ type: 'structure', kind: 'fraction' });
session.execute({ type: 'template', id: 'quadratic' });
session.execute({ type: 'newline' });
session.execute({ type: 'undo' });
session.execute({ type: 'redo' });

These examples assume an existing session. kind accepts the exported StructureKind; see the structure catalog for the current kinds and slot contracts. 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.

mountMathEditor(host, options)

host must be a real HTMLElement. Mount on the client, once per surface.

Option Contract
session Host session; mount-only; survives renderer destruction
defaultValue Trusted document for an internally created session; mount-only
menuHost Suggestion portal HTMLElement; mount-only; nearest native dialog or body by default
menuAvoidElements Workspace: optional callback returning host controls the suggestion menu should avoid; measured in their current position when the menu opens or moves
locale, mode Configure locale/mode; same defaults as session
toolbar Boolean or readonly structure kind list; block defaults on, inline off
toolbarMaxItems Initially visible structure buttons; defaults to eight; More tools reveals the expanded controls
showLineNumbers Whether UI-only multiline gutter is displayed
enterBehavior newline or commit; block defaults newline, inline commit
onChange(document, latex) Committed document changes
onRender() Workspace: final DOM/caret notification, including focus and selection-only updates; no automatic model write
onCommit(snapshot) Host completion request; no automatic save or block creation
onCancel() Escape after dismissing suggestions/grid state; host decides whether to close
onExit(direction) Host caret handoff, -1 before or 1 after

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.

onRender runs synchronously after the native surface/menu update, before host mutation observers reconcile their selections. It can also run during initial mounting, before mountMathEditor returns. Hosts can inspect the current input and schedule caret restoration; avoid issuing commands or calling focus() from the callback, which can trigger another render. This notification is distinct from onChange and does not imply a document or history change. It is available in 0.4.0.

Notifications are suppressed during composition and after teardown. A notification does not guarantee that an input is focused. Any deferred caret restoration should check that its input is still connected and focused, so a later callback cannot revive a closed draft or steal focus from another control.

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.

For a compact draft, pass menuAvoidElements: () => [actionsElement] to keep suggestions clear of Apply, Cancel and tool-expansion controls. The callback can return elements added after mounting. Placement remains constrained by the viewport and actual CSS clipping ancestors; a dialog role alone does not limit the menu to the dialog height. Fixed menus can extend beyond a compact dialog while remaining its DOM descendants. The menu scrolls when the available gap is short. This option changes presentation only and adds no model data.

Saving without caret-only writes

const unsubscribe = session.subscribe((snapshot, documentChanged) => {
  if (!documentChanged) return;
  localStorage.setItem('formula', JSON.stringify(snapshot.state.document));
});
// On teardown: unsubscribe(); editor.destroy(); session.destroy();

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.

Independent toolbar and outputs

mountMathToolbar(host, sessionOrNull, { kinds?, maxItems?, locale?, onExecute? }) mounts a toolbar whose current workspace handle exposes:

Method Contract
setSession(sessionOrNull) Switch the active draft subscription; null disables mutations; switching/detaching closes symbol search
setDisabled(boolean) Disable mutations while keeping the session attached; disabling closes symbol search; restore with false after composition
destroy() Remove the toolbar, its panels and session subscription

maxItems defaults to eight. locale supplies labels before a session is attached; an active session supplies its own locale. Use onExecute: () => editor.focus() to return focus after a successful command. Opening or searching the symbol browser leaves focus in that browser. The toolbar uses the session's caret/range even when the math input has blurred.

The workspace More tools section exposes the full symbol catalog with search, templates, matrix/identity presets and symbol shortcuts. kinds filters structure buttons, templates containing excluded kinds, and grid presets/context controls; it does not limit model or parser support. Active matrices provide row/column operations and delimiter selection; aligned/cases provide row operations. Grid mutations are disabled while text or a model range is selected. The symbol panel stays inside its toolbar host, and Escape closes it without requesting host Cancel.

An internally mounted toolbar is disabled during native formula composition. Hosts mounting an independent toolbar must coordinate composition with setDisabled, or temporarily detach with setSession(null). Host integrations also own containment: keep the toolbar and its panels inside the declared toolbarHost so an outside-click handler does not commit the draft while browsing. See adapter embedding for lifecycle ownership.

mountMathLatex(host, session) renders derived LaTeX and returns destroy().

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.

The website's sized PNG download/copy controls belong to its preview renderer. They are not native DOM/session image-export methods; a reusable library SVG/PNG export API remains future work.

Native selection behavior

Shift+Left/Right extends or shrinks a model range; Shift+Up/Down extends across top-level lines using logical offsets. In the workspace, passive highlights cover exact selected text endpoints. Dragging within the active input keeps native text selection; crossing its boundary can extend a model range across structures. Structural normalization remains unchanged: a MathRange crossing fraction slots or matrix cells selects their common structure. Cross-cell pointer gestures instead create a separate MathMatrixRange in the workspace; see the matrix selection contract above.

After a model selection, plain Left/Up restores the caret at its document-ordered start; Right/Down restores it at the end. The same rule applies to reverse drags and Shift+arrow selections, without consuming another character or changing the formula. Alt+Up/Down browses the wrapping suggestions while retaining the range; Enter applies a candidate. Ordinary input suggestions still use Up/Down.

The first Escape dismisses a selection's wrapping suggestions while preserving the selection and draft. A subsequent Escape reaches the host Cancel callback. Changing the selection or beginning a new pointer gesture makes suggestions available again.

The active native input still edits a whole logical text run with one role color. Passive runs retain lexical colors. The cell-selection work adds full-matrix transpose through existing model structures; no new LaTeX grammar is needed. Native OS IME validation and additional complete locale packs remain deferred; see renderer parity for remaining limits.

Reusable browser-storage adapter with restore and error reporting

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.

import type { MathDocument } from '@barocss/math-editor/core';
import { createMathSession } from '@barocss/math-editor/core';

type Session = ReturnType<typeof createMathSession>;
export function attachBrowserStorage(
  session: Session,
  key: string,
  decode: (json: unknown) => MathDocument,
  onError: (error: unknown) => void,
) {
  // Invoke on the client only. Storage may be unavailable or full.
  try {
    const stored = localStorage.getItem(key);
    if (stored !== null) session.load(decode(JSON.parse(stored)));
  } catch (error) { onError(error); }
  let timer: ReturnType<typeof setTimeout> | undefined;
  let pending: MathDocument | undefined;
  function flush() {
    clearTimeout(timer);
    timer = undefined;
    if (!pending) return;
    try {
      localStorage.setItem(key, JSON.stringify(pending));
      pending = undefined;
    } catch (error) { onError(error); }
  }
  const unsubscribe = session.subscribe((snapshot, documentChanged) => {
    if (!documentChanged) return;
    pending = snapshot.state.document;
    clearTimeout(timer);
    timer = setTimeout(flush, 300);
  });
  window.addEventListener('pagehide', flush);
  return {
    flush,
    dispose() {
      unsubscribe();
      window.removeEventListener('pagehide', flush);
      flush();
    },
  };
}

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().

Failure and compatibility boundaries

  • All adapters are ESM; no CommonJS/global-script API is promised.
  • Loading expects trusted structured JSON; arbitrary LaTeX parsing and evaluation are not supported.
  • Native and rich React editing UX differ; see the parity table.
  • One session is not a concurrent multi-user collaboration engine.
  • Host UI, permissions, persistence, clipboard environment restrictions and SSR boundaries remain the integrator's responsibility.

LaTeX import (0.2.0)

const result = session.importLatex(String.raw`\frac{a}{b}`);
if (!result.ok) {
  // No mutation or subscription notification. Retain the caller's source.
  console.log(result.diagnostics);
}

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).

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; arbitrary LaTeX macros are not supported. These APIs are available in 0.2.0.

Editing utilities — workspace

These additions are included in 0.4.0. Earlier releases do not provide these APIs.

LaTeX insertion

session.pasteLatex(source) inserts at the current caret or replaces the selected model range. It returns LatexInsertionResult: success includes the parsed document and resulting state; failure includes source offsets and diagnostics. It creates one Undo entry, leaves the caret after the inserted fragment, and generates fresh IDs. It does not reset history or replace the entire document. importLatex(source) continues to replace the whole formula.

const result = session.pasteLatex(String.raw`\frac{a}{b} + x_i^2`);
if (!result.ok) console.log(result.diagnostics);

Empty input, unsupported syntax, multiline insertion inside a nested slot or inline session, structures in literal text, and matrix-rectangle targets are rejected without changing document, caret, selection or history. Ordinary clipboard paste is still literal. More tools → Paste as LaTeX opens a nonmodal source field. Alt+Shift+V opens it even when the toolbar is hidden; Ctrl/Cmd+Enter inserts and Escape closes. The UI accepts a user paste and does not read the system clipboard automatically.

The pure insertLatex(state, source, range?, options?) helper supplies the same behavior without a session. The rich React MathEditorHandle.pasteLatex method applies the same helper with its multiline/excluded-structure policy.

Recent items and favorites

createMathPreferences(saved?) creates a UI-only store. Each session has session.preferences; pass an existing store in createMathSession({ preferences }) to share it. An internally owned native session also accepts mount-only DOMMathEditorOptions.preferences. Rich React accepts preferences as a prop.

import { createMathPreferences, createMathSession } from '@barocss/math-editor/core';

const preferences = createMathPreferences();
const first = createMathSession({ preferences });
const second = createMathSession({ preferences });
preferences.toggleFavorite('symbol-α');
preferences.toggleFavorite('template-quadratic');
const saved = preferences.getSnapshot(); // { version: 1, recent: [], favorites: [...] }
const restored = createMathPreferences(saved);

The default store lives in memory. The host can serialize getSnapshot() and use subscribe(listener) to persist changes; unsubscribe when finished. No localStorage, cookies, network calls or user identity are implicit. record(id) moves a known item to the front of the 12-entry recent list. toggleFavorite(id) adds/removes a favorite, bounded to 100. clearRecent() clears recents. Unknown IDs are ignored; restored lists are validated and deduplicated. These actions do not change formula JSON, document change callbacks or Undo.

More tools → Recent & favorites supplies search, All/Recent/Favorites filters and separate star buttons. Symbol/template selections are recorded; arbitrary typed text and full imported formulas are not stored. Toolbar template filters still apply. A preference store can outlive an individual editor.

Contextual presentation settings

Place the caret inside a fence, fraction/binomial, or supported limit/sum/product/integral. The toolbar displays settings for the nearest applicable structure. Nested contents, IDs and caret remain unchanged. A structural or matrix range disables these settings.

import { activePresentation } from '@barocss/math-editor/core';
const target = activePresentation(session.getSnapshot().state);
if (target) session.execute({
  type: 'presentation', id: target.id, change: { mathStyle: 'text' },
});

Choose the change appropriate to the target:

Change Accepted values Applies to
fence parentheses, brackets, braces, angle, absolute, norm, openClosed, closedOpen Structural fences; changes both ends together
mathStyle default, display, text Fractions and binomials
limits default, above-below, side Supported limit, sum/product and integral variants

default removes the explicit metadata. setMathPresentation(state, id, change) is the pure equivalent. Unsupported changes or stale IDs return the original state; the session command returns false. This is presentation editing of existing syntax, not algebraic conversion or general TeX styling.

Toolbars expose closePanels() to dismiss their nonmodal utilities without editing the formula. onOpenPanel and onPasteLatex are optional mounting callbacks for a host that coordinates its own utility panels. Built-in surfaces keep only one utility/discovery panel open at a time.

Keyboard access to transformations

In React and native DOM fields, Alt+Down opens or reopens suggestions and selects the first available contextual transformation. Up/Down browses and Enter applies; Escape dismisses. On macOS, Alt is the Option key. Root and bracket transformations preserve the existing operand. The shortcut works with toolbar: false and contextTools: false, including inline mode. Normal text input continues to use the standard suggestion ordering; a context-only menu requires navigation before Enter can apply a change instead of a host commit.

Low-level boundary and vertical navigation helpers

The core exports unwrapNext(state) and joinNextLine(state) as immutable state operations, symmetric to unwrapPrevious and joinPreviousLine. Unsupported positions return the original state. Apply a changed state through a session/history transaction; these helpers do not commit it themselves.

moveVertical(state, direction, geometry) remains stateless. A custom editing surface can use createVerticalNavigation() for repeated vertical movement: call move(state, direction, geometry) and apply the returned caret. Call reset() after horizontal navigation, text/model changes or pointer placement. Keep this helper local to one view; never serialize its preferred column with the formula.

Keyboard help and separate toolbars

F1 opens localized keyboard/clipboard help in a focused React or native field. The native DOMMathEditor.showHelp() method and rich React MathEditorHandle.showHelp() method open the same help for a custom host button. Closing restores the previous input selection; opening help does not add a document/history entry.

A toolbar mounted separately from its field must identify the owner explicitly:

const field = mountMathEditor(fieldHost, { session, toolbar: false });
const toolbar = mountMathToolbar(toolbarHost, session, {
  onExecute: () => field.focus(),
  onHelp: () => field.showHelp(),
});

Built-in toolbars connect automatically. A separate toolbar without onHelp omits its help button instead of guessing which field to target. F1 still works in the field. Destroy the field to remove any open help. The help UI does not read the OS clipboard. Key remapping is not part of this API.

Barocss Math Editor core · 0.8.0 (workspace) · MIT License