@sobree/core 0.1.28 → 0.1.30
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/dist/doc/mutations/blocks.d.ts +14 -0
- package/dist/doc/mutations/index.d.ts +38 -0
- package/dist/doc/mutations/numbering.d.ts +8 -0
- package/dist/doc/mutations/paragraphs.d.ts +13 -0
- package/dist/doc/mutations/sections.d.ts +40 -0
- package/dist/doc/mutations/styles.d.ts +14 -0
- package/dist/doc/mutations/types.d.ts +57 -0
- package/dist/doc/types/block.d.ts +182 -0
- package/dist/doc/types/document.d.ts +122 -0
- package/dist/doc/types/drawing.d.ts +141 -0
- package/dist/doc/types/headersFooters.d.ts +7 -0
- package/dist/doc/types/index.d.ts +34 -0
- package/dist/doc/types/numbering.d.ts +25 -0
- package/dist/doc/types/paragraph.d.ts +82 -0
- package/dist/doc/types/parts.d.ts +18 -0
- package/dist/doc/types/revisions.d.ts +8 -0
- package/dist/doc/types/runs.d.ts +178 -0
- package/dist/doc/types/sections.d.ts +67 -0
- package/dist/doc/types/styles.d.ts +32 -0
- package/dist/doc/types.d.ts +7 -842
- package/dist/editor/index.d.ts +7 -2
- package/dist/editor/internal/applyMutation.d.ts +9 -0
- package/dist/editor/internal/mutations.d.ts +4 -64
- package/dist/editor/ops/blocks.d.ts +5 -4
- package/dist/editor/renderedDocument/blocks.d.ts +14 -0
- package/dist/editor/renderedDocument/comments.d.ts +6 -0
- package/dist/editor/renderedDocument/index.d.ts +27 -0
- package/dist/editor/renderedDocument/revisions.d.ts +11 -0
- package/dist/editor/renderedDocument/selectors.d.ts +51 -0
- package/dist/editor/renderedDocument/types.d.ts +59 -0
- package/dist/editor/table.d.ts +17 -11
- package/dist/headless.d.ts +14 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5154 -4829
- package/dist/index.js.map +1 -1
- package/dist/ydoc/apply.d.ts +7 -2
- package/dist/ydoc/blockCodec.d.ts +55 -0
- package/dist/ydoc/frameCodec.d.ts +33 -0
- package/dist/ydoc/index.d.ts +3 -2
- package/dist/ydoc/project.d.ts +1 -6
- package/dist/ydoc/schema.d.ts +66 -8
- package/dist/ydoc/seed.d.ts +2 -33
- package/package.json +1 -1
|
@@ -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
|
+
}
|