@sobree/core 0.1.29 → 0.1.31

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.
@@ -23,8 +23,13 @@ import * as Y from "yjs";
23
23
  * JSON blob on the Y.Map's `props` field. Concurrent property
24
24
  * edits clobber. Phase 1c may split into per-key Y.Map.
25
25
  *
26
- * - **Non-paragraph blocks (section_break, table):** stored as
27
- * JSON-encoded `_ast`. Concurrent edits clobber.
26
+ * - **Tables:** nested per-cell `rows`/`cells`/`content` Y.Arrays
27
+ * with per-cell JSON props (see `./blockCodec.ts`). Concurrent edits
28
+ * to *different* cells merge; cell text merges char-level. A legacy
29
+ * whole-table `_ast` migrates to the nested shape on first edit.
30
+ *
31
+ * - **Section breaks / inline frames:** JSON-encoded `_ast` leaf.
32
+ * Concurrent edits clobber.
28
33
  *
29
34
  * - **Document meta (sections, styles, numbering, fonts,
30
35
  * headerFooterBodies):** JSON-encoded on a `meta` Y.Map.
@@ -0,0 +1,55 @@
1
+ import { Block } from '../doc/types';
2
+ /**
3
+ * Recursive block ↔ Y.Map codec — the single place that maps a
4
+ * `SobreeDocument` block to/from its Y.Doc representation, at ANY depth.
5
+ *
6
+ * The same block-Y.Map shape is used at the top level (`body` array) and
7
+ * nested inside composite blocks (a table cell's `content`, a frame
8
+ * textbox's `body`). Three operations, all recursive:
9
+ *
10
+ * - {@link buildBlockSkeleton} + {@link populateBlock} — AST → Y, two
11
+ * phase: skeleton builds the full nested STRUCTURE (Y.Arrays / Y.Maps /
12
+ * empty Y.Texts + all JSON); populate applies the text deltas, which
13
+ * requires the Y.Text to be integrated (so it runs after the root is
14
+ * inserted into an integrated parent).
15
+ * - {@link projectBlock} — Y → AST.
16
+ * - {@link updateBlockYMap} — diff an AST block into an existing integrated
17
+ * map (minimal Y ops; paragraph text via smart diff so concurrent
18
+ * char edits merge).
19
+ *
20
+ * Per-kind storage:
21
+ * - paragraph → `text` Y.Text (char CRDT) + `props` JSON
22
+ * - table → `grid`/`props` JSON + `rows` Y.Array (rows → cells →
23
+ * each cell `props` JSON + `content` Y.Array<blockMap>).
24
+ * Per-CELL props ⇒ concurrent styling of *different*
25
+ * cells merges; cell text merges char-level.
26
+ * - section_break / inline_frame → `_ast` leaf (whole-block JSON)
27
+ *
28
+ * The `content` array helpers ({@link buildContent} / {@link projectContent}
29
+ * / {@link updateContent}) are exported for reuse by the anchored-frame
30
+ * meta restructuring, whose textbox bodies are the same `Block[]`.
31
+ *
32
+ * Nested content arrays diff POSITIONALLY (a cell holds ~one paragraph);
33
+ * the top-level body stays id-matched (see `apply.ts`). Migration: a
34
+ * pre-nesting table is a leaf `_ast`; {@link projectBlock} reads it via the
35
+ * fallback, and {@link updateBlockYMap} rebuilds it nested on first edit.
36
+ */
37
+ import * as Y from "yjs";
38
+ type YMap = Y.Map<unknown>;
39
+ type YArr = Y.Array<YMap>;
40
+ /** Build a detached block Y.Map skeleton — full nested structure + JSON,
41
+ * empty Y.Texts. Call {@link populateBlock} after it's integrated. */
42
+ export declare function buildBlockSkeleton(id: string, block: Block): YMap;
43
+ /** Build a detached `Y.Array` of block skeletons for a `Block[]` content
44
+ * list (cell content, frame body). Nested blocks have no stable id —
45
+ * matched positionally — so they carry an empty id. */
46
+ export declare function buildContent(blocks: readonly Block[]): YArr;
47
+ export declare function populateBlock(map: YMap, block: Block): void;
48
+ /** Apply text deltas across an integrated content array. */
49
+ export declare function populateContent(arr: YArr, blocks: readonly Block[]): void;
50
+ export declare function projectBlock(map: YMap): Block | null;
51
+ export declare function projectContent(arr: YArr): Block[];
52
+ export declare function updateBlockYMap(map: YMap, block: Block): void;
53
+ /** Diff a `Block[]` into an integrated content array (positional). */
54
+ export declare function updateContent(arr: YArr, blocks: readonly Block[]): void;
55
+ export {};
@@ -0,0 +1,33 @@
1
+ import { AnchoredFrame } from '../doc/types';
2
+ /**
3
+ * Recursive AnchoredFrame ↔ Y.Map codec — the floating-layer analogue of
4
+ * `./blockCodec.ts`. Anchored frames (the textbox "pills", brochure panels,
5
+ * grouped drawings) previously rode in `meta` as one JSON blob, so a
6
+ * concurrent edit to ANY frame clobbered the whole layer. Here each frame is
7
+ * its own Y.Map, and a textbox frame's editable `body` reuses the block
8
+ * codec's content arrays — so concurrent edits to DIFFERENT frames merge,
9
+ * and text inside a frame merges char-level like body paragraphs.
10
+ *
11
+ * Per-content storage on a frameMap:
12
+ * - `_ast` — the AnchoredFrame MINUS its editable parts (geometry,
13
+ * position, fill/border, content discriminator)
14
+ * - textbox → `body` Y.Array<blockMap> (the editable body)
15
+ * - group → `children` Y.Array<frameMap> (recurse)
16
+ * - picture / shape → no child arrays (the `_ast` is the whole frame)
17
+ *
18
+ * Two-phase build (skeleton → integrate → populate) mirrors blockCodec, so a
19
+ * textbox body's Y.Texts are integrated before their deltas apply.
20
+ */
21
+ import * as Y from "yjs";
22
+ type YMap = Y.Map<unknown>;
23
+ type YArr = Y.Array<YMap>;
24
+ export declare function buildFrameSkeleton(frame: AnchoredFrame): YMap;
25
+ export declare function buildFrames(frames: readonly AnchoredFrame[]): YArr;
26
+ export declare function populateFrame(m: YMap, frame: AnchoredFrame): void;
27
+ export declare function populateFrames(arr: YArr, frames: readonly AnchoredFrame[]): void;
28
+ export declare function projectFrame(m: YMap): AnchoredFrame | null;
29
+ export declare function projectFrames(arr: YArr): AnchoredFrame[];
30
+ export declare function updateFrame(m: YMap, frame: AnchoredFrame): void;
31
+ /** Diff a frame list into an integrated `Y.Array<frameMap>` (positional). */
32
+ export declare function updateFrames(arr: YArr, frames: readonly AnchoredFrame[]): void;
33
+ export {};
@@ -11,8 +11,9 @@
11
11
  * that preserves CRDT semantics across applies.
12
12
  */
13
13
  export { Y_BLOCK_AST_KEY, Y_BLOCK_ID_KEY, Y_BLOCK_KIND_KEY, Y_BLOCK_PROPS_KEY, Y_BLOCK_TEXT_KEY, Y_BODY_KEY, Y_META_FIELDS, Y_META_KEY, Y_PARTREFS_KEY, Y_PARTS_KEY, } from './schema';
14
- export { seedYDoc, buildBlockYMap, buildSkeletonBlockYMap, populateBlockContent, populateParagraphContent, populateParagraphYMap, } from './seed';
15
- export { projectYDoc, projectBlock } from './project';
14
+ export { seedYDoc } from './seed';
15
+ export { buildBlockSkeleton, buildContent, populateBlock, populateContent, projectBlock, projectContent, updateBlockYMap, updateContent, } from './blockCodec';
16
+ export { projectYDoc } from './project';
16
17
  export { applyDocumentToYDoc, applyPartRefsToYDoc, removePartRefsFromYDoc, } from './apply';
17
18
  export { type DeltaOp, type EmbedContent, type LinkMark, attrsToRunProps, deepEqual, deltaToRuns, runPropsToAttrs, runsToDelta, } from './runs';
18
19
  export { diffApplyText } from './textDiff';
@@ -1,4 +1,4 @@
1
- import { Block, SobreeDocument } from '../doc/types';
1
+ import { SobreeDocument } from '../doc/types';
2
2
  import type * as Y from "yjs";
3
3
  /**
4
4
  * Read the SobreeDocument projection out of a Y.Doc.
@@ -34,8 +34,3 @@ export declare function projectYDoc(ydoc: Y.Doc): {
34
34
  ids: string[];
35
35
  partRefs: Record<string, string>;
36
36
  };
37
- /**
38
- * Read a single block Y.Map into a Block. Returns `null` if the map
39
- * is empty / unrecognizable (defensive — projectYDoc skips nulls).
40
- */
41
- export declare function projectBlock(map: Y.Map<unknown>): Block | null;
@@ -10,12 +10,17 @@
10
10
  * ```
11
11
  * ydoc
12
12
  * ├── getArray("body") : Y.Array<Y.Map> — block list, one Y.Map per block
13
+ * ├── getArray("anchoredFrames") : Y.Array<frameMap> — body floating layer
14
+ * │ (per-frame CRDT; `./frameCodec.ts`).
15
+ * ├── getMap("headerFooterFrames") : Y.Map<zoneId, Y.Array> — per-zone floating
16
+ * │ layers (per-frame CRDT).
13
17
  * ├── getMap("meta") : Y.Map — sections, styles, numbering,
14
18
  * │ headerFooterBodies, fonts.
15
19
  * │ Stored as JSON-encoded values
16
20
  * │ (rarely edited concurrently).
17
- * │ Phase 1c may split fields into
18
- * │ per-key Y types.
21
+ * │ Legacy `anchoredFrames` /
22
+ * │ `headerFooterFrames` keys are read
23
+ * │ only as a migration fallback.
19
24
  * ├── getMap("parts") : Y.Map<Uint8Array> — inline binary parts
20
25
  * │ (legacy / no-BlobStore path).
21
26
  * └── getMap("partRefs") : Y.Map<string> — partPath → SHA-256 hex hash
@@ -49,16 +54,48 @@
49
54
  *
50
55
  * # Block Y.Map shape — non-paragraphs
51
56
  *
52
- * Section breaks and tables stay JSON-encoded — neither has inline
53
- * text content with concurrent-edit demand. Tables get their own
54
- * structural CRDT in a future Phase 1c (per-cell Y.Map<body Y.Array>).
57
+ * Section breaks stay fully JSON-encoded — no editable inline content:
55
58
  *
56
59
  * ```
57
- * otherBlockMap
60
+ * sectionBreakMap
58
61
  * ├── get("id") : string — stable block id
59
62
  * └── get("_ast") : string (JSON) — JSON-encoded Block
60
63
  * ```
61
64
  *
65
+ * # Composite blocks — per-part CRDT (Phase 1c)
66
+ *
67
+ * Tables and textbox inline-frames hold nested editable `Block[]`, so
68
+ * their content lives in nested Y structures (not one opaque `_ast`):
69
+ * concurrent edits to *different* cells / frame paragraphs merge, and
70
+ * cell text merges char-level like body paragraphs. The block-Y.Map ↔
71
+ * `Block` mapping is recursive (`./blockCodec.ts`) — a `content` /
72
+ * `body` array holds the same block-Y.Map shape used at the top level.
73
+ *
74
+ * ```
75
+ * tableMap
76
+ * ├── get("id") : string — stable block id
77
+ * ├── get("kind") : "table" — discriminator
78
+ * ├── get("grid") : string (JSON) — number[] column widths (per-table LWW)
79
+ * ├── get("props") : string (JSON) — TableProperties (per-table LWW)
80
+ * └── get("rows") : Y.Array<rowMap>
81
+ * rowMap.get("props") : string (JSON) — { isHeader? } (per-row LWW)
82
+ * rowMap.get("cells") : Y.Array<cellMap>
83
+ * cellMap.get("props") : string (JSON) — gridSpan/vMerge/shading/… (per-CELL LWW)
84
+ * cellMap.get("content") : Y.Array<blockMap> — recurse
85
+ *
86
+ * inlineFrameMap
87
+ * ├── get("id") : string — stable block id
88
+ * ├── get("kind") : "inline_frame" — discriminator
89
+ * ├── get("_ast") : string (JSON) — the frame MINUS textbox.body (geometry/props)
90
+ * └── get("body") : Y.Array<blockMap> — textbox body, recurse
91
+ * ```
92
+ *
93
+ * Migration: a pre-Phase-1c table/frame is one whole-block `_ast` (no
94
+ * `rows`/`body` array). Projection reads that fallback; the first edit
95
+ * rebuilds it in the nested shape. Nested content arrays diff
96
+ * positionally (cell content is typically one paragraph); the body
97
+ * array stays id-matched.
98
+ *
62
99
  * # Y.Text mark conventions
63
100
  *
64
101
  * The mapping between Sobree's `RunProperties` and Y.Text marks is
@@ -110,9 +147,30 @@ export declare const Y_BLOCK_KIND_KEY = "kind";
110
147
  export declare const Y_BLOCK_TEXT_KEY = "text";
111
148
  /** Phase 1b.5+: JSON-encoded ParagraphProperties on paragraph blocks. */
112
149
  export declare const Y_BLOCK_PROPS_KEY = "props";
113
- /** Phase 1a: JSON-encoded Block on non-paragraph blocks (and on
114
- * Phase 1a-shaped paragraph blocks for backwards compat). */
150
+ /** JSON-encoded Block on leaf non-paragraph blocks (section_break), on
151
+ * Phase 1a-shaped blocks for backwards compat, and — for inline-frame
152
+ * composites — the frame MINUS its textbox body (geometry / props). */
115
153
  export declare const Y_BLOCK_AST_KEY = "_ast";
154
+ /** Table: JSON-encoded `number[]` column-width grid. */
155
+ export declare const Y_TABLE_GRID_KEY = "grid";
156
+ /** Table: `Y.Array` of row Y.Maps. */
157
+ export declare const Y_TABLE_ROWS_KEY = "rows";
158
+ /** Table row: `Y.Array` of cell Y.Maps. */
159
+ export declare const Y_ROW_CELLS_KEY = "cells";
160
+ /** Table cell: `Y.Array` of block Y.Maps (the cell's content). */
161
+ export declare const Y_CELL_CONTENT_KEY = "content";
162
+ /** Textbox frame: `Y.Array` of block Y.Maps (the frame's editable body). */
163
+ export declare const Y_FRAME_BODY_KEY = "body";
164
+ /** Group frame: `Y.Array` of child frame Y.Maps (recurse). */
165
+ export declare const Y_FRAME_CHILDREN_KEY = "children";
166
+ /** Top-level `Y.Array<frameMap>` — the body floating layer (`anchoredFrames`).
167
+ * Replaces the `meta.anchoredFrames` JSON blob; that meta key is read only
168
+ * as a migration fallback for docs seeded before this. */
169
+ export declare const Y_ANCHORED_FRAMES_KEY = "anchoredFrames";
170
+ /** Top-level `Y.Map<zoneId, Y.Array<frameMap>>` — per header/footer zone
171
+ * floating layers (`headerFooterFrames`). Migration fallback: the
172
+ * `meta.headerFooterFrames` JSON blob. */
173
+ export declare const Y_HEADER_FOOTER_FRAMES_KEY = "headerFooterFrames";
116
174
  /** Keys stored on the `meta` Y.Map. */
117
175
  export declare const Y_META_FIELDS: {
118
176
  readonly sections: "sections";
@@ -1,5 +1,5 @@
1
- import { Block, Paragraph, SobreeDocument } from '../doc/types';
2
- import * as Y from "yjs";
1
+ import { SobreeDocument } from '../doc/types';
2
+ import type * as Y from "yjs";
3
3
  /**
4
4
  * Populate a fresh (or reset) Y.Doc with the contents of a SobreeDocument.
5
5
  *
@@ -12,34 +12,3 @@ import * as Y from "yjs";
12
12
  * stack.
13
13
  */
14
14
  export declare function seedYDoc(ydoc: Y.Doc, doc: SobreeDocument, ids: readonly string[]): void;
15
- /**
16
- * Build a *skeleton* block Y.Map — id + kind discriminator + empty
17
- * containers, no content. Content lands in `populateBlockContent`
18
- * after the map is integrated. Used internally by `seedYDoc` and by
19
- * `applyDocumentToYDoc` (`buildBlockYMap` is a one-shot wrapper that
20
- * does both phases on an orphan map; only safe for callers that will
21
- * insert it inside a transact within the same call stack).
22
- */
23
- export declare function buildSkeletonBlockYMap(id: string, block: Block): Y.Map<unknown>;
24
- /** Populate the content of an *integrated* block Y.Map. */
25
- export declare function populateBlockContent(map: Y.Map<unknown>, block: Block): void;
26
- /**
27
- * Populate paragraph content. Caller guarantees the map is integrated
28
- * (so its child Y.Text is too).
29
- */
30
- export declare function populateParagraphContent(map: Y.Map<unknown>, block: Paragraph): void;
31
- /**
32
- * One-shot block Y.Map builder — skeleton + content in a single call.
33
- * Only safe inside a `Y.Doc.transact` block where the returned map
34
- * will be inserted into an integrated parent before any read of its
35
- * Y.Text. Otherwise prefer the two-phase builders above.
36
- *
37
- * Kept for backwards compat with callers that don't yet use the
38
- * two-phase pattern.
39
- */
40
- export declare function buildBlockYMap(id: string, block: Block): Y.Map<unknown>;
41
- /**
42
- * @deprecated Use `populateParagraphContent` (renamed for clarity).
43
- * Kept as a re-export so callers don't break during migration.
44
- */
45
- export declare const populateParagraphYMap: typeof populateParagraphContent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sobree/core",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },