@sobree/core 0.1.27 → 0.1.29

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.
@@ -0,0 +1,14 @@
1
+ import { BlockRef } from '../api';
2
+ import { Block } from '../types';
3
+ import { DocumentMutationResult, MutationInput } from './types';
4
+ /** Replace the block at `target`'s index with `block`. If a section_break
5
+ * is replaced by a non-break, the two sections it delimited merge (the
6
+ * earlier section's properties survive). */
7
+ export declare function replaceBlockMutation(input: MutationInput, target: BlockRef, block: Block): DocumentMutationResult<BlockRef>;
8
+ /** Insert `block` before the target block. */
9
+ export declare function insertBlockBeforeMutation(input: MutationInput, target: BlockRef, block: Block): DocumentMutationResult<BlockRef>;
10
+ /** Insert `block` after the target block. */
11
+ export declare function insertBlockAfterMutation(input: MutationInput, target: BlockRef, block: Block): DocumentMutationResult<BlockRef>;
12
+ /** Delete the target block. Deleting the only block leaves one empty
13
+ * paragraph. Deleting a section_break merges the sections it delimited. */
14
+ export declare function deleteBlockMutation(input: MutationInput, target: BlockRef): DocumentMutationResult<void>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pure document mutation engine — the single source of truth for the
3
+ * AST-level edits both Sobree peers perform.
4
+ *
5
+ * # Why this exists
6
+ *
7
+ * The browser `Editor` (DOM-backed) and `HeadlessSobree` (no-DOM peer for
8
+ * LLM agents / MCP / automation) used to implement the same block,
9
+ * paragraph, section, style, and numbering mutations independently. That
10
+ * is silent-drift risk: change one path and the other — hence MCP, hence
11
+ * every collaborating peer — keeps the old behavior, and Y.Doc parity or
12
+ * block-version semantics diverge with nothing failing loudly. These
13
+ * functions are that shared logic, called by both adapters.
14
+ *
15
+ * # The contract
16
+ *
17
+ * A mutation function is **pure**: given a {@link MutationInput} it returns
18
+ * a {@link DocumentMutationResult} — a document patch plus the
19
+ * registry-level {@link Mutation}s to apply — and nothing else.
20
+ *
21
+ * Strictly NO:
22
+ * - DOM / `HTMLElement` / selection APIs
23
+ * - `Y.Doc` / Yjs
24
+ * - renderer / paginator calls
25
+ * - command bus / plugin / editor-instance access
26
+ *
27
+ * The adapter owns commit, optimistic-lock application, Y.Doc mirroring,
28
+ * history, rendering, and events. If a mutation seems to need any of the
29
+ * above, the boundary is wrong — the engine should only know document
30
+ * structure, block references, optimistic-lock metadata, and pure AST
31
+ * transformations.
32
+ */
33
+ export * from './blocks';
34
+ export * from './numbering';
35
+ export * from './paragraphs';
36
+ export * from './sections';
37
+ export * from './styles';
38
+ export * from './types';
@@ -0,0 +1,8 @@
1
+ import { NumberingDefinition, NumberingLevel } from '../types';
2
+ import { DocumentMutationResult, MutationInput } from './types';
3
+ /** Add a new numbering definition. Fails if `def.numId` already exists. */
4
+ export declare function defineNumberingMutation(input: MutationInput, def: NumberingDefinition): DocumentMutationResult<void>;
5
+ /** Replace the levels of the definition with `numId`. Fails if missing. */
6
+ export declare function updateNumberingMutation(input: MutationInput, numId: number, levels: NumberingLevel[]): DocumentMutationResult<void>;
7
+ /** Remove the definition with `numId`. Fails if missing. */
8
+ export declare function removeNumberingMutation(input: MutationInput, numId: number): DocumentMutationResult<void>;
@@ -0,0 +1,13 @@
1
+ import { ParagraphPropertiesPatch } from '../../editor/types';
2
+ import { BlockRef } from '../api';
3
+ import { ParagraphProperties } from '../types';
4
+ import { DocumentMutationResult, MutationInput } from './types';
5
+ /**
6
+ * Merge a `ParagraphPropertiesPatch` into existing properties.
7
+ * `undefined` in the patch removes a field; everything else
8
+ * overwrites.
9
+ */
10
+ export declare function mergeParagraphProps(prev: ParagraphProperties, patch: ParagraphPropertiesPatch): ParagraphProperties;
11
+ /** Merge a patch into each target paragraph's properties. Bumps each
12
+ * paragraph's version; fails if any target is not a paragraph. */
13
+ export declare function applyBlockPropertiesMutation(input: MutationInput, targets: readonly BlockRef[], patch: ParagraphPropertiesPatch): DocumentMutationResult<void>;
@@ -0,0 +1,40 @@
1
+ import { SectionPropertiesPatch } from '../../editor/types';
2
+ import { Block, SectionProperties } from '../types';
3
+ import { DocumentMutationResult, MutationInput } from './types';
4
+ /**
5
+ * Index in `sections` of the section that ENDS at the section_break at
6
+ * `breakIndex`. Sections are 1:1 with section_breaks; the first
7
+ * section ends at the first break (or at the end of `body` if there's
8
+ * no break).
9
+ *
10
+ * body = [p, p, break, p, break, p]
11
+ * sections = [s0, s1, s2]
12
+ *
13
+ * breakIndex = 2 → 0 (the first break ends section 0)
14
+ * breakIndex = 4 → 1 (the second break ends section 1)
15
+ */
16
+ export declare function removedSectionIndex(body: readonly Block[], breakIndex: number): number;
17
+ /**
18
+ * Drop the section at `endingIndex + 1` from `sections` — that's the
19
+ * section the now-removed break STARTED. The section ENDED by the
20
+ * removed break (at `endingIndex`) absorbs whatever content used to
21
+ * belong to its successor. Properties of the surviving section are
22
+ * preserved verbatim; nothing about the removed section's settings is
23
+ * carried over.
24
+ *
25
+ * If `sections` doesn't have a successor (the removed break was the
26
+ * last one and there's only one section), the array is returned
27
+ * unchanged.
28
+ */
29
+ export declare function mergeSectionsAcross(sections: readonly SectionProperties[], endingIndex: number): SectionProperties[];
30
+ /**
31
+ * Merge a {@link SectionPropertiesPatch} onto existing section properties.
32
+ * `pageSize` / `pageMargins` are FIELD-merged (a partial stays valid); the
33
+ * other fields replace wholesale. For the optional fields (`columns`,
34
+ * `titlePage`, `type`, `vAlign`) an explicit `undefined` clears them, while
35
+ * the required `headerRefs` / `footerRefs` only replace when present.
36
+ */
37
+ export declare function mergeSectionProps(prev: SectionProperties, patch: SectionPropertiesPatch): SectionProperties;
38
+ /** Merge a patch into the section at `sectionIndex`. No block versions
39
+ * bump — a section is not a block. Fails if the index is out of range. */
40
+ export declare function applySectionPropertiesMutation(input: MutationInput, sectionIndex: number, patch: SectionPropertiesPatch): DocumentMutationResult<void>;
@@ -0,0 +1,14 @@
1
+ import { NamedStylePatch } from '../../editor/types';
2
+ import { NamedStyle } from '../types';
3
+ import { DocumentMutationResult, MutationInput } from './types';
4
+ /** Merge a {@link NamedStylePatch} onto an existing style. Each present
5
+ * field replaces the style's field wholesale; an explicit `undefined`
6
+ * clears an OPTIONAL field. The required `type` / `displayName` are never
7
+ * cleared (an undefined for them is ignored). */
8
+ export declare function mergeNamedStyle(prev: NamedStyle, patch: NamedStylePatch): NamedStyle;
9
+ /** Add a new named style. Fails if `style.id` already exists. */
10
+ export declare function defineStyleMutation(input: MutationInput, style: NamedStyle): DocumentMutationResult<void>;
11
+ /** Merge a patch into the style with `id`. Fails if no such style. */
12
+ export declare function updateStyleMutation(input: MutationInput, id: string, patch: NamedStylePatch): DocumentMutationResult<void>;
13
+ /** Remove the style with `id`. Fails if no such style. */
14
+ export declare function removeStyleMutation(input: MutationInput, id: string): DocumentMutationResult<void>;
@@ -0,0 +1,57 @@
1
+ import { BlockRef, EditResult } from '../api';
2
+ import { SobreeDocument } from '../types';
3
+ /**
4
+ * One registry-level operation produced by a mutation. The adapter
5
+ * applies these to its BlockRegistry after committing the new doc:
6
+ * `insert` adds an id, `remove` drops one, `bump` keeps the same id
7
+ * but increments its version.
8
+ */
9
+ export type Mutation = {
10
+ type: "bump";
11
+ index: number;
12
+ } | {
13
+ type: "insert";
14
+ index: number;
15
+ } | {
16
+ type: "remove";
17
+ index: number;
18
+ };
19
+ /**
20
+ * The read-only slice of a block registry the engine depends on. Both
21
+ * the browser `Editor`'s registry and `HeadlessSobree`'s satisfy this
22
+ * directly — the engine stays unaware of how ids/versions are stored or
23
+ * mutated.
24
+ */
25
+ export interface BlockRegistryView {
26
+ indexOf(id: string): number;
27
+ refAt(index: number): BlockRef;
28
+ refById(id: string): BlockRef | null;
29
+ documentVersion(): number;
30
+ }
31
+ export interface MutationInput {
32
+ doc: SobreeDocument;
33
+ registry: BlockRegistryView;
34
+ }
35
+ /**
36
+ * The result of a successful mutation: the document fields to merge, the
37
+ * registry mutations to apply, and the value the public method returns
38
+ * (e.g. the new `BlockRef` for an insert). The adapter's `commit` applies
39
+ * `update` + `mutations` and surfaces `value`.
40
+ */
41
+ export interface MutationPatch<T = void> {
42
+ update: Partial<SobreeDocument>;
43
+ mutations: readonly Mutation[];
44
+ value: T;
45
+ }
46
+ export type DocumentMutationResult<T = void> = EditResult<MutationPatch<T>>;
47
+ /** Wrap a computed patch in a successful {@link DocumentMutationResult}.
48
+ * Affected refs are left empty — the adapter's `commit` computes them when
49
+ * it applies the registry mutations. */
50
+ export declare function okPatch<T = void>(update: Partial<SobreeDocument>, mutations: readonly Mutation[], value?: T): DocumentMutationResult<T>;
51
+ /**
52
+ * Optimistic-lock check shared by both adapters. Aggregates every stale
53
+ * or missing ref into a single lock conflict: a ref whose id no longer
54
+ * exists is reported with `actual: null`; a version mismatch with the
55
+ * live version. Returns `null` when every ref is current.
56
+ */
57
+ export declare function checkRefs(registry: BlockRegistryView, refs: readonly BlockRef[]): EditResult<never> | null;
@@ -0,0 +1,182 @@
1
+ import { Shading, TableBorders, TableCellBorders, TableCellMargins } from '../formatting.types';
2
+ import { TableLook } from '../tableStyle.types';
3
+ import { ParagraphAlignment, ParagraphProperties } from './paragraph';
4
+ import { InlineRun } from './runs';
5
+ export type Block = Paragraph | Table | SectionBreak | InlineFrame;
6
+ export interface Paragraph {
7
+ kind: "paragraph";
8
+ properties: ParagraphProperties;
9
+ /** Inline runs in document order. May be empty (a blank paragraph). */
10
+ runs: InlineRun[];
11
+ }
12
+ /** Explicit page-break or section-break marker emitted between paragraphs. */
13
+ export interface SectionBreak {
14
+ kind: "section_break";
15
+ /** Which section in `SobreeDocument.sections` continues after this point. */
16
+ toSectionIndex: number;
17
+ }
18
+ export interface Table {
19
+ kind: "table";
20
+ /** Column widths in twips. Length = number of columns. */
21
+ grid: number[];
22
+ rows: TableRow[];
23
+ properties: TableProperties;
24
+ }
25
+ export interface TableProperties {
26
+ /** Total table width in twips, or "auto" for content-driven. */
27
+ widthTwips?: number;
28
+ alignment?: ParagraphAlignment;
29
+ borders?: TableBorders;
30
+ /** Style reference (e.g. "TableGrid"). */
31
+ styleId?: string;
32
+ /** `<w:tblLook>` — which of the table style's conditional formats are
33
+ * active (first row / column, last row / column, row / column
34
+ * banding). Gates {@link TableStyleDefinition} resolution. */
35
+ look?: TableLook;
36
+ /** `<w:tblCellMar>` — default inner padding for every cell (the table's
37
+ * own value wins over the style's). Word's stock default is ~108 twips
38
+ * left / right and 0 top / bottom when omitted. */
39
+ cellMargins?: TableCellMargins;
40
+ }
41
+ export interface TableRow {
42
+ cells: TableCell[];
43
+ /** True if this row is a header row repeated on each page. */
44
+ isHeader?: boolean;
45
+ }
46
+ export interface TableCell {
47
+ /** Number of grid columns this cell spans horizontally. */
48
+ gridSpan?: number;
49
+ /** Vertical merge state — `restart` begins a merge, `continue` continues. */
50
+ vMerge?: "restart" | "continue";
51
+ verticalAlign?: "top" | "center" | "bottom";
52
+ shading?: Shading;
53
+ borders?: TableCellBorders;
54
+ /** Cell content — paragraphs and (rare) nested tables. */
55
+ content: Block[];
56
+ }
57
+ /**
58
+ * A rectangular drawing region that flows inline with body blocks
59
+ * (NOT absolutely positioned — that's `AnchoredFrame`). Owns its own
60
+ * picture decoration(s), its own textbox body (recursive `Block[]`),
61
+ * and its own break / keep-with-next directives. Maps 1:1 to
62
+ * `<w:drawing><wp:inline>` with a `<wpg:wgp>` payload that wraps a
63
+ * `<wps:txbx>` textbox shape + decorative `<pic:pic>` / `<wps:wsp>`
64
+ * siblings.
65
+ *
66
+ * Replaces the legacy lifter's "split into N body paragraphs with
67
+ * `liftedFromTextBox` + `framePictures` + `textboxShape` glued on"
68
+ * approach. One frame = one block. The page-break directive that
69
+ * belonged to the containing paragraph in the source OOXML moves
70
+ * here (`pageBreakBefore`), where the paginator's top-level-child
71
+ * inspection actually finds it.
72
+ *
73
+ * See `packages/core/docs/INLINE_FRAME_DESIGN.md` for the full
74
+ * design and migration plan.
75
+ *
76
+ * **Status**: type declared (Phase 1.0). Importer does not yet emit
77
+ * this; renderer treats it as a no-op. Wiring lands in Phase 1.1-1.2.
78
+ */
79
+ /** One textbox shape inside an inline-frame group: its intra-group
80
+ * position + size and recursive body, plus optional chrome (fill /
81
+ * border / text insets) and vertical text anchor. */
82
+ export interface InlineFrameTextbox {
83
+ offsetEmu: {
84
+ xEmu: number;
85
+ yEmu: number;
86
+ };
87
+ sizeEmu: {
88
+ wEmu: number;
89
+ hEmu: number;
90
+ };
91
+ body: Block[];
92
+ fill?: string;
93
+ border?: FrameBorder;
94
+ /** `<wps:bodyPr>` text insets (lIns/tIns/rIns/bIns) → CSS padding. */
95
+ padding?: {
96
+ topEmu: number;
97
+ rightEmu: number;
98
+ bottomEmu: number;
99
+ leftEmu: number;
100
+ };
101
+ /** Vertical text anchor from `<wps:bodyPr anchor>`; defaults to "top". */
102
+ vAlign?: "top" | "center" | "bottom";
103
+ }
104
+ export interface InlineFrame {
105
+ kind: "inline_frame";
106
+ /** From the containing `<w:p>`'s `<w:pPr>`. The paginator emits a
107
+ * `Penalty(-Infinity)` before the frame when set. */
108
+ pageBreakBefore?: boolean;
109
+ /** From the containing `<w:p>`'s `<w:pPr>`. Keep with the next
110
+ * block to avoid widowing a section heading from its body. */
111
+ keepNext?: boolean;
112
+ /** The containing `<w:p>`'s resolved paragraph properties. An inline
113
+ * drawing lives inside a host paragraph; that paragraph carries
114
+ * spacing (before/after), alignment, etc. from its style cascade
115
+ * (commonly `Normal`'s `<w:spacing w:after>`). The renderer applies
116
+ * these to the frame wrapper so the band reserves the SAME vertical
117
+ * box (drawing height + paragraph spacing) Word does — without it
118
+ * the band is short by the spacing-after, which compounds down the
119
+ * page and shifts pagination. Absent → wrapper uses bare defaults. */
120
+ hostProps?: ParagraphProperties;
121
+ /** The drawing group's intrinsic coordinate-system extent. Every
122
+ * child's `offsetEmu` / `sizeEmu` below is expressed in this
123
+ * space; the renderer scales them by `sizeEmu / groupExtentEmu`
124
+ * when painting at the final rendered size. */
125
+ groupExtentEmu: {
126
+ wEmu: number;
127
+ hEmu: number;
128
+ };
129
+ /** The frame's rendered display dimensions. Usually equal to
130
+ * `groupExtentEmu` for inline drawings (no scaling) but kept
131
+ * separate so a future "render at half-size" / scaling case
132
+ * doesn't require touching every child. */
133
+ sizeEmu: {
134
+ wEmu: number;
135
+ hEmu: number;
136
+ };
137
+ /** The group's textbox SHAPES, in document (child) order. Most groups
138
+ * have one (a section "pill" heading: a centred line over a background
139
+ * picture), but a "Project: X" entry has two — a title textbox and a
140
+ * details textbox — and the renderer must show BOTH. Empty when the
141
+ * group carries only pictures / shapes (no textbox content). */
142
+ textboxes: InlineFrameTextbox[];
143
+ /** Decorative pictures inside the group. Each carries its own
144
+ * intra-group position. The renderer paints them as
145
+ * absolute-positioned `<img>` children of the frame wrapper,
146
+ * scaled by the same `sizeEmu / groupExtentEmu` ratio. */
147
+ pictures: ReadonlyArray<{
148
+ partPath: string;
149
+ offsetEmu: {
150
+ xEmu: number;
151
+ yEmu: number;
152
+ };
153
+ sizeEmu: {
154
+ wEmu: number;
155
+ hEmu: number;
156
+ };
157
+ altText?: string;
158
+ }>;
159
+ /** Non-picture decorative shapes (rect / ellipse / line) inside
160
+ * the group. Same positioning model as `pictures`. */
161
+ shapes: ReadonlyArray<{
162
+ geometry: "rect" | "ellipse" | "roundedRect" | "line";
163
+ offsetEmu: {
164
+ xEmu: number;
165
+ yEmu: number;
166
+ };
167
+ sizeEmu: {
168
+ wEmu: number;
169
+ hEmu: number;
170
+ };
171
+ fill?: string;
172
+ border?: FrameBorder;
173
+ }>;
174
+ }
175
+ /** Shared border descriptor for inline / anchored frame chrome.
176
+ * Distinct from `BorderSpec` (used for paragraph / table borders
177
+ * whose OOXML `w:val` covers a different vocabulary). */
178
+ export interface FrameBorder {
179
+ color: string;
180
+ widthEmu: number;
181
+ style: "solid" | "dashed" | "dotted" | "double";
182
+ }
@@ -0,0 +1,122 @@
1
+ import { FontDeclaration } from '../../fonts/types';
2
+ import { Block, InlineFrame } from './block';
3
+ import { AnchoredFrame } from './drawing';
4
+ import { NumberingDefinition } from './numbering';
5
+ import { SectionProperties } from './sections';
6
+ import { NamedStyle } from './styles';
7
+ export interface SobreeDocument {
8
+ /** Top-level body content, in document order. */
9
+ body: Block[];
10
+ /**
11
+ * One section per body slice. The simplest doc has exactly one section
12
+ * spanning the whole body. Phase N1 supports a single-section model.
13
+ */
14
+ sections: SectionProperties[];
15
+ /**
16
+ * Header and footer bodies keyed by `HeaderFooterRef.partId`. The partId
17
+ * is the ZIP target name (`header1.xml`, `footer2.xml`, …). Rendering
18
+ * emits each body to its own OOXML part at export time.
19
+ */
20
+ headerFooterBodies: Record<string, Block[]>;
21
+ /**
22
+ * Floating objects that live inside a header/footer part, keyed by the
23
+ * SAME `HeaderFooterRef.partId` as `headerFooterBodies`. A header part
24
+ * is a self-contained sub-document: its flow blocks live in
25
+ * `headerFooterBodies[partId]`, its anchored frames here. The renderer
26
+ * paints these into a per-zone overlay exactly like body `anchoredFrames`.
27
+ * Empty/absent for the common header-without-floats case.
28
+ */
29
+ headerFooterFrames?: Record<string, AnchoredFrame[]>;
30
+ /** Named styles (Heading1, Quote, Body Text, …) defined at the doc level. */
31
+ styles: NamedStyle[];
32
+ /** List/numbering definitions referenced by `Paragraph.properties.numbering`. */
33
+ numbering: NumberingDefinition[];
34
+ /**
35
+ * Embedded binary parts keyed by ZIP path (e.g. `word/media/image1.png`).
36
+ * Images, fonts, custom XML — anything not represented in body XML.
37
+ */
38
+ rawParts: Record<string, Uint8Array>;
39
+ /**
40
+ * Font declarations from `word/fontTable.xml`. Empty for new docs.
41
+ * Embedded faces reference parts inside `rawParts`.
42
+ */
43
+ fonts: FontDeclaration[];
44
+ /**
45
+ * Footnote bodies keyed by id. Body inline runs use `FootnoteRefRun`
46
+ * with the matching `id` to reference them. Empty for docs without
47
+ * footnotes (the common case).
48
+ */
49
+ footnotes?: Record<number, Block[]>;
50
+ /**
51
+ * Comments keyed by id. Body inline runs whose properties carry a
52
+ * matching `commentIds` mark the range each comment annotates.
53
+ * Empty for docs without comments (the common case).
54
+ */
55
+ comments?: Record<number, Comment>;
56
+ /**
57
+ * Document-wide layout settings parsed from `word/settings.xml`.
58
+ * Currently only `defaultTabStopTwips` — Word's default interval for
59
+ * tab advances in paragraphs that don't declare their own `<w:tabs>`
60
+ * stops. Word's factory default is 720 twips (0.5"). Without
61
+ * respecting this, the browser falls back to the CSS `tab-size`
62
+ * default of 8 characters which is much narrower than what Word
63
+ * shows, and tab-aligned content (e.g. label/value columns in
64
+ * letterheads) ends up cramped.
65
+ */
66
+ settings?: {
67
+ defaultTabStopTwips?: number;
68
+ };
69
+ /**
70
+ * NOTE: inline-drawing frames are emitted as `InlineFrame` BLOCKS
71
+ * directly in `body` (the importer splices them in at their source
72
+ * paragraph's position via `ConvertOptions.replaceParagraphs`), so
73
+ * they paginate in document flow. This optional top-level list is
74
+ * a secondary handle kept for tooling/inspection; the renderer
75
+ * consumes the body blocks, not this list.
76
+ *
77
+ * The first-class model replaced the old `liftTextBoxContent`
78
+ * machinery that exploded section-heading textboxes into body
79
+ * paragraphs with synthetic `framePictures` / `liftedFromTextBox`
80
+ * metadata. See `packages/core/docs/INLINE_FRAME_DESIGN.md`.
81
+ */
82
+ inlineFrames?: InlineFrame[];
83
+ /**
84
+ * Floating objects (`<w:drawing>/<wp:anchor>`, `<w:pict>` VML) that
85
+ * live in their own layer above the body. Each frame carries its
86
+ * own coordinates + dimensions in EMU and is independent of
87
+ * pagination — the paginator only sees `body` blocks. The renderer
88
+ * places each frame on the page its anchor resolves to and paints
89
+ * it into a per-page `<div class="paper-anchors">` overlay.
90
+ *
91
+ * Replaces the pre-AnchoredFrame "lifter" architecture where the
92
+ * importer exploded each `<w:txbxContent>` into N body paragraphs
93
+ * with synthetic `liftedFromTextBox` metadata. The flat list here
94
+ * is simpler, makes selection / resize / move trivial (each frame
95
+ * is one DOM element), and frees the paginator from having to
96
+ * route around absolute-positioned ghosts.
97
+ */
98
+ anchoredFrames?: AnchoredFrame[];
99
+ }
100
+ export interface Comment {
101
+ id: number;
102
+ author?: string;
103
+ /** Author initials as recorded in the docx — Word uses them in the
104
+ * comment sidebar header. */
105
+ initials?: string;
106
+ /** ISO-8601 date string. */
107
+ date?: string;
108
+ /** Comment body — typically one or more paragraphs. */
109
+ body: Block[];
110
+ /**
111
+ * Resolved / "Done" flag from `word/commentsExtended.xml`
112
+ * (`<w15:commentEx w15:done="1">`). Absent or false → open.
113
+ */
114
+ done?: boolean;
115
+ /**
116
+ * Id of the parent comment when this is a reply in a thread.
117
+ * Resolved from `<w15:commentEx w15:paraIdParent="…">` by matching
118
+ * the parent's body-paragraph paraId back to its comment id.
119
+ * Absent → top-level comment.
120
+ */
121
+ replyToId?: number;
122
+ }
@@ -0,0 +1,141 @@
1
+ import { Block } from './block';
2
+ /**
3
+ * A floating object pinned to a page or paragraph at an explicit
4
+ * coordinate. Sources: `<w:drawing>/<wp:anchor>` shapes,
5
+ * `<w:pict>` VML boxes.
6
+ *
7
+ * Coordinates use OOXML's EMU (914400 EMU = 1 inch). The renderer
8
+ * converts to CSS millimetres at paint time.
9
+ */
10
+ export interface AnchoredFrame {
11
+ /** Stable id, deterministic from document import order. */
12
+ id: string;
13
+ /** What the offsets are measured from. */
14
+ anchor: AnchorOrigin;
15
+ offsetXEmu: number;
16
+ offsetYEmu: number;
17
+ widthEmu: number;
18
+ heightEmu: number;
19
+ /** Stacking order. Higher = on top. Default 0. */
20
+ zIndex?: number;
21
+ /** When true the frame paints BEHIND body text (z-index negative).
22
+ * Maps to OOXML `<wp:anchor behindDoc="1">`. */
23
+ behindText?: boolean;
24
+ /** Text-wrap mode from the `<wp:wrap*>` child of `<wp:anchor>`.
25
+ * Decides whether the frame DISPLACES body flow: `square` /
26
+ * `topAndBottom` / `tight` / `through` reserve vertical space (the
27
+ * paginator treats the anchor paragraph as that tall), while `none`
28
+ * floats over text and reserves nothing. Absent ⇒ unknown (treated
29
+ * as non-displacing). */
30
+ wrap?: "square" | "topAndBottom" | "tight" | "through" | "none";
31
+ /** `wrapText` side from `<wp:wrapSquare|Tight|Through wrapText="…">` —
32
+ * which sides of the frame body text flows on. Default `bothSides`.
33
+ * Only meaningful for the displacing wrap modes; drives whether a
34
+ * floated image goes `float: left` (text on the right) or `right`. */
35
+ wrapText?: "bothSides" | "left" | "right" | "largest";
36
+ /** Text-distance insets — `distT/B/L/R` on `<wp:anchor>`, in EMU. The
37
+ * gap Word keeps between the frame and the text wrapping around it;
38
+ * rendered as margins on the floated frame. */
39
+ textDistancesEmu?: {
40
+ topEmu: number;
41
+ rightEmu: number;
42
+ bottomEmu: number;
43
+ leftEmu: number;
44
+ };
45
+ /** What this frame contains. */
46
+ content: AnchoredContent;
47
+ }
48
+ /**
49
+ * Where an anchored frame is positioned and which page receives it.
50
+ *
51
+ * - `sectionIndex` decides which section's pages are candidates.
52
+ * - `paragraphIndex` (optional) ties the frame to a specific body
53
+ * paragraph — useful when `verticalFrom: "paragraph"` so the
54
+ * frame floats to whichever page the paragraph paginates onto.
55
+ * When absent, the frame is page-relative (lands on the first
56
+ * page of its section).
57
+ * - `horizontalFrom` / `verticalFrom` mirror OOXML's
58
+ * `relativeFromH` / `relativeFromV` enums.
59
+ */
60
+ export interface AnchorOrigin {
61
+ sectionIndex: number;
62
+ paragraphIndex?: number;
63
+ horizontalFrom: "page" | "margin" | "column";
64
+ verticalFrom: "page" | "margin" | "paragraph";
65
+ }
66
+ /**
67
+ * What an AnchoredFrame contains. Closed union — adding a new variant
68
+ * requires both importer and renderer support.
69
+ *
70
+ * - "picture" — a single image from `rawParts`. The frame's
71
+ * widthEmu / heightEmu give the display size; the picture
72
+ * stretches to fill (matches Word's default sizing behaviour).
73
+ *
74
+ * - "textbox" — rich body content. Renders recursively via
75
+ * `renderBlocks` inside the frame, with `overflow: hidden` so
76
+ * text that exceeds the frame's height clips (Word behaviour).
77
+ * Optional fill / border / padding paint the textbox chrome.
78
+ *
79
+ * - "shape" — a vector primitive (filled rectangle, ellipse,
80
+ * rounded-rect). No text. Used for decorative backgrounds and
81
+ * dividers (dotted lines, banner rectangles).
82
+ *
83
+ * - "group" — wraps other frames at NESTED coordinates. The
84
+ * children's offsets are interpreted in the group's local
85
+ * coordinate system, scaled to fill `widthEmu × heightEmu`.
86
+ * Maps to OOXML `<wpg:wgp>`. This is how a single project
87
+ * heading carries its rounded-rect frame + atom icon + arrow
88
+ * stripe as ONE selectable unit.
89
+ */
90
+ export type AnchoredContent = {
91
+ kind: "picture";
92
+ partPath: string;
93
+ altText?: string;
94
+ } | {
95
+ kind: "textbox";
96
+ body: Block[];
97
+ fill?: string;
98
+ border?: {
99
+ color: string;
100
+ widthEmu: number;
101
+ style: "solid" | "dashed" | "dotted" | "double";
102
+ };
103
+ padding?: {
104
+ topEmu: number;
105
+ rightEmu: number;
106
+ bottomEmu: number;
107
+ leftEmu: number;
108
+ };
109
+ } | {
110
+ kind: "shape";
111
+ geometry: "rect" | "ellipse" | "roundedRect" | "line" | "custom";
112
+ fill?: string;
113
+ border?: {
114
+ color: string;
115
+ widthEmu: number;
116
+ style: "solid" | "dashed" | "dotted" | "double";
117
+ };
118
+ /** Present when `geometry === "custom"`: a DrawingML `<a:custGeom>`
119
+ * outline as an SVG path in its own `widthEmu × heightEmu` box,
120
+ * rendered as a scaled `<svg><path>`. Absent for preset geometry. */
121
+ path?: {
122
+ widthEmu: number;
123
+ heightEmu: number;
124
+ d: string;
125
+ };
126
+ } | {
127
+ kind: "group";
128
+ children: AnchoredFrame[];
129
+ /** Local coordinate system extent (`<a:chExt>`). A child at offset
130
+ * `P` maps into the group's rendered box as
131
+ * `(P − childCoordOffset) × (size / childCoordSystem)` — the
132
+ * extent gives the scale. */
133
+ childCoordSystemCx: number;
134
+ childCoordSystemCy: number;
135
+ /** Local coordinate system ORIGIN (`<a:chOff>`). Child offsets are
136
+ * measured from this point, not from 0 — so it must be subtracted
137
+ * before scaling, or the children shift by `chOff × scale`.
138
+ * Absent ⇒ origin is `(0, 0)` (the common case). */
139
+ childCoordOffsetX?: number;
140
+ childCoordOffsetY?: number;
141
+ };
@@ -0,0 +1,7 @@
1
+ export interface HeaderFooterRef {
2
+ type: "default" | "first" | "even";
3
+ /** Internal id pointing into `SobreeDocument.rawParts` /
4
+ * `relationships`. We store the header/footer body itself as a
5
+ * `Block[]` keyed in a side table at the SobreeDocument level. */
6
+ partId: string;
7
+ }