@drghaliasri/butex
v7.1.4
Published
MathJax extension layer for Arabic-friendly math notation (XeLaTeX-like customs beyond plain MathJax)
Maintainers
Readme
BuTeX
Integrators: treat Integration contract (host apps) as the source of truth for wiring MathJax and BuTeX. The npm package ships dist/ and this README.md only; design notes and ADRs live in the GitHub repo under docs/ (they are not included in the published tarball).
BuTeX is a browser-side foundation for Arabic mathematical typography and equation editing on top of MathJax. It ships thin MathJax extensions such as \arabsqrt (Arabic-style mirrored radical; CommonHTML uses CSS mirror/unmirror, SVG uses the MathJax SVG pipeline plus BuTeX SVG helpers where applicable), optional GUI/document layers, and helpers such as parseBuTeX. The longer-term direction is a GUI editor driven by structured equation ASTs.
The vendored MathJax-src/ folder in this repo is reference only — runtime integration uses the mathjax npm package.
Project direction
The intended editor model is AST-first:
- A GUI edits equation nodes rather than raw LaTeX strings.
- BuTeX will maintain local TypeScript ASTs for English and Arabic equation structures so users can switch views.
- A remote service may send equations as JSON compatible with the local ASTs; the browser imports that JSON and renders/edit it locally.
- Arabic/English TeX strings are generated from the AST for MathJax rendering. Remote-rendered strings can be useful for testing/debugging, but should not be the editor state.
- The Python files in references/ are reference material for the emerging JSON shape and conversion behavior.
Rendering outputs and MVP scope
- CommonHTML (
chtml) and SVG are supported. Load the matching MathJax 4 bundle (tex-chtml.jsvstex-svg.js) and pass the same mode asoutputtorenderBuTeXMathIsland/mountBuTeXMathIsland.<ButexEditor />and document math preview follow the loaded bundle when possible (tex2chtmlvstex2svg). \arsqrt: canonical serialized Arabic TeX command with an optional index and mandatory radicand (same argument shape as\sqrt).- Compatibility macros: MathJax accepts
\arabsqrtas an alias of\arsqrt;\unit{...}and\idx{...}are browser-side passthrough wrappers for imported/reference TeX. - Arabic surface parser (
parseBuTeX) — optional helper that maps a tiny subset of Arabic command names to MathJax-safe TeX before rendering (see below). Long-term editor state is structured AST/JSON, not this string transform alone.
Arabic preprocessor (parseBuTeX)
Convert Arabic-friendly math surface syntax to plain MathJax TeX:
| Input | Output |
| ------------------- | -------------------------------- |
| \جذر[3]{س} | \arabsqrt[3]{\text{س}} |
| \كسر{1}{2} | \frac{1}{2} |
| \كسر{\جذر{س}}{10} | \frac{\arabsqrt{\text{س}}}{10} |
| \جتا | \arcos |
Pure string transform — call parseBuTeX(tex), pass the result to MathJax.
import { parseBuTeX } from 'butex';
const mjTex = parseBuTeX(String.raw`\جذر[3]{س}`);
// await renderBuTeXMathIsland(mjTex, { display: true, output: 'chtml' }) ...Run tests with npm test. Live parser demo: demo/parser.html (after npm run build; serve the repo root).
Install
npm install butex mathjaxPeer dependency: mathjax ^4.x (aligned with MathJax 4 components). If you use butex/react, also install react and react-dom (^18 or ^19).
Usage (browser)
- Load MathJax (e.g.
tex-chtml.jsortex-svg.js) after settingwindow.MathJaxconfig. - Load BuTeX’s IIFE bundle (
dist/index.global.jsexposes globalBuTeX). - In
MathJax.startup.ready, callBuTeX.registerBuTeX(MathJax)beforeMathJax.startup.defaultReady(). - Inject styles once:
BuTeX.injectBuTeXStyles()(or embedBuTeX.BUTEX_CHROME_CSSyourself). - When using SVG (
tex-svg.js), callBuTeX.registerBuTeXSvgTextWrapper(MathJax)afterdefaultReady()so Takween/Diwani/Maghribi text,\ad, and Arabic atomic commands (\arsin, etc.) render in the preview with the correct fonts. CommonHTML (tex-chtml.js) uses the injected CSS classes instead. - Render math with
renderBuTeXMathIsland(tex, options?)or mount into a host element viamountBuTeXMathIsland(host, tex, options?). Passoutput: 'svg'when the host loadstex-svg.js, oroutput: 'chtml'withtex-chtml.js. For raw Arabic TeX that did not come from the editor AST, passmirrorOperators: trueto wrap directional operators according to BuTeX's shared operator table.ButexEditorpicks SVG vs CHTML automatically from the loaded MathJax bundle (tex2svgvstex2chtml).
Ensure TeX packages includes butex-arabic-math (use BUTEX_TEX_PACKAGE in config when using { '[+]': [...] }). BuTeX registers lightweight browser compatibility macros for toolbar symbols such as \coloneqq and \eqqcolon; XeLaTeX export still relies on the document preamble's mathtools.
Usage (npm / bundler)
MathJax entrypoints (bundlers)
Exact import strings depend on your bundler and how it resolves the mathjax package. Typical ESM imports for MathJax 4 components:
| Desired output | Typical import |
| -------------- | -------------- |
| CommonHTML | import MathJax from 'mathjax/tex-chtml.js' |
| SVG | import MathJax from 'mathjax/tex-svg.js' |
Use the same mode in renderBuTeXMathIsland / mountBuTeXMathIsland via output: 'chtml' or output: 'svg'. If resolution fails, point your import at whatever path your build resolves to the same component bundle (see MathJax’s docs for your version).
import MathJax from 'mathjax/tex-chtml.js'; // or tex-svg.js — match `output` in render calls
import {
registerBuTeX,
injectBuTeXStyles,
renderBuTeXMathIsland,
BUTEX_TEX_PACKAGE,
} from 'butex';
// Before startup resolves — same timing rules as browser:
MathJax.startup.ready = () => {
registerBuTeX(MathJax);
injectBuTeXStyles();
MathJax.startup.defaultReady();
};
MathJax.config.tex = {
packages: { '[+]': ['ams', BUTEX_TEX_PACKAGE] },
};Maintainer-facing design notes live in the GitHub repo under docs/ (not shipped on npm).
Integration contract (host apps)
BuTeX supports two integration modes. Keep this contract for stable behavior.
1) Render-only contract (MathJax + BuTeX macros)
Use this when you only need to render LaTeX strings:
- Register BuTeX with MathJax via
registerBuTeX(MathJax). - Include
butex-arabic-mathin TeX packages. - Inject BuTeX MathJax styles once via
injectBuTeXStyles(document)(or embedBUTEX_CHROME_CSS). - Render expressions with
renderBuTeXMathIsland/mountBuTeXMathIsland(setoutputto match the host bundle:tex-svg.jsvstex-chtml.js). - For raw Arabic TeX, set
mirrorOperators: trueor callmirrorBuTeXOperatorsInTex(tex)before non-island MathJax typesetting. Leave it off for editor-generated TeX because the editor already emits\butexmirror{...}. - For Arabic-friendly surface strings, optionally preprocess with
parseBuTeX(...)before rendering.
This mode does not require the GUI editor runtime.
Host CSS resets (Tailwind Preflight and similar)
MathJax renders SVG equations as inline <svg> elements, and MathJax 4's inline
line-breaking can emit one equation as several sibling <svg> chunks. Global
CSS resets that declare svg { display: block } (e.g. Tailwind Preflight)
stack those chunks vertically, so equations render one term per line.
BuTeX's injected styles guard against this with:
mjx-container svg {
display: inline;
}The guard ships in both injectBuTeXStyles() (BUTEX_CHROME_CSS) and
injectBuTeXDocument2Styles() (DOCUMENT2_WIDGET_CSS), so make sure the host
calls at least one of them (or embeds the CSS constants). If your app injects
neither and you see stacked/wrapped equations under Tailwind, add the rule above
to your global stylesheet after the reset.
Also pin the MathJax version the host loads (e.g. [email protected]) instead of a
floating major tag, so CDN updates cannot change rendering behavior silently.
2) GUI editor contract (shippable editor UX)
Use this when you want the same editor UX as the demo:
- Inject editor styles once via
injectBuTeXEditorStyles(document)(or embedBUTEX_EDITOR_CSS). - Create runtime with
Editor.createEditorRuntime({ ... }). - Pass your editor surface element as
surfaceEl. - Optionally pass
buttonElements(undo/redo/copy/cut/split toggle) for auto button state refresh. - Wire your toolbar/actions to runtime methods (
toggleSide,insertDelimiterByKind,addSup,addSub,removeSup,removeSub,deleteStructure,performUndo,performRedo,performCopy,performCut,performPaste). - Use
onSessionChange(session)to render external previews (e.g., MathJax pane, status labels). - Imported
MathObject/CharObjectfor the editor: document LaTeX preview treatsCharObject.expras already-rendered Arabic TeX (e.g.\text{م}). When opening an imported equation in<ButexEditor />viamathObjectToEditorSession, BuTeX unwraps supported LaTeX text/font wrappers into plain glyphexprplus editorcharacterFont. Supported patterns:\text{…},\takween{…},\diwani{…},\butextakween{…},\butexdiwani{…},\butexdiwanioutline{…},\butexmaghribi{…}, and nested forms such as\text{\takween{…}}. On save, default-font chars stay plainCharObjectnodes; non-default fonts are stored as fontCommandObjectwrappers so editor round-trip preservescharacterFont. Upstream converters may still emit wrappedexpron import.
Minimal browser example:
BuTeX.injectBuTeXEditorStyles(document);
const runtime = BuTeX.Editor.createEditorRuntime({
surfaceEl: document.getElementById('surface'),
onSessionChange: (session) => {
// host-render math preview/status here
},
});React widget (butex/react)
Shipped as a separate entry so apps that do not use React never pull it in.
- Import:
import { ButexEditor } from 'butex/react'. - Peer dependencies when using this entry:
reactandreact-dom(^18 or ^19). - The component wraps
Editor.createEditorRuntime(toolbar, surface, MathJax preview strip, optional dev panels viadebug). - Touch devices use a textarea-backed input bridge, so tapping the structured equation surface opens the software keyboard and supports normal text, deletion, and composed/IME input without making TeX strings the editor state. When
window.visualViewportexposes valid metrics, BuTeX automatically follows its visible bounds and re-reveals the active caret after keyboard or orientation resizing;100dvhremains the no-API fallback. - Load MathJax and register BuTeX before relying on the preview (same timing as the GUI contract above). Equation preview in
ButexEditoruses the samerenderBuTeXMathIslandpath as document math islands (output follows the loaded bundle: SVG whentex2svgis available, otherwise CHTML). Optional legacymountBuTeXMathTypeset(typesetPromise) remains exported for hosts that still rely on it. - Next.js App Router: put BuTeX in a client component (
'use client').
'use client';
import { ButexEditor } from 'butex/react';
export default function Page() {
return <ButexEditor uiLocale="en" defaultSide="english" showSideSwitcher={false} />;
}uiLocale selects Arabic ("ar", the default) or English ("en") GUI labels, tooltips, errors, accessibility text, and chrome direction. defaultSide independently selects the initial "english" or "arabic" equation side. Side controls remain visible by default for standalone compatibility; pass showSideSwitcher={false} to hide both the desktop switch and compact-sheet side choices without removing either synchronized equation tree.
Equation-toolbar responsiveness is automatic and package-owned. At a layout viewport width of 768px or less, the wrapped desktop toolbar is replaced by a compact command hierarchy: Undo, Redo, Fraction, Superscript, Subscript, Delimiters, Delete, and More. Advanced commands remain available in localized Structures, Functions, Operators, Matrices, Typography, Spacing, and Digit forms sheets. The rendered preview stays mounted but starts collapsed in compact mode so the structured surface receives the available height. Above 768px the existing desktop toolbar and preview remain unchanged. Use responsiveMode="desktop" or responsiveMode="compact" only when the host intentionally needs to override the automatic workflow.
Compact command sheets trap focus while open. Dismissing a sheet restores its trigger, while running an editing command restores the textarea-backed input bridge. Wide and nested equations pan locally inside the structured surface; edits and caret navigation reveal the active position by the smallest required horizontal movement without scrolling the host page or reacting to panning alone.
There is no configurable breakpoint or keyboard-aware prop. These behaviors are automatic enhancements and do not change the equation AST or session API. BuTeX never calls window.scrollTo or locks the host document body; hosts should leave the editor's fixed overlay outside transformed or unexpectedly clipping ancestors and should test their own nested scrolling shell with the target mobile keyboards.
Document AST (butex/document)
Use this headless entry when a host app or remote service already has DocumentObject JSON and needs a structured import/export layer.
import {
fromDocumentJson,
createEmptyDocument,
renderDocumentLatex,
buildDocumentPreview,
} from 'butex/document';
const documentNode = fromDocumentJson({
node_type: 'DocumentObject',
blocks: [{ command: '\\section', value: 'Intro $x$' }],
});
const emptyDocument = createEmptyDocument();
const latex = renderDocumentLatex(documentNode);
const preview = buildDocumentPreview(documentNode);V1 supports simple headings, paragraphs, itemize / enumerate, tabular, includegraphics, raw blocks, and math spans detected inside text. The document layer detects math delimiters and can align them with ordered imported MathObject JSON; it does not parse arbitrary equation LaTeX into chain nodes in the browser.
Document React widget (butex/react-document)
Use this entry for the first document-editor UI. It edits supported document blocks, shows a live semantic preview, and opens the equation editor for supported imported math islands.
'use client';
import { ButexDocumentEditor } from 'butex/react-document';
export default function Page() {
return (
<ButexDocumentEditor
debug
onLatexChange={(latex) => console.log(latex)}
/>
);
}The preview is semantic HTML for document structure. Math is emitted as escaped per-span islands, so host apps still need the normal MathJax + BuTeX registration when they want typeset math preview rather than TeX placeholders.
Document AST v2 (butex/document2)
Use this parallel v2 headless entry for the token-owned document model. Imported delimited math is converted into math tokens between prose tokens; normal editing should mutate text tokens and math tokens separately rather than treating raw delimited TeX as one textarea value.
import {
fromDocumentJson2,
document2Latex,
document2Preview,
} from 'butex/document2';
const documentNode = fromDocumentJson2({
node_type: 'DocumentObject',
blocks: [{ command: '\\paragraph', value: 'نص $x$' }],
});
const latex = document2Latex(documentNode);
const preview = document2Preview(documentNode, 'svg');document2Latex(doc) emits a complete Arabic XeLaTeX document by default (preamble + \begin{document} + body + \end{document}). Structured and editor-authored Arabic roots serialize with the canonical \arsqrt; the bundled preamble also accepts raw legacy \arabsqrt with the same optional-index signature. It defines BuTeX font wrappers (\butextakween, \butexdiwani, \butexdiwanioutline, \butexmaghribi) and the MathJax-compatible \butexmirror bridge. The title-path basmala keeps its Maghribi title styling; the fixed closing hamdala is emitted as normal centered \maghribi text rather than display math so it participates in ordinary column/page flow. Pass { wrapDocument: false } for body-only blocks. Paragraph blocks export as \par followed by the inline content, so prose, citations, and math are not wrapped inside \paragraph{...}. Optional digitsMapping: 'arabicdigits' | 'digits' controls eastern vs western digit font mapping in the preamble (default 'arabicdigits'). Pass { twocolumn: true } for \documentclass[12pt,a4paper,notitlepage,twocolumn]{article}; title content stays in-column and Document2 figure/table floats use fixed [H] placement (with the float package) so source block order remains predictable. Figures continue to use \columnwidth. See examples/example-latex-export.tex for a full sample (regenerated by the wrap vitest). Hosts can also call getArabicXeLatexPreamble() alone.
For XeLaTeX compiler images, install src/fonts/Almaghribi-Warsh-Quran.otf as a system font and refresh fontconfig (fc-cache -f -v). Verify with fc-match "Almaghribi Warsh Quran"; the preamble uses that installed family name.
To add another character font, add its metadata to src/editor/characterFonts.ts and its browser @font-face asset to src/diwani-font.ts.
Figures and tables export as centered figure / table environments. In one-column export they retain normal [htbp] float placement; in twocolumn export they use fixed [H] placement to avoid unexpected reordering/gaps. Simple editor table column specifications made only from l, c, and r (for example lll) receive a visible grid by default (|l|l|l| plus \hline); explicitly structured specifications such as c|c are preserved. Optional caption/label fields emit \caption / \label; a labeled figure/table without a caption uses \refstepcounter{figure} / \refstepcounter{table} before its label so the reference owns the correct counter. Display math stays \[…\] unless an equation label is enabled, then export uses \begin{equation}…\label{…}\end{equation}. Internal cross-refs still store \ref / \eqref tokens separately from bibliography \cite, but LaTeX export adds the known Arabic object type (الشكل, الجدول, or المعادلة) when the reference target is known and the prose immediately before the token has not already named that type.
V2 exports Arabic TeX by default for equations saved from the embedded editor. Imported raw-only math is preserved as raw source and marked non-editable until a structured equation object is attached.
Headless document commands
Canonical Document2Json exports include stable block IDs, list-item IDs, and an inline_ids sidecar for every text-bearing field. Table cells use a parallel cell_inline_ids matrix. Each sidecar contains a stable field_id plus text/math/citation/reference token IDs and their JavaScript string offsets. Legacy input may omit all identities; its next import/export normalization adds them lazily.
Blocks may also carry creation provenance as metadata: { source: 'agent' | 'user' }. The editor and preview expose it as data-butex-block-source and intentionally ship no provenance styling.
Use the pure command and outline helpers for server-side session transforms:
import {
applyDocument2Command,
document2Outline,
fromDocumentJson2,
toDocumentJson2,
} from '@drghaliasri/butex/document2';
const canonical = toDocumentJson2(fromDocumentJson2(legacyDocument));
const outline = document2Outline(canonical);
const updated = applyDocument2Command(canonical, {
op: 'insert_text_block',
kind: 'paragraph',
text: 'New session text',
anchor: { end: true },
});Block insertions accept { end: true }, { before_block_id }, or { after_block_id }; missing targets fail explicitly. The command union also includes stable-ID inline token operations, list/item operations, and table cell/row/column operations. List and table targets are found recursively, while row and column coordinates are zero-based. Column shape changes require the complete resulting LaTeX columns specification.
Article authoring commands can move top-level blocks, patch meta, manage the ordered reference catalog by normalized key, insert the derived bibliography block, revise figure/table captions and labels, and update an existing figure's host asset association or include value. Reference anchors use { before_reference_key }, { after_reference_key }, or { end: true }. Asset upload/storage and session revisions remain host-owned; see docs/fastapi-document2-worker-contract.md for the complete command and error contract.
Whole-field replacement rejects formatted text, citations, references, and math so an agent cannot silently discard structured content. Inline math insertion/replacement requires one complete delimited source plus a matching structured MathObject; BuTeX does not parse raw LaTeX into an equation AST.
Document worker CLI
Node 20+ can run the same transforms through the isolated butex-document2 executable. One-shot mode reads one request from stdin and writes one response to stdout:
printf '%s' '{"action":"normalize","document":{"node_type":"DocumentObject","blocks":[]}}' | butex-document2Railway HTTP mode is stateless and requires a service token:
BUTEX_WORKER_TOKEN=replace-me PORT=3000 \
butex-document2 --serve --host 0.0.0.0It exposes GET /health plus authenticated POST /v1/document2/normalize, /v1/document2/outline, and /v1/document2/commands. The worker accepts document JSON and returns transformed JSON only. FastAPI remains responsible for user authentication, article/session lookup, revisions, idempotency, S3 persistence, and MCP policy; browsers and agents must never call the worker directly.
Document JSON contract (value + math_objects)
Both document layers import the same shape. A text field holds value (the full string, with math delimiters kept in place) and an optional parallel math_objects array that supplies the structured equation AST for each math span found in that string.
Rules:
- Detect, then align. The importer scans
valuefor delimiters ($…$,\(…\),\[…\],$$…$$, and math environments) and binds each detected span tomath_objectsin reading order (left-to-right). - Count and order must match. Extra or missing entries produce a diagnostic; the browser never silently remaps.
math_mode/closingon eachMathObjectmust match the delimiter invalue. - Per field, not per document. Each paragraph, heading, and list
itemcarries its ownmath_objects. - Tables are the exception: one flat
math_objectsarray lives on the\begin{tabular}block and is consumed row-major across all cells (each cell takes as many entries as it has spans). - Missing
math_objects→ the span renders as a non-editable raw math chip from its delimited source. - Mixed raw and structured math uses
nullplaceholders so every detected span still has one positional entry, for examplemath_objects: [null, structuredMathObject]. A labeled raw display span uses aRawMathObjectpositional metadata entry so its label survives without pretending the equation has an editable AST. - The browser does not parse equation LaTeX bodies into
ChainClass; structured chains come frommath_objects(import) or the equation editor (GUI).
Paragraph with one inline span:
{
"command": "\\paragraph",
"value": "لدينا $x^2$ ثابت.",
"math_objects": [
{ "node_type": "MathObject", "math_mode": "$",
"lines": [{ "node_type": "ChainClass", "chain": [] }], "closing": "$" }
]
}List item with a display span (math_mode/closing match the delimiter in value):
{
"value": "الحل \\[a+b\\]",
"math_objects": [
{ "node_type": "MathObject", "math_mode": "\\[",
"lines": [{ "node_type": "ChainClass", "chain": [] }], "closing": "\\]" }
]
}Table: one flat array, consumed row-major (here $x$, then $y$):
{
"command": "\\begin{tabular}",
"columns": "cc",
"rows": [["الرمز $x$", "القيمة"], ["الرمز $y$", "القيمة"]],
"math_objects": [
{ "node_type": "MathObject", "math_mode": "$",
"lines": [{ "node_type": "ChainClass", "chain": [] }], "closing": "$" },
{ "node_type": "MathObject", "math_mode": "$",
"lines": [{ "node_type": "ChainClass", "chain": [] }], "closing": "$" }
]
}Document React widget v2 (butex/react-document2)
Use this entry for the new document editor integration. It opens embedded <ButexEditor /> sessions in Arabic/RTL mode by default and renders document preview math islands with MathJax SVG by default (mathOutput defaults to 'svg'; pass mathOutput="chtml" only if the host loads tex-chtml.js).
'use client';
import { ButexDocumentEditor2 } from 'butex/react-document2';
export default function Page() {
return (
<ButexDocumentEditor2
debug
uiLocale="en"
documentDirection="ltr"
equationSide="english"
editableEquations
onLatexChange={(latex) => console.log(latex)}
/>
);
}Persisting Document Editor v2 state
Document2Node is the live editor AST. It contains class-backed equation nodes and must not cross an API, database, local-storage, worker, or JSON.stringify boundary. Persist the canonical Document2Json wire format instead. The backend may store and return that JSON unchanged.
Use onDocumentJsonChange for a persistence-ready snapshot:
'use client';
import { useRef } from 'react';
import { ButexDocumentEditor2 } from '@drghaliasri/butex/react-document2';
import type { Document2Json } from '@drghaliasri/butex/document2';
export function ArticleEditor({ loadedDocument }: { loadedDocument: Document2Json }) {
const latestDocument = useRef<Document2Json>(loadedDocument);
async function saveArticle() {
await fetch('/api/article', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ document: latestDocument.current }),
});
}
return (
<>
<ButexDocumentEditor2
initialDocument={loadedDocument}
editableEquations
onDocumentJsonChange={(json) => {
latestDocument.current = json;
}}
/>
<button type="button" onClick={saveArticle}>Save</button>
</>
);
}onDocumentChange remains available for hosts that need the live AST. Convert it before persistence with toDocumentJson2:
import { toDocumentJson2 } from '@drghaliasri/butex/document2';
onDocumentChange={(node) => {
latestDocument.current = toDocumentJson2(node);
}}Do not call JSON.stringify(documentNode) directly. JSON serialization removes CharNode, CommandNode, and other equation prototypes: the saved TeX source can still render in preview, but the structured equation cannot reopen in the editor. Also do not feed every change callback back into initialDocument; that prop is for initial or externally loaded data, and changing it resets the editor state and undo history. Keep the current save snapshot in a ref or separate state that is not passed back as initialDocument. Change callbacks run during editing, so debounce network autosaves or save explicitly.
Host-driven figure insert
After an external upload (for example S3), insert a figure on the live editor without remounting or rewriting initialDocument:
'use client';
import { useRef } from 'react';
import {
ButexDocumentEditor2,
type ImageAssetRef,
type ButexDocumentEditor2Ref,
} from '@drghaliasri/butex/react-document2';
export function ArticleEditorWithUpload() {
const editorRef = useRef<ButexDocumentEditor2Ref>(null);
async function onFileUploaded(uuid: string) {
// Sets asset_id and \\includegraphics{assets/<uuid>.jpg} after the focused block.
// Do not call addDocument2ImageBlock outside the component or remount with key++.
editorRef.current?.insertImageBlock({ assetId: `assets/${uuid}.jpg` } satisfies ImageAssetRef);
}
return (
<ButexDocumentEditor2
ref={editorRef}
resolveImageUrl={({ assetId, value }) =>
assetId ? `https://cdn.example/${assetId}` : value
}
onRequestImagePick={async ({ current }) => {
/* open host asset picker; return { assetId, value?, label?, thumbUrl? } or null */
return current;
}}
onDocumentJsonChange={(json) => {
/* persist json */
}}
/>
);
}insertImageBlock matches the toolbar figure button (focus-aware insert + undo snapshot). Pass { assetId, value?, label?, thumbUrl? } to set both live assetId and wire asset_id; passing a non-empty string remains supported and treats that string as both value and asset_id. New figures are centered by default, and each image block exposes a Center figure checkbox backed by the existing JSON centered field and LaTeX \centering output. Use updateImageBlockAsset(blockId, ref) to set a host asset on an existing image block, updateImageBlockValue(blockId, value) to change only the path value while preserving any existing asset identity, and getDocumentJson() to read the canonical wire JSON without host-side AST mutation. BuTeX never uploads image bytes; hosts provide resolveImageUrl, optionally onRequestImagePick, listImageAssets, or renderImageBlockEditor for picker/inventory UI.
Host apps still register BuTeX with MathJax before preview rendering. uiLocale selects Arabic ("ar", the default) or English ("en") document-editor chrome and is passed to the embedded equation editor. documentDirection independently controls prose inputs and preview flow without creating a second document tree. equationSide independently controls whether structured equations open, render, and save from the "english" or "arabic" side; the embedded drawer hides side-switching controls so editing remains locked to that host-selected side. Set editableEquations={false} to show math islands without equation insertion, deletion, or editor access. Set previewOnly={true} to render only the read-only document preview with no toolbar, editor panel, or equation drawer. Optional \includegraphics asset_id is preserved on import; pass resolveImageUrl={({ assetId, value }) => …}) so preview can load S3/CDN/local assets while plain value URLs keep working without a resolver. Document JSON may include root meta (title, authors, structured Hijri date { day, month, year } with Arabic month names such as "محرم", abstract) plus a references catalog (key, authors, title, year, url, venue) and \cite{key1,key2} spans in text values. Pass documentMeta to seed missing meta fields from the host; JSON keys win when present. The editor always centers a fixed basmala line before the title block and a closing ḥamdala after the body (preview + LaTeX). Hijri date uses day/month/year dropdowns (no calendar conversion; default year 1448). The editor shows numeric cite chips and a bibliography block. RTL preview/chips render reversed labels such as [٣،٢،١] (Arabic comma); LTR stays [1, 2, 3]. Pass digitForm (western / arabicIndic / persianIndic, defaulting from documentDirection) or use the toolbar digit control. LaTeX export keeps logical \cite{…} key order for XeLaTeX; digit shaping and bidi display in PDF belong in the host preamble (bidi/polyglossia + font Mapping, see references/commands.py). Range compression ([3–7]) is not implemented yet. Raw-only equations keep their original source because the browser does not parse raw LaTeX into equation ASTs. Document editor v2 defaults to tex-svg.js; use tex-chtml.js only if you pass mathOutput="chtml".
Styling/theming contract
- Core editor classes (
.surface,.chain,.slot,.node,.scripts,.delim*, etc.) are shipped fromsrc/editor/styles.ts. - Theme with one namespace,
--butex-*, on.butex-widget(or an ancestor of the editor surface). Defaults are set onButexEditor’s root; hosts override accents, borders,--butex-surface-bg/--butex-surface-fg,--butex-preview-bg/--butex-preview-fg, focus/slot/selection, scripts, optional--butex-dev-*/--butex-dev-inset-*when usingdebug, etc., without redefining structure classes. - Document editor theming is scoped under
.butex-document-widget. Override--butex-document-*variables on that root or an ancestor, especially--butex-document-bg,--butex-document-fg,--butex-document-panel,--butex-document-border,--butex-document-accent,--butex-document-preview-bg,--butex-document-drawer-bg,--butex-document-input-bg,--butex-document-table-border,--butex-document-math-bg, and--butex-document-dev-bg. - Document editor v2 theming is scoped under
.butex-document2-widget. Override--butex-document2-*variables on that root or an ancestor; defaults inherit from the existing--butex-*variables where practical, including accent, panel, border, preview, focus, danger/error, and debug colors. The embedded equation editor opens as a centered modal (backdrop + panel); its BuTeX chrome reads the same--butex-document2-*tokens via a scoped bridge, so hosts normally theme once on.butex-document2-widgetwithout a second equation theme. The built-in editor uses a sticky compact icon toolbar (Arabicنصformatting icons, heading glyphs, inline$…$vs display\[…\]math buttons with tooltips, a labeled references menu, and a table size popover), focus-aware block insertion after the active block, ↑/↓ block reorder, math delete (chip × and drawer button), document undo/redo (toolbar + Ctrl/⌘Z, Ctrl/⌘Shift+Z, Ctrl+Y) via AST snapshots, and theme-tinted block cards. Preview tables keep black cell borders and scroll inside their preview region when their columns cannot shrink safely; raw\rawblocks stay out of preview until dedicated rendering exists.
Both React editors register shrinkable inline-size containers for local overflow containment, but interaction mode follows the layout viewport, not the editor container. At a viewport width of 900px or narrower, Document Editor v2 automatically presents one principal panel at a time with an accessible Edit/Preview control; at 768px or narrower, the equation editor uses its compact toolbar and categorized sheets. This keeps a narrow editor inside a wide desktop page on the refined desktop workflow. Opening a software keyboard does not change modes because visualViewport is used only to reposition the equation modal, not to select a toolbar. At 430px or narrower, phone spacing, safe areas, and local scrolling apply. previewOnly continues to render Preview alone without either toolbar.
Both components accept responsiveMode="auto" | "desktop" | "compact" ("auto" is the default). Hosts normally use one component tree and leave the mode automatic; force a mode only for an intentional desktop or touch-oriented shell:
<ButexEditor responsiveMode="desktop" />
<ButexDocumentEditor2 responsiveMode="compact" />desktop and compact select the toolbar/panel workflow. Physical phone modal sizing remains viewport-based, so forcing compact mode in a wide kiosk does not turn the equation drawer into an unnecessarily wide full-screen overlay.
Edit/Preview changes are announced through a localized polite status region. Closing an embedded equation restores the originating math chip or text/block position; saving focuses the updated or newly inserted equation location, and deletion focuses adjacent editable text when possible. Nested equation command sheets remain above the drawer in the focus stack, so Escape closes only the top surface.
The automatic 900px and 768px breakpoints remain package defaults; responsiveMode is the supported override when a host deliberately needs another workflow. BuTeX does not display document-level saved/dirty state: persistence remains owned by the host through onDocumentJsonChange, onDocumentChange, or the imperative JSON getter. Wide matrices and other unbreakable equations scroll inside the equation surface, preview, or document math island; caret movement reveals the active position without intentionally scrolling the host page.
Keeping the Document Editor v2 toolbar sticky
<ButexDocumentEditor2 /> injects style#butex-document2-widget-styles; its main toolbar ships with position: sticky, top: var(--butex-document2-sticky-top), and a stacking layer. If a strict CSP blocks injected inline styles, embed DOCUMENT2_WIDGET_CSS through the host's approved global stylesheet or nonced style path, after Tailwind Preflight or other resets.
Do not override .butex-document2-widget__toolbar with position: static or position: fixed. Avoid overflow: auto, scroll, or hidden on editor ancestors unless that ancestor is intentionally the constrained vertical scrolling container; sticky positioning follows the nearest such ancestor. Avoid transform, filter, or contain values that turn an ancestor into a containing block for the fixed equation overlay. overflow-x: clip by itself does not require an override. For a fixed application header, set the supported sticky-offset variable on the editor root:
.article-editor.butex-document2-widget {
--butex-document2-sticky-top: var(--app-header-height, 0px);
}The default stacking order is toolbar (--butex-document2-z-toolbar: 20), toolbar popovers (--butex-document2-z-popover: 30), document overlays (--butex-document2-z-overlay: 40), then the equation modal (--butex-document2-z-equation: 50). Override those variables on the editor root when coordinating with a host stacking system. Safe-area defaults come from env(safe-area-inset-*); hosts may set --butex-safe-area-top, --butex-safe-area-right, --butex-safe-area-bottom, and --butex-safe-area-left on the editor root when they need explicit values.
When diagnosing host CSS, verify that #butex-document2-widget-styles exists and that getComputedStyle(toolbar).position is sticky before adding overrides.
The Phase 4 viewport and accessibility hardening adds no prop, entrypoint, AST, or JSON migration. The repository's recommended mobile and keyboard matrix, evidence status, and release limits are recorded in docs/responsive-mobile-validation.md.
Migrating to 7.0.0
Version 7 makes the responsive workflows above the package defaults: Document2 switches to its single-panel workflow at a 900px viewport, and the equation editor switches to its compact command hierarchy at a 768px viewport. Existing call sites remain valid; the additive responsiveMode prop is available when a host must force either workflow. Equation/document ASTs, JSON formats, runtime methods, and package entrypoints are unchanged. Upgrades from 6.1.1 also include the validated Document2 public entrypoint and LaTeX/export fixes released in 6.1.2–6.1.5.
Demo
From repo root (after npm run build):
npm run demoOpen:
http://localhost:4173/demo/(MathJax +\arabsqrt)http://localhost:4173/demo/parser.html(parser-only normalize preview)- Equation editor (React): run
cd demo/editor-app && npm install && npm run dev, then open the URL Vite prints (seedemo/editor.htmlfor a short pointer). The equation editor demo loads MathJax SVG frompublic/vendor/and includes portrait/landscape frames, a constrained host scroller, visible-viewport diagnostics, UI-language and equation-side controls, wide equations, and matrices. - Document editor (React): run
cd demo/document-editor-app && npm install && npm run dev, then open the URL Vite prints (seedemo/document.htmlfor a short pointer). - Document editor v2 (React): run
cd demo/document-editor-app2 && npm install && npm run dev, then open the URL Vite prints. The v2 demo loads MathJax SVG, exercises metadata, editable wide math and matrices, lists, tables, figures, citations, and bibliography, and includes 320, 360, 375, 390, 430, 768, 900px, and desktop-width presets. Its realistic host-shell mode adds a sticky application header, constrained vertical scroller, safe-area overrides, surrounding content, locale/side controls, and visible-viewport diagnostics. Optional bottom dev panels (LaTeX + AST) appear only if you append?debug=1to the URL or pass thedebugprop from your host app.
Editor MVP notes (demo/editor-app)
- Editing is AST-first (not raw LaTeX text editing).
- End-user UI is Arabic-first.
- Supported structures in this MVP: chars, numbers, operators, delimiter pairs
(),[],{}, fractions (\frac), and sup/sub chains. - Caret: Left/Right walk every insertion gap in a fixed depth-first order (baseline and nested chains). Up/Down move between superscript and subscript where applicable (from the baseline, Up prefers superscript and Down prefers subscript when both exist). On the Arabic surface, arrow keys follow RTL progression.
- Char/number typing: by default keystrokes merge into the same leaf along the caret gap (digits only extend
number, letters only extendchar). Use the Split leaf typing toolbar control to toggle split mode (one new leaf per keystroke). Delete/Backspace trim inside the merged string when split mode is off. - Structural edits are mirrored across Arabic/English trees; text edits apply to the active side.
- Undo/redo: full-session snapshots (
createUndoRedoStacks,pushUndoRedoSnapshot,restoreUndo,restoreRedo). The demo exposes toolbar buttons plus Ctrl+Z / ⌘Z for undo and Ctrl+Shift+Z / ⌘⇧Z / Ctrl+Y for redo while the editing surface is focused. Caret moves and language switching are not recorded so undo targets content edits only. History caps at ~100 steps by default (DEFAULT_UNDO_HISTORY_MAX_DEPTH). - Selection + copy/cut/paste: range selection lives at the slot level inside a single chain (no cross-chain selection in the MVP). Whole nodes are the copy unit, so superscripts/subscripts/inner subtrees always travel with their owner.
- Keyboard: Shift+ArrowLeft/ArrowRight extend selection (RTL-aware), Ctrl/⌘+C/X/V copy/cut/paste, Backspace/Delete and typing replace an active selection.
- Mouse: click-drag from one slot to another inside the same chain. Cross-chain mouse moves are ignored.
- Clipboard format:
application/x-butex-fragment+jsonwith both EN and AR mirrored nodes (EditorFragmentv1) plus a plain-text LaTeX fallback for cross-app pasting. External plain text re-enters through the typing path so split/merge mode applies. - Future node types: the single helper
nodeChildChains(node)enumerates a node's nested chains. Adding command, env, or math-object kinds (see[references/nodes.py](references/nodes.py)) only requires extending this helper; selection, copy, paste, and id-remap stay unchanged.
- Debug panels are dev-only (
debugprop on<ButexEditor />, or?debug=1in the Vite demo URL).- Includes LaTeX dump, passive chain preview, and copyable command/render log.
Editor theming (CSS variables)
Override these on :root (or a host container) to customize colors:
--butex-bg--butex-fg--butex-muted--butex-accent--butex-border--butex-panel--butex-caret--butex-selected--butex-error--butex-error-bg--butex-accent-hover(optional, demo toolbar)--butex-shadow-sm/--butex-shadow-md(optional, panels)--butex-dev-bg/--butex-dev-border/--butex-dev-badge/--butex-dev-muted(optional, developer-only strips whendebugis on)
License
ISC (BuTeX package). MathJax is Apache-2.0 — see upstream.
