@sobree/core 0.1.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/__vite-browser-external-DYxpcVy9.js +5 -0
  4. package/dist/__vite-browser-external-DYxpcVy9.js.map +1 -0
  5. package/dist/blob/cache.d.ts +69 -0
  6. package/dist/blob/fetch.d.ts +18 -0
  7. package/dist/blob/hash.d.ts +13 -0
  8. package/dist/blob/index.d.ts +33 -0
  9. package/dist/blob/memory.d.ts +2 -0
  10. package/dist/blob/types.d.ts +80 -0
  11. package/dist/createSobree.d.ts +132 -0
  12. package/dist/doc/api.d.ts +132 -0
  13. package/dist/doc/builders.d.ts +42 -0
  14. package/dist/doc/pageSetupBridge.d.ts +26 -0
  15. package/dist/doc/parts.d.ts +18 -0
  16. package/dist/doc/runs.d.ts +47 -0
  17. package/dist/doc/styles.d.ts +19 -0
  18. package/dist/doc/types.d.ts +800 -0
  19. package/dist/doc/walk.d.ts +30 -0
  20. package/dist/docx/export/contentTypes.d.ts +35 -0
  21. package/dist/docx/export/context.d.ts +59 -0
  22. package/dist/docx/export/document.d.ts +19 -0
  23. package/dist/docx/export/drawings.d.ts +10 -0
  24. package/dist/docx/export/headers.d.ts +19 -0
  25. package/dist/docx/export/index.d.ts +14 -0
  26. package/dist/docx/export/runs.d.ts +8 -0
  27. package/dist/docx/export/styles.d.ts +8 -0
  28. package/dist/docx/export/zip.d.ts +13 -0
  29. package/dist/docx/import/anchoredFrames.d.ts +34 -0
  30. package/dist/docx/import/comments.d.ts +3 -0
  31. package/dist/docx/import/document.d.ts +57 -0
  32. package/dist/docx/import/flowFrames.d.ts +11 -0
  33. package/dist/docx/import/footnotes.d.ts +3 -0
  34. package/dist/docx/import/headers.d.ts +50 -0
  35. package/dist/docx/import/index.d.ts +12 -0
  36. package/dist/docx/import/inlineFrames.d.ts +62 -0
  37. package/dist/docx/import/numbering.d.ts +2 -0
  38. package/dist/docx/import/paragraph.d.ts +24 -0
  39. package/dist/docx/import/paragraphs.d.ts +27 -0
  40. package/dist/docx/import/rels.d.ts +5 -0
  41. package/dist/docx/import/runs.d.ts +64 -0
  42. package/dist/docx/import/settings.d.ts +48 -0
  43. package/dist/docx/import/styles.d.ts +3 -0
  44. package/dist/docx/import/tables.d.ts +12 -0
  45. package/dist/docx/import/unzip.d.ts +13 -0
  46. package/dist/docx/shared/namespaces.d.ts +31 -0
  47. package/dist/docx/shared/pageSize.d.ts +27 -0
  48. package/dist/docx/shared/shading.d.ts +2 -0
  49. package/dist/docx/shared/units.d.ts +35 -0
  50. package/dist/docx/shared/xml.d.ts +29 -0
  51. package/dist/docx/types.d.ts +98 -0
  52. package/dist/editor/index.d.ts +1078 -0
  53. package/dist/editor/internal/blockRegistry.d.ts +91 -0
  54. package/dist/editor/internal/mutations.d.ts +63 -0
  55. package/dist/editor/internal/positionMap.d.ts +35 -0
  56. package/dist/editor/table.d.ts +96 -0
  57. package/dist/editor/view/docRenderer/anchorLayer.d.ts +26 -0
  58. package/dist/editor/view/docRenderer/block.d.ts +13 -0
  59. package/dist/editor/view/docRenderer/fontFallback.d.ts +28 -0
  60. package/dist/editor/view/docRenderer/index.d.ts +18 -0
  61. package/dist/editor/view/docRenderer/inline.d.ts +15 -0
  62. package/dist/editor/view/docRenderer/inlineFrame.d.ts +4 -0
  63. package/dist/editor/view/docRenderer/lists.d.ts +28 -0
  64. package/dist/editor/view/docRenderer/paragraph.d.ts +2 -0
  65. package/dist/editor/view/docRenderer/properties.d.ts +2 -0
  66. package/dist/editor/view/docRenderer/table.d.ts +15 -0
  67. package/dist/editor/view/docRenderer/units.d.ts +48 -0
  68. package/dist/editor/view/docSerialize/block.d.ts +14 -0
  69. package/dist/editor/view/docSerialize/index.d.ts +8 -0
  70. package/dist/editor/view/docSerialize/inline.d.ts +11 -0
  71. package/dist/editor/view/docSerialize/table.d.ts +12 -0
  72. package/dist/editor/view/imageResize.d.ts +16 -0
  73. package/dist/embed/floatingCorner.d.ts +44 -0
  74. package/dist/embed/viewport.d.ts +133 -0
  75. package/dist/fonts/embedAPI.d.ts +33 -0
  76. package/dist/fonts/emit.d.ts +24 -0
  77. package/dist/fonts/fontFaceRegistry.d.ts +20 -0
  78. package/dist/fonts/fsType.d.ts +36 -0
  79. package/dist/fonts/index.d.ts +19 -0
  80. package/dist/fonts/liveness.d.ts +2 -0
  81. package/dist/fonts/odttf.d.ts +33 -0
  82. package/dist/fonts/parse.d.ts +29 -0
  83. package/dist/fonts/types.d.ts +52 -0
  84. package/dist/headless.d.ts +168 -0
  85. package/dist/history/history.d.ts +100 -0
  86. package/dist/history/index.d.ts +4 -0
  87. package/dist/history/types.d.ts +54 -0
  88. package/dist/index.css +1 -0
  89. package/dist/index.d.ts +52 -0
  90. package/dist/index.js +10561 -0
  91. package/dist/index.js.map +1 -0
  92. package/dist/markdown/parse.d.ts +6 -0
  93. package/dist/pagination/cost.d.ts +32 -0
  94. package/dist/pagination/index.d.ts +2 -0
  95. package/dist/pagination/paginate.d.ts +10 -0
  96. package/dist/pagination/postConditions.d.ts +10 -0
  97. package/dist/pagination/types.d.ts +94 -0
  98. package/dist/paperStack/pageSetup.d.ts +42 -0
  99. package/dist/paperStack/paginationAdapter/buildItems.d.ts +19 -0
  100. package/dist/paperStack/paginationAdapter/distribute.d.ts +23 -0
  101. package/dist/paperStack/paginationAdapter/index.d.ts +18 -0
  102. package/dist/paperStack/paginationAdapter/paragraphLines.d.ts +23 -0
  103. package/dist/paperStack/paginationAdapter/splitList.d.ts +19 -0
  104. package/dist/paperStack/paginationAdapter/splitParagraph.d.ts +21 -0
  105. package/dist/paperStack/paginationAdapter/types.d.ts +30 -0
  106. package/dist/paperStack/paper.d.ts +107 -0
  107. package/dist/paperStack/paperStack.d.ts +245 -0
  108. package/dist/plugin.d.ts +24 -0
  109. package/dist/plugins/marks.d.ts +49 -0
  110. package/dist/plugins/sections.d.ts +15 -0
  111. package/dist/presence/attach.d.ts +48 -0
  112. package/dist/presence/awareness.d.ts +28 -0
  113. package/dist/presence/index.d.ts +19 -0
  114. package/dist/presence/overlay.d.ts +28 -0
  115. package/dist/presence/state.d.ts +36 -0
  116. package/dist/sobree.d.ts +211 -0
  117. package/dist/tokens.css +144 -0
  118. package/dist/util/selection.d.ts +13 -0
  119. package/dist/ydoc/apply.d.ts +68 -0
  120. package/dist/ydoc/index.d.ts +18 -0
  121. package/dist/ydoc/project.d.ts +41 -0
  122. package/dist/ydoc/runs.d.ts +51 -0
  123. package/dist/ydoc/schema.d.ts +123 -0
  124. package/dist/ydoc/seed.d.ts +45 -0
  125. package/dist/ydoc/textDiff.d.ts +59 -0
  126. package/dist/zoneEdit/index.d.ts +22 -0
  127. package/package.json +61 -0
@@ -0,0 +1,91 @@
1
+ import { BlockRef } from '../../doc/api';
2
+ /**
3
+ * Per-block identity + version tracking.
4
+ *
5
+ * The Editor owns one BlockRegistry for its lifetime. Every block in the
6
+ * current body has a stable `id` (sequential strings like `"b1"`, `"b2"`)
7
+ * and a `version` that bumps on every modification. Indices shift as
8
+ * blocks are inserted / removed, but `id` doesn't.
9
+ *
10
+ * The whole registry is reset on `setDocument` / `openDocx`. It is NOT
11
+ * persisted — versions are a runtime-only contract.
12
+ *
13
+ * Pure data + arithmetic; no DOM access.
14
+ */
15
+ export interface BlockRegistryOptions {
16
+ /**
17
+ * Prefix for newly-allocated ids. Default `"b"` produces `b1`, `b2`,
18
+ * … — fine for single-peer use. Phase 1b+ embedders pass a
19
+ * peer-unique prefix (e.g. `${ydoc.clientID.toString(36)}_`) so two
20
+ * peers don't both mint `b5` for different blocks.
21
+ */
22
+ idPrefix?: string;
23
+ }
24
+ export declare class BlockRegistry {
25
+ /** Per-block state, parallel to the body array. */
26
+ private entries;
27
+ /** Fast lookup by id. Points at the same objects as `entries`. */
28
+ private byId;
29
+ /** Next numeric suffix to allocate. Monotonic across `reset()` calls
30
+ * so an id minted before a reset never collides with one minted
31
+ * after. */
32
+ private nextNum;
33
+ /** String prepended to every newly-minted id. Constant for the
34
+ * lifetime of the registry. */
35
+ private readonly idPrefix;
36
+ /** Document-wide monotonic counter — bumps on any modification. */
37
+ private docVersion;
38
+ constructor(opts?: BlockRegistryOptions);
39
+ /** Replace the registry for a fresh document of `blockCount` blocks. */
40
+ reset(blockCount: number): void;
41
+ /**
42
+ * Replace the registry from an explicit id list — used when adopting
43
+ * an existing Y.Doc's body (Phase 1b: peer joining an active room).
44
+ * The supplied ids are kept verbatim; future inserts continue to use
45
+ * the registry's own `idPrefix` so peer-minted ids stay distinct from
46
+ * adopted-foreign ids.
47
+ *
48
+ * `nextNum` is advanced past any local-prefix collisions found in
49
+ * the adopted set, so a future local insert can't shadow an adopted
50
+ * id.
51
+ */
52
+ adoptIds(ids: readonly string[]): void;
53
+ /** Number of blocks currently tracked. */
54
+ length(): number;
55
+ /** Current document counter (monotonic across all edits). */
56
+ documentVersion(): number;
57
+ /** Ref for the block at `index`. Throws on out-of-range. */
58
+ refAt(index: number): BlockRef;
59
+ /** Ref by id, or `null` if the id isn't live. */
60
+ refById(id: string): BlockRef | null;
61
+ /** Current body-index of the given id, or `-1` if not found. */
62
+ indexOf(id: string): number;
63
+ /** Whether `id` is currently live (not deleted). */
64
+ has(id: string): boolean;
65
+ /**
66
+ * Bump the version of the block at `index`. Bumps doc version too.
67
+ * Returns the new ref for the caller to pass back in results.
68
+ */
69
+ bump(index: number): BlockRef;
70
+ /**
71
+ * Insert a fresh entry at `index`, shifting subsequent entries right.
72
+ * The new entry starts at version 0. Bumps doc version.
73
+ */
74
+ insert(index: number): BlockRef;
75
+ /**
76
+ * Remove the entry at `index`, shifting subsequent entries left.
77
+ * Bumps doc version.
78
+ */
79
+ remove(index: number): void;
80
+ /**
81
+ * Replace everything at once — used by `syncFromDom` when the rebuild
82
+ * preserves identity but content may have changed per-block. Pass a
83
+ * parallel array of "did this block change" booleans; true entries
84
+ * bump; the registry itself doesn't know what changed.
85
+ *
86
+ * Callers that know nothing about diffs should call `.reset(n)` instead.
87
+ */
88
+ bumpChanged(changed: readonly boolean[]): void;
89
+ private allocEntry;
90
+ private newEntry;
91
+ }
@@ -0,0 +1,63 @@
1
+ import { Block, ParagraphProperties, RunProperties, SectionProperties, SobreeDocument } from '../../doc/types';
2
+ import { ParagraphPropertiesPatch, WrapTag } from '../index';
3
+ import { RunPropertiesPatch } from '../../doc/runs';
4
+ /**
5
+ * One registry-level operation produced by a mutation. The caller
6
+ * applies these to the BlockRegistry after committing the new doc:
7
+ * `insert` adds an id, `remove` drops one, `bump` keeps the same id
8
+ * but increments its version.
9
+ */
10
+ export type Mutation = {
11
+ type: "bump";
12
+ index: number;
13
+ } | {
14
+ type: "insert";
15
+ index: number;
16
+ } | {
17
+ type: "remove";
18
+ index: number;
19
+ };
20
+ /**
21
+ * Index in `sections` of the section that ENDS at the section_break at
22
+ * `breakIndex`. Sections are 1:1 with section_breaks; the first
23
+ * section ends at the first break (or at the end of `body` if there's
24
+ * no break).
25
+ *
26
+ * body = [p, p, break, p, break, p]
27
+ * sections = [s0, s1, s2]
28
+ *
29
+ * breakIndex = 2 → 0 (the first break ends section 0)
30
+ * breakIndex = 4 → 1 (the second break ends section 1)
31
+ */
32
+ export declare function removedSectionIndex(body: readonly Block[], breakIndex: number): number;
33
+ /**
34
+ * Drop the section at `endingIndex + 1` from `sections` — that's the
35
+ * section the now-removed break STARTED. The section ENDED by the
36
+ * removed break (at `endingIndex`) absorbs whatever content used to
37
+ * belong to its successor. Properties of the surviving section are
38
+ * preserved verbatim; nothing about the removed section's settings is
39
+ * carried over.
40
+ *
41
+ * If `sections` doesn't have a successor (the removed break was the
42
+ * last one and there's only one section), the array is returned
43
+ * unchanged.
44
+ */
45
+ export declare function mergeSectionsAcross(sections: readonly SectionProperties[], endingIndex: number): SectionProperties[];
46
+ /**
47
+ * Merge a `ParagraphPropertiesPatch` into existing properties.
48
+ * `undefined` in the patch removes a field; everything else
49
+ * overwrites.
50
+ */
51
+ export declare function mergeParagraphProps(prev: ParagraphProperties, patch: ParagraphPropertiesPatch): ParagraphProperties;
52
+ /**
53
+ * Map a semantic "wrap" tag to the run-property patch that achieves it.
54
+ * Same mapping the browser editor uses for toolbar buttons.
55
+ */
56
+ export declare function wrapTagToPatch(tag: WrapTag): RunPropertiesPatch;
57
+ /** Map an image MIME type to a `.docx` part filename extension. */
58
+ export declare function mimeToExtension(mime: string): string;
59
+ /** Find the next free `word/media/imageN.<ext>` slot in `rawParts`. */
60
+ export declare function allocateMediaPath(doc: SobreeDocument, ext: string): string;
61
+ /** Convert pixels (CSS @ 96 dpi) to OOXML's EMU (914400 per inch). */
62
+ export declare function pxToEmu(px: number): number;
63
+ export type { RunProperties };
@@ -0,0 +1,35 @@
1
+ import { InlinePosition, Range as ApiRange, Selection } from '../../doc/api';
2
+ import { BlockRegistry } from './blockRegistry';
3
+ /**
4
+ * Resolve a DOM point `(node, offset)` to an `InlinePosition`, or null
5
+ * if the point is outside the editor's tracked blocks.
6
+ *
7
+ * For table cells, returns an InlinePosition at the table block with
8
+ * `offset = 0` — block-internal table addressing is a future extension.
9
+ */
10
+ export declare function positionFromDomPoint(hosts: readonly HTMLElement[], registry: BlockRegistry, node: Node, domOffset: number): InlinePosition | null;
11
+ /** Build an API `Range` from a live DOM `Range`. */
12
+ export declare function rangeFromDomRange(hosts: readonly HTMLElement[], registry: BlockRegistry, range: Range): ApiRange | null;
13
+ /** Read `window.getSelection()` as a model `Selection`. */
14
+ export declare function selectionFromDom(hosts: readonly HTMLElement[], registry: BlockRegistry): Selection;
15
+ /** Resolve an `InlinePosition` to a DOM `{ node, offset }` point. */
16
+ export declare function domPointFromPosition(hosts: readonly HTMLElement[], registry: BlockRegistry, pos: InlinePosition): {
17
+ node: Node;
18
+ offset: number;
19
+ } | null;
20
+ /** Apply a model `Selection` to `window.getSelection()`. */
21
+ export declare function applySelectionToDom(hosts: readonly HTMLElement[], registry: BlockRegistry, selection: Selection): boolean;
22
+ /** Total character-count length of a block, per the counting rules above. */
23
+ export declare function blockLength(blockEl: Element): number;
24
+ /**
25
+ * Enumerate blocks across all content hosts in document order,
26
+ * expanding `<ul>`/`<ol>` children as one block each. Returns the
27
+ * total block count.
28
+ */
29
+ export declare function countBlocks(hosts: readonly HTMLElement[]): number;
30
+ /**
31
+ * Return the DOM element that hosts a given block index. For list-item
32
+ * blocks this is the `<li>`; for everything else it's the direct host
33
+ * child.
34
+ */
35
+ export declare function blockElementAtIndex(hosts: readonly HTMLElement[], index: number): HTMLElement | null;
@@ -0,0 +1,96 @@
1
+ import { BlockRef, EditResult } from '../doc/api';
2
+ import { SobreeDocument, Block, ParagraphAlignment, Table, TableCell, TableProperties } from '../doc/types';
3
+ /**
4
+ * Minimal slice of `Editor` that `EditorTable` actually needs. Defining
5
+ * it here (rather than `import type { Editor } from "./"`) keeps this
6
+ * module a leaf in the editor/* import graph — no cycle with index.ts.
7
+ */
8
+ export interface EditorTableHost {
9
+ getDocument(): SobreeDocument;
10
+ getBlockById(id: string): {
11
+ kind: string;
12
+ index: number;
13
+ } | null;
14
+ replaceBlock(target: BlockRef, block: Block): EditResult<BlockRef>;
15
+ }
16
+ /**
17
+ * Pointer to one cell inside a table. `row`/`col` are **visual** indices
18
+ * (after expanding `gridSpan`) — the grid the user actually sees. Not a
19
+ * stable handle: indices shift as rows/columns are added or removed.
20
+ */
21
+ export interface CellRef {
22
+ table: BlockRef;
23
+ row: number;
24
+ col: number;
25
+ }
26
+ /** Where an insertion should land, relative to an anchor row/column. */
27
+ export type InsertAt = "start" | "end" | "before" | "after";
28
+ export interface InsertRowOpts {
29
+ at: InsertAt;
30
+ /** Required for `"before"` / `"after"`. Visual row index of the anchor. */
31
+ index?: number;
32
+ /** Custom cells. Defaults to empty paragraphs, one per grid column. */
33
+ cells?: TableCell[];
34
+ }
35
+ export interface InsertColumnOpts {
36
+ at: InsertAt;
37
+ /** Required for `"before"` / `"after"`. Visual column index of the anchor. */
38
+ index?: number;
39
+ /** Width of the new column in twips. Default: average of existing columns, or 2400. */
40
+ widthTwips?: number;
41
+ /**
42
+ * When the target column falls inside an existing `gridSpan` cell, the
43
+ * default is to **extend** the span (the existing merge grows). Pass
44
+ * `split: true` to split the merge and insert a fresh cell instead.
45
+ */
46
+ split?: boolean;
47
+ }
48
+ export interface MergeCellsOpts {
49
+ /** Visual coordinates of the merge region's top-left corner. */
50
+ row: number;
51
+ col: number;
52
+ /** Number of visual rows the merge should span. Defaults to 1. */
53
+ rowSpan?: number;
54
+ /** Number of visual columns the merge should span. Defaults to 1. */
55
+ colSpan?: number;
56
+ }
57
+ /**
58
+ * Ergonomic table mutation surface. Lives on `editor.table`.
59
+ *
60
+ * Every method does the same three steps under the hood:
61
+ * 1. Resolve the target table by `BlockRef` (inherits optimistic-lock
62
+ * checking from `editor.replaceBlock`).
63
+ * 2. Clone and mutate the table immutably.
64
+ * 3. Delegate to `editor.replaceBlock(ref, nextTable)`.
65
+ *
66
+ * No new plumbing; lock semantics, affected-block tracking, and event
67
+ * emission come from the underlying core.
68
+ */
69
+ export declare class EditorTable {
70
+ private readonly editor;
71
+ constructor(editor: EditorTableHost);
72
+ insertRow(ref: BlockRef, opts: InsertRowOpts): EditResult<BlockRef>;
73
+ deleteRow(ref: BlockRef, index: number): EditResult<BlockRef>;
74
+ insertColumn(ref: BlockRef, opts: InsertColumnOpts): EditResult<BlockRef>;
75
+ deleteColumn(ref: BlockRef, index: number): EditResult<BlockRef>;
76
+ mergeCells(ref: BlockRef, opts: MergeCellsOpts): EditResult<BlockRef>;
77
+ unmergeCell(cell: CellRef): EditResult<BlockRef>;
78
+ setCellContent(cell: CellRef, content: Block[]): EditResult<BlockRef>;
79
+ setCellProperties(cell: CellRef, patch: Partial<Omit<TableCell, "content">>): EditResult<BlockRef>;
80
+ setColumnWidth(ref: BlockRef, col: number, widthTwips: number): EditResult<BlockRef>;
81
+ toggleHeaderRow(ref: BlockRef, row: number): EditResult<BlockRef>;
82
+ setProperties(ref: BlockRef, patch: Partial<TableProperties>): EditResult<BlockRef>;
83
+ private getTable;
84
+ private updateCell;
85
+ }
86
+ /**
87
+ * Given a `(row, col)` in *visual* coordinates, find which TableCell in
88
+ * `row.cells` holds that position, accounting for `gridSpan`.
89
+ */
90
+ export declare function cellAtVisual(table: Table, row: number, col: number): {
91
+ cellIndex: number;
92
+ cell: TableCell;
93
+ startCol: number;
94
+ } | null;
95
+ /** Used internally by alignment setters in the toolbar. */
96
+ export type TableCellAlignment = ParagraphAlignment;
@@ -0,0 +1,26 @@
1
+ import { AnchoredFrame, Block } from '../../../doc/types';
2
+ export interface AnchorLayerContext {
3
+ /** Map ZIP-path → bytes, used to mint blob URLs for picture content. */
4
+ rawParts: Record<string, Uint8Array>;
5
+ /**
6
+ * Reuse picture URLs across calls so the same image isn't re-blobbed
7
+ * every render. The caller (PaperStack) owns the map.
8
+ */
9
+ pictureUrlCache: Map<string, string>;
10
+ /**
11
+ * Render a textbox's `Block[]` body into a host element. Injected by
12
+ * the caller (PaperStack wires in `renderBlocks`) so this module
13
+ * stays decoupled from the heavy block renderer — anchorLayer only
14
+ * knows the AnchoredFrame model + DOM, not the full paragraph/list/
15
+ * table pipeline. When absent, textbox bodies render as plain
16
+ * stacked text (test/headless fallback).
17
+ */
18
+ renderBody?: (blocks: Block[], host: HTMLElement) => void;
19
+ }
20
+ /**
21
+ * Build the per-page anchor layer. Returns a single `<div>` whose
22
+ * children are the frames — render order = z-stack order (later
23
+ * siblings paint on top). The wrapper itself is `position: absolute`
24
+ * inset:0 inside the paper-content area.
25
+ */
26
+ export declare function renderAnchorLayer(frames: readonly AnchoredFrame[], ctx: AnchorLayerContext): HTMLElement;
@@ -0,0 +1,13 @@
1
+ import { Block, NamedStyle, NumberingDefinition, SectionProperties } from '../../../doc/types';
2
+ /**
3
+ * Render a `Block[]` stream into `host`, grouping consecutive paragraphs
4
+ * that share a `numId` into a single `<ul>`/`<ol>` so the browser renders
5
+ * proper list markers.
6
+ *
7
+ * `numbering` maps `numId` → definition so we can decide ordered vs
8
+ * bulleted. Unknown numIds fall back to `<ul>`.
9
+ *
10
+ * `rawParts` is threaded to image rendering so `<img src>` can be
11
+ * populated from embedded bytes via a blob URL.
12
+ */
13
+ export declare function renderBlocks(blocks: readonly Block[], host: HTMLElement, numbering: readonly NumberingDefinition[], styles?: readonly NamedStyle[], rawParts?: Record<string, Uint8Array>, blockIds?: readonly string[], sections?: readonly SectionProperties[]): void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Build a CSS `font-family` value with metric-compatible fallbacks.
3
+ *
4
+ * Why: when a docx specifies `font-family: "Bookman Old Style"` and
5
+ * the host machine doesn't have it installed, the browser picks an
6
+ * arbitrary system serif. The fallback may have a much taller natural
7
+ * ascender+descender than the requested font — and CSS's line-box
8
+ * height = `max(line-height, font-strut-height)`, so the line box
9
+ * inflates beyond the declared `line-height`. That cumulative inflation
10
+ * shifts pagination decisions (a single docx ends up taking more pages
11
+ * than Word produces).
12
+ *
13
+ * The fix: emit a fallback chain whose members have metrics close to
14
+ * the primary font. Georgia is metrically near Bookman; Helvetica Neue
15
+ * near Calibri; etc. The chain ends in a generic family so we never
16
+ * fall off the cliff into the browser's last-resort serif/sans pick.
17
+ *
18
+ * Diagnosis trail: per-block height inflation surfaced via
19
+ * `pnpm fixtures:compare --pages` on user-contract.docx (Bookman not
20
+ * installed on macOS by default). See [diagnosis notes].
21
+ */
22
+ /**
23
+ * Wrap `fontFamily` in quotes if it contains spaces or unusual
24
+ * characters, and append a metric-compatible fallback chain. If the
25
+ * font already matches a curated entry, use that chain directly so the
26
+ * primary font isn't double-listed.
27
+ */
28
+ export declare function withFallbacks(fontFamily: string): string;
@@ -0,0 +1,18 @@
1
+ import { SobreeDocument } from '../../../doc/types';
2
+ /**
3
+ * Walk a SobreeDocument's body into `host`, replacing its existing
4
+ * children. List grouping, heading style mapping, and image resolution
5
+ * live downstream in `block.ts` / `inline.ts`.
6
+ *
7
+ * Footnotes (when present) render as an `<aside class="sobree-footnotes">`
8
+ * appended *after* the body content. True per-page pinning of footnotes
9
+ * is a paginator feature deferred until a fixture demands it; for now
10
+ * the references in body text link to footnote bodies at the doc end.
11
+ *
12
+ * Comments are NOT rendered here — core only emits the neutral inline
13
+ * `<span class="sobree-comment-range">` marks (in `inline.ts`). The
14
+ * `@sobree/review` plugin reads those marks + `doc.comments` to render
15
+ * the comment cards. Without the plugin, commented text is still
16
+ * highlighted; there's just no card surface.
17
+ */
18
+ export declare function renderSobreeDocument(doc: SobreeDocument, host: HTMLElement, blockIds?: readonly string[]): void;
@@ -0,0 +1,15 @@
1
+ import { InlineRun } from '../../../doc/types';
2
+ /**
3
+ * Render a list of InlineRuns into DOM children of `parent`. Empty run
4
+ * lists produce a `<br>` placeholder so contenteditable can place a
5
+ * caret in the paragraph.
6
+ *
7
+ * `rawParts` is threaded through so `DrawingRun` can resolve its
8
+ * `partPath` to an `<img src>` via a blob URL / data URI.
9
+ */
10
+ export declare function appendInlineRuns(parent: HTMLElement, runs: readonly InlineRun[], rawParts?: Record<string, Uint8Array>): void;
11
+ /** Convert a raw part's bytes (from `doc.rawParts`) into a blob URL
12
+ * the browser can render as `<img src>` / `background-image`. Exported
13
+ * for the section-frame renderer in `block.ts` which paints the
14
+ * `framePictures` background outside the inline-run code path. */
15
+ export declare function partPathToUrl(partPath: string, rawParts: Record<string, Uint8Array>): string | null;
@@ -0,0 +1,4 @@
1
+ import { Block, InlineFrame, NamedStyle, NumberingDefinition } from '../../../doc/types';
2
+ /** The recursive block renderer, injected to break the import cycle. */
3
+ export type RenderBody = (blocks: readonly Block[], host: HTMLElement, numbering: readonly NumberingDefinition[], styles: readonly NamedStyle[], rawParts: Record<string, Uint8Array>) => void;
4
+ export declare function renderInlineFrameBlock(frame: InlineFrame, numbering: readonly NumberingDefinition[], styles: readonly NamedStyle[], rawParts: Record<string, Uint8Array>, renderBody: RenderBody): HTMLElement;
@@ -0,0 +1,28 @@
1
+ import { Block, NumberingDefinition } from '../../../doc/types';
2
+ export interface ListInfo {
3
+ numId: number;
4
+ ordered: boolean;
5
+ /** Effective left indent (text wrap position) for this list level,
6
+ * in twips — from the numbering definition's `<w:lvl><w:pPr><w:ind>`.
7
+ * Applied as `padding-left` on the OL / UL so wrapped text lands at
8
+ * the right position and the marker hangs to its left. */
9
+ leftTwips?: number;
10
+ /** Twips the FIRST line hangs to the left of `leftTwips` (= `@w:hanging`).
11
+ * The marker sits at `(leftTwips - hangingTwips)` from the content edge. */
12
+ hangingTwips?: number;
13
+ /** The level's lvlText glyph (post-Wingdings remapping), for bullets. */
14
+ bulletGlyph?: string;
15
+ }
16
+ /**
17
+ * Resolve a paragraph's list membership from the numbering table.
18
+ * Returns `null` for non-list paragraphs (no `numbering` property or
19
+ * a `numId` not present in `numbering`).
20
+ */
21
+ export declare function paragraphListInfo(block: Block, numbering: readonly NumberingDefinition[]): ListInfo | null;
22
+ /**
23
+ * Build the `<ol>` / `<ul>` container for a run of list items sharing
24
+ * a `numId`. Sets marker geometry (padding-left + hanging custom
25
+ * property) and bullet glyph (native CSS keyword where one exists,
26
+ * else a `::marker`-content custom property).
27
+ */
28
+ export declare function createListContainer(info: ListInfo, sectionIndex: number): HTMLElement;
@@ -0,0 +1,2 @@
1
+ import { NamedStyle, Paragraph } from '../../../doc/types';
2
+ export declare function renderParagraph(p: Paragraph, styles: readonly NamedStyle[], rawParts: Record<string, Uint8Array>): HTMLElement;
@@ -0,0 +1,2 @@
1
+ import { NamedStyle, ParagraphProperties } from '../../../doc/types';
2
+ export declare function applyParagraphProps(el: HTMLElement, props: ParagraphProperties, styles?: readonly NamedStyle[]): void;
@@ -0,0 +1,15 @@
1
+ import { NamedStyle, NumberingDefinition, Table } from '../../../doc/types';
2
+ /**
3
+ * Render a Table to a `<table>` element. `vMerge: "restart"` cells
4
+ * emit with `rowspan=N` computed from the following `"continue"` cells
5
+ * in the same column. `"continue"` cells render nothing — their column
6
+ * is covered by the spanned restart cell above.
7
+ *
8
+ * `styles` + `numbering` thread through so each cell's paragraphs go
9
+ * through the full cascade (line-height, spacing, alignment, font from
10
+ * style). Without this, table-cell paragraphs would render flat
11
+ * (visible bug: BodyText-styled paragraphs inside cells lost their
12
+ * 1.5× line-height because the cell's own renderer ignored per-paragraph
13
+ * properties).
14
+ */
15
+ export declare function renderTable(table: Table, numbering?: readonly NumberingDefinition[], styles?: readonly NamedStyle[], rawParts?: Record<string, Uint8Array>): HTMLElement;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * OOXML measurement units → CSS conversions, in one place.
3
+ *
4
+ * OOXML mixes three length units:
5
+ * - EMU (English Metric Units): 914400 per inch. Used by DrawingML
6
+ * (`<a:off>`, `<a:ext>`, `<wp:extent>`, picture sizes).
7
+ * - twips (twentieths of a point): 1440 per inch. Used by
8
+ * WordprocessingML (`<w:ind>`, `<w:spacing>`, `<w:tblW>`).
9
+ * - half-points / eighths-of-point: font + border sizes (handled
10
+ * at their own call sites; not length conversions).
11
+ *
12
+ * Derived constants (all exact):
13
+ * - 1 inch = 25.4 mm = 96 px (CSS reference pixel) = 914400 EMU = 1440 twips
14
+ * - 914400 / 25.4 = 36000 EMU per mm
15
+ * - 914400 / 96 = 9525 EMU per px
16
+ *
17
+ * Rounding is a SEPARATE concern from conversion. These helpers return
18
+ * exact values; call sites that want integer mm / px (e.g. for crisp
19
+ * image edges, or to match a historical snapshot) round explicitly.
20
+ */
21
+ export declare const EMU_PER_INCH = 914400;
22
+ export declare const TWIPS_PER_INCH = 1440;
23
+ export declare const MM_PER_INCH = 25.4;
24
+ export declare const PX_PER_INCH = 96;
25
+ /** 914400 / 25.4 = 36000, pinned as an integer literal. Computing it
26
+ * as `914400 / 25.4` risks float drift (25.4 isn't exactly
27
+ * representable), which would change emitted CSS strings vs the
28
+ * literal `36000` the renderer used before this module existed. */
29
+ export declare const EMU_PER_MM = 36000;
30
+ /** 914400 / 96 = 9525, exact. */
31
+ export declare const EMU_PER_PX = 9525;
32
+ /** EMU → millimetres, exact. */
33
+ export declare function emuToMm(emu: number): number;
34
+ /** EMU → CSS pixels, exact. */
35
+ export declare function emuToPx(emu: number): number;
36
+ /**
37
+ * Twips → millimetres, ROUNDED to the nearest whole millimetre.
38
+ *
39
+ * Word's body-geometry values (indents, spacing, column gaps) are
40
+ * authored in whole points / mm and survive a round-trip as twips;
41
+ * rounding to integer mm here keeps the emitted CSS stable (`5mm`,
42
+ * not `4.97mm`) and matches the renderer's long-standing output that
43
+ * the oracle snapshots are blessed against. Callers needing sub-mm
44
+ * precision should use `twipsToMmExact`.
45
+ */
46
+ export declare function twipsToMm(twips: number): number;
47
+ /** Twips → millimetres, exact (no rounding). */
48
+ export declare function twipsToMmExact(twips: number): number;
@@ -0,0 +1,14 @@
1
+ import { Block, NumberingDefinition } from '../../../doc/types';
2
+ export interface BlockSerializeContext {
3
+ /** Accumulated numbering definitions, one per encountered list. */
4
+ numbering: NumberingDefinition[];
5
+ /**
6
+ * Allocated numId for the CURRENT list stream. Reset to `null` by the
7
+ * caller between lists.
8
+ */
9
+ currentList: {
10
+ numId: number;
11
+ ordered: boolean;
12
+ } | null;
13
+ }
14
+ export declare function blocksFromNodes(nodes: readonly Node[], ctx: BlockSerializeContext): Block[];
@@ -0,0 +1,8 @@
1
+ import { SobreeDocument } from '../../../doc/types';
2
+ /**
3
+ * Serialise DOM content across one or more host elements (in document
4
+ * order) into a SobreeDocument. Sections / headers / footers are NOT
5
+ * produced here — the Sobree façade injects those from its current page
6
+ * setup state before handing the document off to the exporter.
7
+ */
8
+ export declare function serializeHostsToDocument(hosts: readonly HTMLElement[]): SobreeDocument;
@@ -0,0 +1,11 @@
1
+ import { InlineRun } from '../../../doc/types';
2
+ /**
3
+ * Serialise DOM children of `el` into a flat `InlineRun[]`. Nested
4
+ * formatting wrappers (`<strong><em>...`) are flattened — each leaf text
5
+ * node yields one `TextRun` whose `RunProperties` is the union of all
6
+ * formatting seen on the path from `el` to that text node.
7
+ *
8
+ * `<a>` elements produce a `HyperlinkRun` wrapping recursively-serialised
9
+ * children (their own flat run list).
10
+ */
11
+ export declare function serializeInlineChildren(el: HTMLElement): InlineRun[];
@@ -0,0 +1,12 @@
1
+ import { Table } from '../../../doc/types';
2
+ /**
3
+ * Convert a `<table>` back into a Table AST node.
4
+ *
5
+ * The AST mirrors OOXML's row/cell model: every visual cell, including
6
+ * cells occluded by a vertical merge above them, is represented as its
7
+ * own `TableCell` (with `vMerge: "continue"` for the occluded ones and
8
+ * `vMerge: "restart"` on the cell that spans them). The DOM's
9
+ * `rowspan` attribute collapses multiple rows into a single `<td>`; we
10
+ * synthesize the continuation cells back here.
11
+ */
12
+ export declare function tableFromElement(el: HTMLElement): Table;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Click-to-select-and-drag-the-corner image resizing.
3
+ *
4
+ * Implementation:
5
+ * - Listen for clicks inside the editor host. If an `<img>` was clicked,
6
+ * mark it as selected (one image at a time) and show a single
7
+ * bottom-right corner handle absolutely positioned over its corner.
8
+ * - The handle is a sibling of the image (not a child — the image is
9
+ * replaced/edited as content), tracked in a closure so we can move
10
+ * it as the page scrolls or paginates.
11
+ * - Dragging the handle updates the image's `width`/`height` styles in
12
+ * real time, preserving aspect ratio (hold Shift to free).
13
+ * - On mouseup we dispatch an `input` event on the host so the editor's
14
+ * existing debounced change pipeline picks up the new dimensions.
15
+ */
16
+ export declare function attachImageResize(host: HTMLElement): () => void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared floating-corner stack — a flex container per (host, corner)
3
+ * that plugins append floating UIs to so multiple docks in the same
4
+ * corner stack predictably instead of overlapping.
5
+ *
6
+ * The problem this solves: `@sobree/zoom-controls`, `@sobree/review`'s
7
+ * dock, a future toast/notification plugin, etc. all want to anchor
8
+ * to a corner of the rendering area. Each appending directly to the
9
+ * host with `position: absolute` makes them collide.
10
+ *
11
+ * Each corner gets its own `<div class="sobree-floating-corner">`
12
+ * created on first use. The container's flex direction is chosen so
13
+ * the *first* item added sits hugging the corner, and subsequent
14
+ * items grow toward the centre:
15
+ *
16
+ * top-right → flex-direction: column (grows downward)
17
+ * top-left → flex-direction: column (grows downward)
18
+ * bottom-right → flex-direction: column-reverse (grows upward)
19
+ * bottom-left → flex-direction: column-reverse (grows upward)
20
+ *
21
+ * This keeps the "primary" floating UI nearest the corner regardless
22
+ * of which side it lives on, which matches user intuition for a
23
+ * docked toolbar.
24
+ *
25
+ * The host must be `position: relative` (or otherwise establish a
26
+ * containing block); the corner container uses `position: absolute`
27
+ * to pin to the edge. The function does NOT modify the host's
28
+ * positioning — that's the host owner's responsibility.
29
+ */
30
+ export type FloatingCornerPlacement = "top-left" | "top-right" | "bottom-left" | "bottom-right";
31
+ /**
32
+ * Return the flex container for `placement` inside `host`, creating
33
+ * it on first call. Subsequent calls with the same `(host, placement)`
34
+ * pair return the same element — so plugins can `appendChild` their
35
+ * floating UI and trust they'll stack with other corner-residents
36
+ * rather than overlap them.
37
+ *
38
+ * The returned element's contents are managed by the appending
39
+ * plugins; this function only owns the container itself. To remove
40
+ * a floating UI, just `.remove()` it — the container stays alive for
41
+ * other tenants. Empty containers are inexpensive (4-byte `<div>` per
42
+ * corner per host) so we don't garbage-collect them.
43
+ */
44
+ export declare function getFloatingCorner(host: HTMLElement, placement: FloatingCornerPlacement): HTMLElement;