@drghaliasri/butex 5.5.2 → 5.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -271,6 +271,7 @@ Rules:
271
271
  - **Per field, not per document.** Each paragraph, heading, and list `item` carries its own `math_objects`.
272
272
  - **Tables are the exception:** one flat `math_objects` array lives on the `\begin{tabular}` block and is consumed **row-major** across all cells (each cell takes as many entries as it has spans).
273
273
  - **Missing `math_objects`** → the span renders as a non-editable raw math chip from its delimited source.
274
+ - **Mixed raw and structured math** uses `null` placeholders so every detected span still has one positional entry, for example `math_objects: [null, structuredMathObject]`. A labeled raw display span uses a `RawMathObject` positional metadata entry so its label survives without pretending the equation has an editable AST.
274
275
  - The browser **does not** parse equation LaTeX bodies into `ChainClass`; structured chains come from `math_objects` (import) or the equation editor (GUI).
275
276
 
276
277
  Paragraph with one inline span:
@@ -337,6 +338,57 @@ export default function Page() {
337
338
  }
338
339
  ```
339
340
 
341
+ #### Persisting Document Editor v2 state
342
+
343
+ `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.
344
+
345
+ Use `onDocumentJsonChange` for a persistence-ready snapshot:
346
+
347
+ ```tsx
348
+ 'use client';
349
+
350
+ import { useRef } from 'react';
351
+ import { ButexDocumentEditor2 } from '@drghaliasri/butex/react-document2';
352
+ import type { Document2Json } from '@drghaliasri/butex/document2';
353
+
354
+ export function ArticleEditor({ loadedDocument }: { loadedDocument: Document2Json }) {
355
+ const latestDocument = useRef<Document2Json>(loadedDocument);
356
+
357
+ async function saveArticle() {
358
+ await fetch('/api/article', {
359
+ method: 'PUT',
360
+ headers: { 'content-type': 'application/json' },
361
+ body: JSON.stringify({ document: latestDocument.current }),
362
+ });
363
+ }
364
+
365
+ return (
366
+ <>
367
+ <ButexDocumentEditor2
368
+ initialDocument={loadedDocument}
369
+ editableEquations
370
+ onDocumentJsonChange={(json) => {
371
+ latestDocument.current = json;
372
+ }}
373
+ />
374
+ <button type="button" onClick={saveArticle}>Save</button>
375
+ </>
376
+ );
377
+ }
378
+ ```
379
+
380
+ `onDocumentChange` remains available for hosts that need the live AST. Convert it before persistence with `toDocumentJson2`:
381
+
382
+ ```ts
383
+ import { toDocumentJson2 } from '@drghaliasri/butex/document2';
384
+
385
+ onDocumentChange={(node) => {
386
+ latestDocument.current = toDocumentJson2(node);
387
+ }}
388
+ ```
389
+
390
+ 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.
391
+
340
392
  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. 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"`.
341
393
 
342
394
  ### Styling/theming contract
@@ -346,6 +398,20 @@ Host apps still register BuTeX with MathJax before preview rendering. `uiLocale`
346
398
  - 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`.
347
399
  - 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-widget` without 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 use fixed black cell borders; raw `\raw` blocks stay out of preview until dedicated rendering exists.
348
400
 
401
+ #### Keeping the Document Editor v2 toolbar sticky
402
+
403
+ `<ButexDocumentEditor2 />` injects `style#butex-document2-widget-styles`; its main toolbar ships with `position: sticky`, `top: 0`, 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.
404
+
405
+ Do not override `.butex-document2-widget__toolbar` with `position: static`, `position: fixed`, a lower stacking layer, or an unrelated `top`. 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. `overflow-x: clip` by itself does not require an override. For a fixed application header, keep the toolbar sticky and apply only a scoped offset:
406
+
407
+ ```css
408
+ .article-editor .butex-document2-widget__toolbar {
409
+ top: var(--app-header-height, 0px);
410
+ }
411
+ ```
412
+
413
+ When diagnosing host CSS, verify that `#butex-document2-widget-styles` exists and that `getComputedStyle(toolbar).position` is `sticky` before adding overrides.
414
+
349
415
  ## Demo
350
416
 
351
417
  From repo root (after `npm run build`):
@@ -60,7 +60,7 @@ type DocumentJson = {
60
60
  type DocumentBlockJson = {
61
61
  command: string;
62
62
  value?: string;
63
- math_objects?: MathObjectJson[];
63
+ math_objects?: Array<MathObjectJson | null>;
64
64
  options?: Record<string, string>;
65
65
  closing?: string;
66
66
  items?: DocumentListItemJson[];
@@ -69,7 +69,7 @@ type DocumentBlockJson = {
69
69
  };
70
70
  type DocumentListItemJson = {
71
71
  value: string;
72
- math_objects?: MathObjectJson[];
72
+ math_objects?: Array<MathObjectJson | null>;
73
73
  blocks?: DocumentBlockJson[];
74
74
  };
75
75
  type DocumentCommand = '\\section' | '\\subsection' | '\\subsubsection' | '\\paragraph' | '\\begin{itemize}' | '\\begin{enumerate}' | '\\begin{tabular}' | '\\includegraphics' | '\\raw';
@@ -156,6 +156,7 @@ declare function createImageBlock(value?: string, options?: Record<string, strin
156
156
  declare function isSupportedDocumentCommand(command: string): command is DocumentCommand;
157
157
 
158
158
  declare function fromMathObjectJson(json: unknown, mode?: DocumentParseMode): MathNode;
159
+ declare function toMathObjectJson(math: MathNode): MathObjectJson;
159
160
  declare function renderMathNodeLatex(math: MathNode): string;
160
161
  declare function isDisplayMathNode(math: MathNode): boolean;
161
162
 
@@ -250,7 +251,7 @@ declare function mathNodeFromEditorSession(session: EditorSession, mathMode?: st
250
251
  declare function mathSourceFromEditorSession(session: EditorSession, mathMode?: string, closing?: string): string;
251
252
 
252
253
  declare function detectMathSpans(value: string): DetectedMathSpan[];
253
- declare function buildInlines(value: string, mathObjects?: MathObjectJson[], mode?: DocumentParseMode): DocumentInline[];
254
+ declare function buildInlines(value: string, mathObjects?: Array<MathObjectJson | null>, mode?: DocumentParseMode): DocumentInline[];
254
255
 
255
256
  declare function renderDocumentLatex(document: DocumentNode): string;
256
257
  declare function buildDocumentPreview(document: DocumentNode): DocumentPreview;
@@ -307,4 +308,4 @@ declare function replaceMathSpanFromEditor(document: DocumentNode, target: Docum
307
308
  declare function insertMathSpanFromEditor(document: DocumentNode, target: DocumentInlineTarget, insertAt: number, math: MathNode, source?: string): DocumentNode;
308
309
  declare function documentInlineText(inlines: DocumentInline[]): string;
309
310
 
310
- export { type DetectedMathSpan, type DocumentBlockJson, type DocumentBlockNode, type DocumentCommand, type DocumentInline, type DocumentInlineTarget, type DocumentJson, type DocumentListItemJson, type DocumentMathTarget, type DocumentNode, type DocumentParseMode, type DocumentPreview, type DocumentPreviewMathIsland, type ImageBlockNode, type ListBlockNode, type ListItemNode, type MathInline, type MathNode, type MathObjectJson, type NewDocumentBlockKind, type RawBlockNode, type TableBlockNode, type TextBlockNode, type TextInline, addDocumentBlock, addListItem, addNestedListBlock, addTableColumn, addTableRow, buildDocumentPreview, buildInlines, createDocumentBlock, createEmptyDocument, createImageBlock, createListBlock, createRawBlock, createTableBlock, createTextBlock, detectMathSpans, documentInlineText, fromDocumentJson, fromMathObjectJson, insertMathSpanFromEditor, isDisplayMathNode, isSupportedDocumentCommand, mathNodeFromEditorSession, mathSourceFromEditorSession, moveDocumentBlock, removeDocumentBlock, removeListItem, removeTableColumn, removeTableRow, renderDocumentLatex, renderMathNodeLatex, replaceMathSpanFromEditor, replaceMathSpanSource, updateImageBlock, updateListItemValue, updateRawBlockValue, updateTableCell, updateTableColumns, updateTextBlockValue };
311
+ export { type DetectedMathSpan, type DocumentBlockJson, type DocumentBlockNode, type DocumentCommand, type DocumentInline, type DocumentInlineTarget, type DocumentJson, type DocumentListItemJson, type DocumentMathTarget, type DocumentNode, type DocumentParseMode, type DocumentPreview, type DocumentPreviewMathIsland, type ImageBlockNode, type ListBlockNode, type ListItemNode, type MathInline, type MathNode, type MathObjectJson, type NewDocumentBlockKind, type RawBlockNode, type TableBlockNode, type TextBlockNode, type TextInline, addDocumentBlock, addListItem, addNestedListBlock, addTableColumn, addTableRow, buildDocumentPreview, buildInlines, createDocumentBlock, createEmptyDocument, createImageBlock, createListBlock, createRawBlock, createTableBlock, createTextBlock, detectMathSpans, documentInlineText, fromDocumentJson, fromMathObjectJson, insertMathSpanFromEditor, isDisplayMathNode, isSupportedDocumentCommand, mathNodeFromEditorSession, mathSourceFromEditorSession, moveDocumentBlock, removeDocumentBlock, removeListItem, removeTableColumn, removeTableRow, renderDocumentLatex, renderMathNodeLatex, replaceMathSpanFromEditor, replaceMathSpanSource, toMathObjectJson, updateImageBlock, updateListItemValue, updateRawBlockValue, updateTableCell, updateTableColumns, updateTextBlockValue };
@@ -60,7 +60,7 @@ type DocumentJson = {
60
60
  type DocumentBlockJson = {
61
61
  command: string;
62
62
  value?: string;
63
- math_objects?: MathObjectJson[];
63
+ math_objects?: Array<MathObjectJson | null>;
64
64
  options?: Record<string, string>;
65
65
  closing?: string;
66
66
  items?: DocumentListItemJson[];
@@ -69,7 +69,7 @@ type DocumentBlockJson = {
69
69
  };
70
70
  type DocumentListItemJson = {
71
71
  value: string;
72
- math_objects?: MathObjectJson[];
72
+ math_objects?: Array<MathObjectJson | null>;
73
73
  blocks?: DocumentBlockJson[];
74
74
  };
75
75
  type DocumentCommand = '\\section' | '\\subsection' | '\\subsubsection' | '\\paragraph' | '\\begin{itemize}' | '\\begin{enumerate}' | '\\begin{tabular}' | '\\includegraphics' | '\\raw';
@@ -156,6 +156,7 @@ declare function createImageBlock(value?: string, options?: Record<string, strin
156
156
  declare function isSupportedDocumentCommand(command: string): command is DocumentCommand;
157
157
 
158
158
  declare function fromMathObjectJson(json: unknown, mode?: DocumentParseMode): MathNode;
159
+ declare function toMathObjectJson(math: MathNode): MathObjectJson;
159
160
  declare function renderMathNodeLatex(math: MathNode): string;
160
161
  declare function isDisplayMathNode(math: MathNode): boolean;
161
162
 
@@ -250,7 +251,7 @@ declare function mathNodeFromEditorSession(session: EditorSession, mathMode?: st
250
251
  declare function mathSourceFromEditorSession(session: EditorSession, mathMode?: string, closing?: string): string;
251
252
 
252
253
  declare function detectMathSpans(value: string): DetectedMathSpan[];
253
- declare function buildInlines(value: string, mathObjects?: MathObjectJson[], mode?: DocumentParseMode): DocumentInline[];
254
+ declare function buildInlines(value: string, mathObjects?: Array<MathObjectJson | null>, mode?: DocumentParseMode): DocumentInline[];
254
255
 
255
256
  declare function renderDocumentLatex(document: DocumentNode): string;
256
257
  declare function buildDocumentPreview(document: DocumentNode): DocumentPreview;
@@ -307,4 +308,4 @@ declare function replaceMathSpanFromEditor(document: DocumentNode, target: Docum
307
308
  declare function insertMathSpanFromEditor(document: DocumentNode, target: DocumentInlineTarget, insertAt: number, math: MathNode, source?: string): DocumentNode;
308
309
  declare function documentInlineText(inlines: DocumentInline[]): string;
309
310
 
310
- export { type DetectedMathSpan, type DocumentBlockJson, type DocumentBlockNode, type DocumentCommand, type DocumentInline, type DocumentInlineTarget, type DocumentJson, type DocumentListItemJson, type DocumentMathTarget, type DocumentNode, type DocumentParseMode, type DocumentPreview, type DocumentPreviewMathIsland, type ImageBlockNode, type ListBlockNode, type ListItemNode, type MathInline, type MathNode, type MathObjectJson, type NewDocumentBlockKind, type RawBlockNode, type TableBlockNode, type TextBlockNode, type TextInline, addDocumentBlock, addListItem, addNestedListBlock, addTableColumn, addTableRow, buildDocumentPreview, buildInlines, createDocumentBlock, createEmptyDocument, createImageBlock, createListBlock, createRawBlock, createTableBlock, createTextBlock, detectMathSpans, documentInlineText, fromDocumentJson, fromMathObjectJson, insertMathSpanFromEditor, isDisplayMathNode, isSupportedDocumentCommand, mathNodeFromEditorSession, mathSourceFromEditorSession, moveDocumentBlock, removeDocumentBlock, removeListItem, removeTableColumn, removeTableRow, renderDocumentLatex, renderMathNodeLatex, replaceMathSpanFromEditor, replaceMathSpanSource, updateImageBlock, updateListItemValue, updateRawBlockValue, updateTableCell, updateTableColumns, updateTextBlockValue };
311
+ export { type DetectedMathSpan, type DocumentBlockJson, type DocumentBlockNode, type DocumentCommand, type DocumentInline, type DocumentInlineTarget, type DocumentJson, type DocumentListItemJson, type DocumentMathTarget, type DocumentNode, type DocumentParseMode, type DocumentPreview, type DocumentPreviewMathIsland, type ImageBlockNode, type ListBlockNode, type ListItemNode, type MathInline, type MathNode, type MathObjectJson, type NewDocumentBlockKind, type RawBlockNode, type TableBlockNode, type TextBlockNode, type TextInline, addDocumentBlock, addListItem, addNestedListBlock, addTableColumn, addTableRow, buildDocumentPreview, buildInlines, createDocumentBlock, createEmptyDocument, createImageBlock, createListBlock, createRawBlock, createTableBlock, createTextBlock, detectMathSpans, documentInlineText, fromDocumentJson, fromMathObjectJson, insertMathSpanFromEditor, isDisplayMathNode, isSupportedDocumentCommand, mathNodeFromEditorSession, mathSourceFromEditorSession, moveDocumentBlock, removeDocumentBlock, removeListItem, removeTableColumn, removeTableRow, renderDocumentLatex, renderMathNodeLatex, replaceMathSpanFromEditor, replaceMathSpanSource, toMathObjectJson, updateImageBlock, updateListItemValue, updateRawBlockValue, updateTableCell, updateTableColumns, updateTextBlockValue };
package/dist/document.js CHANGED
@@ -52,6 +52,7 @@ __export(document_exports, {
52
52
  renderMathNodeLatex: () => renderMathNodeLatex,
53
53
  replaceMathSpanFromEditor: () => replaceMathSpanFromEditor,
54
54
  replaceMathSpanSource: () => replaceMathSpanSource,
55
+ toMathObjectJson: () => toMathObjectJson,
55
56
  updateImageBlock: () => updateImageBlock,
56
57
  updateListItemValue: () => updateListItemValue,
57
58
  updateRawBlockValue: () => updateRawBlockValue,
@@ -409,6 +410,66 @@ function fromMathObjectJson(json, mode = "english") {
409
410
  closing: json.closing
410
411
  };
411
412
  }
413
+ function scriptsToJson(node, path) {
414
+ return {
415
+ ...node.superscript ? { superscript: chainToJson(node.superscript, `${path}.superscript`) } : {},
416
+ ...node.subscript ? { subscript: chainToJson(node.subscript, `${path}.subscript`) } : {}
417
+ };
418
+ }
419
+ function nodeToJson(node, path) {
420
+ const scripts = scriptsToJson(node, path);
421
+ if (node instanceof CharNode || node instanceof NumberNode || node instanceof OperatorNode) {
422
+ return { node_type: node.nodeType, expr: node.expr, ...scripts };
423
+ }
424
+ if (node instanceof DelimiterNode) {
425
+ return {
426
+ node_type: "DelimiterObject",
427
+ left_delim_expr: node.leftDelimExpr,
428
+ inner_expr: chainToJson(node.innerExpr, `${path}.inner_expr`),
429
+ right_delim_expr: node.rightDelimExpr,
430
+ ...scripts
431
+ };
432
+ }
433
+ if (node instanceof CommandNode) {
434
+ return {
435
+ node_type: "CommandObject",
436
+ name: node.name,
437
+ optional_args: node.optionalArgs.map((arg, index) => chainToJson(arg, `${path}.optional_args[${String(index)}]`)),
438
+ mandatory_args: node.mandatoryArgs.map((arg, index) => chainToJson(arg, `${path}.mandatory_args[${String(index)}]`)),
439
+ ...scripts
440
+ };
441
+ }
442
+ if (node instanceof EnvNode) {
443
+ return {
444
+ node_type: "EnvObject",
445
+ opening: node.opening,
446
+ lines: node.lines.map((line, index) => chainToJson(line, `${path}.lines[${String(index)}]`)),
447
+ closing: node.closing,
448
+ ...scripts
449
+ };
450
+ }
451
+ throw new Error(`Unsupported runtime math node at ${path}: ${node.nodeType}`);
452
+ }
453
+ function chainToJson(chain, path) {
454
+ if (!(chain instanceof ChainNode)) {
455
+ throw new Error(`Expected live ChainNode at ${path}`);
456
+ }
457
+ return {
458
+ node_type: "ChainClass",
459
+ chain: chain.chain.map((node, index) => nodeToJson(node, `${path}.chain[${String(index)}]`))
460
+ };
461
+ }
462
+ function toMathObjectJson(math) {
463
+ if (math.nodeType !== "MathObject" || !Array.isArray(math.lines) || math.lines.length === 0) {
464
+ throw new Error("Cannot serialize invalid runtime MathObject");
465
+ }
466
+ return {
467
+ node_type: "MathObject",
468
+ math_mode: math.mathMode,
469
+ lines: math.lines.map((line, index) => chainToJson(line, `$.lines[${String(index)}]`)),
470
+ closing: math.closing
471
+ };
472
+ }
412
473
  function renderMathNodeLatex(math) {
413
474
  const lines = math.lines.map((line) => line.arabicLatex());
414
475
  if (math.mathMode === "$" || math.mathMode === "\\(") {
@@ -3319,6 +3380,7 @@ function documentInlineText(inlines) {
3319
3380
  renderMathNodeLatex,
3320
3381
  replaceMathSpanFromEditor,
3321
3382
  replaceMathSpanSource,
3383
+ toMathObjectJson,
3322
3384
  updateImageBlock,
3323
3385
  updateListItemValue,
3324
3386
  updateRawBlockValue,