@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 sobree.dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the “Software”), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @sobree/core
2
+
3
+ Embeddable, print-view-first WYSIWYG editor for `.docx`. Framework-free
4
+ core, plugin architecture, native OOXML round-trip.
5
+
6
+ → Docs: **[docs.sobree.dev](https://docs.sobree.dev)**
7
+ → Live editor: **[sobree.dev/try](https://sobree.dev/try)**
8
+
9
+ ## Install
10
+
11
+ `@sobree/core` is the **minimal editor kernel** — AST + paginator + DOCX
12
+ I/O + history + fonts. It ships with **zero plugin packages**. Install
13
+ the plugins you want and pass them to `createSobree()`.
14
+
15
+ For an interactive editor with toolbar, keyboard shortcuts, and zoom dock:
16
+
17
+ ```sh
18
+ pnpm add @sobree/core @sobree/keyboard @sobree/block-tools @sobree/zoom-controls
19
+ ```
20
+
21
+ For a headless / API-driven peer (LLM agent, automation):
22
+
23
+ ```sh
24
+ pnpm add @sobree/core yjs
25
+ ```
26
+
27
+ Use `HeadlessSobree` — the no-DOM counterpart of the browser editor.
28
+ For multi-user collab, add `@sobree/collab-providers` (client glue)
29
+ and run `@sobree/collab-server` somewhere.
30
+
31
+ ## Hello world
32
+
33
+ ```ts
34
+ import { createSobree } from "@sobree/core";
35
+ import { keyboard } from "@sobree/keyboard";
36
+ import { blockTools } from "@sobree/block-tools";
37
+ import { zoomControls } from "@sobree/zoom-controls";
38
+ import "@sobree/core/tokens.css";
39
+
40
+ const editor = createSobree("#editor", {
41
+ content: "# Hello\n\nStart typing.",
42
+ plugins: [
43
+ keyboard(), // Cmd+B / Cmd+Z / …
44
+ blockTools(), // floating toolbar + gutter indicator
45
+ zoomControls(), // bottom-right zoom dock
46
+ ],
47
+ });
48
+
49
+ editor.on("change", ({ doc }) => {
50
+ console.log("body has", doc.body.length, "blocks");
51
+ });
52
+ ```
53
+
54
+ `createSobree()` mounts the viewport, the paginated paper stack, runs each
55
+ plugin's `setup()` against a shared context, and returns a single handle.
56
+ Plugins are torn down in reverse-of-mount order on `editor.destroy()`.
57
+
58
+ ## What's in the box
59
+
60
+ - **Native OOXML AST.** Every node maps 1:1 to a `<w:…>` element.
61
+ - **Y.Doc backed.** Every editor is backed by a Yjs `Y.Doc`; mutations mirror in via `Y.Doc.transact`. Embedders attach `y-websocket` / `y-indexeddb` / `y-webrtc` providers for persistence + collaboration. `editor.ydoc` is the escape hatch.
62
+ - **Pure paginator.** TeX-style break selection, widow / orphan, keep-with-next, multi-section. No DOM, no I/O.
63
+ - **Per-peer undo / redo.** Thin wrapper around `Y.UndoManager` — each peer's `Cmd+Z` reverses only its own edits via tracked Y origins; remote edits flow through but stay off the local stack. Selection restored from `stackItem.meta`.
64
+ - **OOXML font embedding.** `word/fontTable.xml` round-trip + ODTTF obfuscation + OS/2 fsType licence check + runtime `@font-face` registration. Note: `fsType` is an advisory gate — the embedder is responsible for ensuring they have rights to ship any font they pass to `editor.embedFont(...)`. Pass `{ allowRestricted: true }` only when you do.
65
+ - **Minimal core, opt-in plugins.** Toolbar (`@sobree/block-tools`), keyboard (`@sobree/keyboard`), zoom dock (`@sobree/zoom-controls`) ship as siblings. For multi-user collab: `@sobree/collab-providers` (client) + `@sobree/collab-server` (Node relay). `@sobree/core` has no plugin dependencies — only `fflate` for ZIP and `yjs` for the CRDT.
66
+ - **Y-protocol IS the wire.** No separate RPC plugin. Headless callers (LLMs, automation) participate via `HeadlessSobree` — same commands, same Y.Doc.
67
+ - **Wire-ready surface.** `editor.commands.execute(name, args)` is the single dispatch path used by keyboard, toolbar, and external transports.
68
+ - **JSON-clean projection.** `editor.getDocument()` returns a JSON-clean `SobreeDocument` snapshot of the underlying Y.Doc — survives `structuredClone` and any wire.
69
+
70
+ ## Polymorphic content
71
+
72
+ ```ts
73
+ createSobree("#editor", { content: "# Markdown" }); // seed string
74
+ createSobree("#editor", { content: docxBlob }); // .docx bytes
75
+ createSobree("#editor", { content: astLiteral }); // built with the doc builders
76
+ createSobree("#editor"); // empty
77
+ ```
78
+
79
+ For the `.docx` path, `await editor.ready` resolves once the import lands.
80
+
81
+ ## Save back to `.docx`
82
+
83
+ ```ts
84
+ const { blob } = editor.toDocx();
85
+ ```
86
+
87
+ Or load a different file at runtime:
88
+
89
+ ```ts
90
+ const { warnings } = await editor.loadDocx(file);
91
+ ```
92
+
93
+ ## Documentation
94
+
95
+ - [Quick start](https://docs.sobree.dev/quick-start/)
96
+ - [`createSobree()` API](https://docs.sobree.dev/api/create-sobree/)
97
+ - [History (undo / redo)](https://docs.sobree.dev/api/history/)
98
+ - [Fonts (embedding + fontTable)](https://docs.sobree.dev/api/fonts/)
99
+ - [Architecture](https://docs.sobree.dev/concepts/architecture/)
100
+ - [Document model](https://docs.sobree.dev/concepts/document/)
101
+ - [Plugin model](https://docs.sobree.dev/concepts/plugins/)
102
+ - [Building your own plugin](https://docs.sobree.dev/plugins/build-your-own/)
103
+
104
+ ## License
105
+
106
+ MIT.
@@ -0,0 +1,5 @@
1
+ const e = {};
2
+ export {
3
+ e as default
4
+ };
5
+ //# sourceMappingURL=__vite-browser-external-DYxpcVy9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"__vite-browser-external-DYxpcVy9.js","sources":["../__vite-browser-external"],"sourcesContent":["export default {}"],"names":["__viteBrowserExternal"],"mappings":"AAAA,MAAAA,IAAe,CAAA;"}
@@ -0,0 +1,69 @@
1
+ import { BlobHash, BlobStore } from './types';
2
+ export interface BlobCacheOptions {
3
+ /** Underlying store. */
4
+ store: BlobStore;
5
+ /**
6
+ * Optional listener fired when a previously-missing hash arrives in
7
+ * the cache (background fetch landed). Editors use this to know
8
+ * when to re-render — a `<img>` whose bytes were pending now has
9
+ * something to show.
10
+ */
11
+ onResolved?: (hash: BlobHash) => void;
12
+ /**
13
+ * Optional listener fired when a fetch fails. The cache leaves the
14
+ * hash in "missing" state and lets a future `ensureLoaded` retry.
15
+ * Default: warn to console.
16
+ */
17
+ onError?: (hash: BlobHash, err: unknown) => void;
18
+ }
19
+ export declare class BlobCache {
20
+ private readonly store;
21
+ private readonly onResolved;
22
+ private readonly onError;
23
+ private readonly cache;
24
+ /** In-flight fetches, keyed by hash. Promise resolves to the bytes
25
+ * (cached) or `null` (not found / error). */
26
+ private readonly inflight;
27
+ constructor(opts: BlobCacheOptions);
28
+ /**
29
+ * Synchronously read bytes for a hash. Returns `null` if not yet
30
+ * cached. Doesn't trigger a fetch — call `ensureLoaded` first if
31
+ * you need to wait.
32
+ */
33
+ get(hash: BlobHash): Uint8Array | null;
34
+ /** Whether the hash is currently in the cache. */
35
+ has(hash: BlobHash): boolean;
36
+ /**
37
+ * Insert bytes into the cache directly. Used when the embedder
38
+ * already has the bytes in hand (paste image, font embed, DOCX
39
+ * import) and wants the local renderer to find them immediately.
40
+ *
41
+ * Returns the bytes' hash. Caller is responsible for uploading
42
+ * to the BlobStore separately (typically `await store.put(bytes)`
43
+ * with the same input).
44
+ */
45
+ put(hash: BlobHash, bytes: Uint8Array): void;
46
+ /**
47
+ * Ensure the given hashes are in the cache. Returns a Promise that
48
+ * resolves once every hash is either in the cache or has failed to
49
+ * fetch (failures don't reject the Promise — they're reported via
50
+ * `onError` and skipped, consistent with the "best-effort
51
+ * availability" model).
52
+ *
53
+ * Already-cached hashes resolve immediately. Already-in-flight
54
+ * fetches are deduplicated (multiple concurrent callers wait on
55
+ * the same Promise).
56
+ */
57
+ ensureLoaded(hashes: readonly BlobHash[]): Promise<void>;
58
+ /**
59
+ * Number of cached blobs. Diagnostic / test helper; production
60
+ * code shouldn't depend on this for correctness.
61
+ */
62
+ size(): number;
63
+ /**
64
+ * Clear all cached blobs. Used by the Editor on `destroy` to free
65
+ * memory; future refinements may auto-evict.
66
+ */
67
+ clear(): void;
68
+ private fetchOne;
69
+ }
@@ -0,0 +1,18 @@
1
+ import { BlobStore } from './types';
2
+ export interface FetchBlobStoreOptions {
3
+ /** Base URL — bytes are addressed at `<baseUrl>/<hash>`. No trailing slash. */
4
+ baseUrl: string;
5
+ /**
6
+ * Optional headers factory. Called per request — embed auth tokens,
7
+ * trace IDs, custom content types, etc. Defaults to no extra headers.
8
+ */
9
+ headers?: () => Record<string, string> | Promise<Record<string, string>>;
10
+ /** Pass through to the underlying `fetch` (CORS mode, signal, etc.). */
11
+ fetchInit?: RequestInit;
12
+ /**
13
+ * Override the global `fetch`. Useful for testing or running inside
14
+ * a runtime with a custom fetch impl. Default: `globalThis.fetch`.
15
+ */
16
+ fetch?: typeof fetch;
17
+ }
18
+ export declare function fetchBlobStore(opts: FetchBlobStoreOptions): BlobStore;
@@ -0,0 +1,13 @@
1
+ import { BlobHash } from './types';
2
+ /**
3
+ * Compute the SHA-256 hex digest of `bytes`. Lowercase hex, 64 chars.
4
+ *
5
+ * Always returns a Promise — the WebCrypto API is async, and we
6
+ * shape the Node path the same way for API consistency.
7
+ */
8
+ export declare function sha256Hex(bytes: Uint8Array): Promise<BlobHash>;
9
+ /**
10
+ * Validate that a string looks like a SHA-256 hex digest. Used as a
11
+ * defensive guard when bytes come from the wire.
12
+ */
13
+ export declare function isBlobHash(s: string): boolean;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @sobree/core blob module — content-hashed binary parts.
3
+ *
4
+ * Without configuring a `BlobStore`, Sobree behaves as it always has:
5
+ * binary parts (images, fonts) live inline in `SobreeDocument.rawParts`
6
+ * and inside the Y.Doc's `parts: Y.Map<Uint8Array>`.
7
+ *
8
+ * With a `BlobStore` configured on `createSobree({ blobStore })`,
9
+ * Sobree shifts to *content-hashed* mode: bytes go to the BlobStore
10
+ * (the side channel) and the Y.Doc stores only SHA-256 hashes
11
+ * (`partRefs: Y.Map<string, string>`). Y updates stay small — a
12
+ * 5 MB image paste no longer means 5 MB on every peer's Y.Doc.
13
+ *
14
+ * Two reference implementations ship out of the box:
15
+ *
16
+ * - `inMemoryBlobStore()` — tests, local-only deployments
17
+ * - `fetchBlobStore({ baseUrl, headers })` — HTTP backend
18
+ *
19
+ * For production at scale, write a custom `BlobStore` against S3 /
20
+ * R2 / Postgres / Redis. The interface is three methods.
21
+ *
22
+ * The `BlobCache` class bridges the async `BlobStore` and the
23
+ * synchronous editor renderer — pre-fetch with `ensureLoaded`, read
24
+ * synchronously after.
25
+ */
26
+ export type { BlobHash, BlobStore } from './types';
27
+ export { BlobStoreError } from './types';
28
+ export { sha256Hex, isBlobHash } from './hash';
29
+ export { inMemoryBlobStore } from './memory';
30
+ export { fetchBlobStore } from './fetch';
31
+ export type { FetchBlobStoreOptions } from './fetch';
32
+ export { BlobCache } from './cache';
33
+ export type { BlobCacheOptions } from './cache';
@@ -0,0 +1,2 @@
1
+ import { BlobStore } from './types';
2
+ export declare function inMemoryBlobStore(): BlobStore;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * BlobStore — the side-channel where binary parts live when an
3
+ * embedder configures Sobree for content-hashed assets (Phase 3.2+).
4
+ *
5
+ * # The contract
6
+ *
7
+ * Tiny, intentionally minimal. Three operations: `put` (returns the
8
+ * content hash under which bytes are stored), `get` (fetches by
9
+ * hash), and optional `has` (cheap existence check). Every reasonable
10
+ * backend — in-memory, IndexedDB, HTTP, S3, R2, Cloudflare Workers,
11
+ * Postgres LO, Redis — implements this.
12
+ *
13
+ * # Why content addressing
14
+ *
15
+ * Two reasons:
16
+ *
17
+ * 1. **Deduplication.** A 5 MB logo pasted into 100 docs lives once
18
+ * in the blob store, not 100 times. Hashes collide on identity,
19
+ * so identical bytes produce identical keys.
20
+ * 2. **Tamper resistance.** A peer can verify any fetched blob by
21
+ * re-hashing — no trust in the storage layer required for
22
+ * integrity. (Confidentiality is a separate concern; encrypt at
23
+ * the transport / storage layer if you need it.)
24
+ *
25
+ * # Hash algorithm
26
+ *
27
+ * Sobree uses SHA-256 hex (lowercase, no separators) for blob
28
+ * addressing. 64 chars per hash. Sized for the next decade or two of
29
+ * web content; not so long that hashes are awkward to log or shove
30
+ * into a URL path.
31
+ *
32
+ * # Concurrency
33
+ *
34
+ * Implementations should be safe for concurrent `put` of the same
35
+ * content (multiple callers, same bytes → same hash, idempotent).
36
+ * `get(hash)` must return the same bytes that were `put`.
37
+ */
38
+ /** A SHA-256 hex digest. 64 lowercase hex chars. */
39
+ export type BlobHash = string;
40
+ export interface BlobStore {
41
+ /**
42
+ * Upload bytes and return their content hash. Idempotent — calling
43
+ * `put` with the same bytes returns the same hash and is a no-op
44
+ * after the first call.
45
+ */
46
+ put(bytes: Uint8Array): Promise<BlobHash>;
47
+ /**
48
+ * Fetch bytes by hash. Returns `null` when the blob isn't present
49
+ * (e.g. another peer wrote the partRef but the bytes haven't
50
+ * propagated yet). Implementations should NOT throw on "not
51
+ * found" — return null.
52
+ */
53
+ get(hash: BlobHash): Promise<Uint8Array | null>;
54
+ /**
55
+ * Optional cheap existence check — returns `true` if a blob with
56
+ * the given hash exists, without transferring its content. Used by
57
+ * `BlobCache` to decide whether to schedule a fetch. Falls back to
58
+ * `(await get(hash)) !== null` if not provided.
59
+ */
60
+ has?(hash: BlobHash): Promise<boolean>;
61
+ /**
62
+ * Optional removal. Most production deployments don't expose this
63
+ * over the wire (blob deletion needs care: ref counting, garbage
64
+ * collection, distributed coordination). Provided for tests and
65
+ * local-only deployments where the embedder owns lifecycle.
66
+ */
67
+ delete?(hash: BlobHash): Promise<void>;
68
+ }
69
+ /**
70
+ * BlobStoreError is thrown for low-level failures (transport,
71
+ * authorization, integrity check). "Not found" is *not* an error —
72
+ * `get` returns `null` for that case so callers can distinguish.
73
+ */
74
+ export declare class BlobStoreError extends Error {
75
+ /** Originating exception, network response, etc. */
76
+ readonly cause?: unknown | undefined;
77
+ constructor(message: string,
78
+ /** Originating exception, network response, etc. */
79
+ cause?: unknown | undefined);
80
+ }
@@ -0,0 +1,132 @@
1
+ import { Sobree, SobreeEvent, SobreeEventPayload, SobreeUnsubscribe } from './sobree';
2
+ import { Editor, CommandBus } from './editor';
3
+ import { Viewport } from './embed/viewport';
4
+ import { SobreePlugin } from './plugin';
5
+ import { PageSetup } from './paperStack/pageSetup';
6
+ import { SobreeDocument } from './doc/types';
7
+ /**
8
+ * Initial content. Type detection is automatic:
9
+ * - `string` → seed-quality Markdown (see `parseMarkdown`)
10
+ * - `Blob` / `File` → `.docx` bytes (loaded asynchronously)
11
+ * - `ArrayBuffer` → `.docx` bytes
12
+ * - `Uint8Array` → `.docx` bytes
13
+ * - `SobreeDocument` → AST literal (use the builders to construct)
14
+ *
15
+ * For a `.docx` source, the constructor returns synchronously with an
16
+ * empty editor and `.ready` resolves once the import lands. For all
17
+ * other source types, `.ready` is already resolved when the factory
18
+ * returns.
19
+ */
20
+ export type SobreeContent = string | Blob | ArrayBuffer | Uint8Array | SobreeDocument;
21
+ /**
22
+ * How the viewport is fitted on initial mount.
23
+ * - `"width"` (default) — first paper fills the host width; you see
24
+ * the document the way you'd read it.
25
+ * - `"page"` — first paper is fully contained (whole page visible).
26
+ * - `"none"` — leave the viewport at 1:1, no auto-fit.
27
+ */
28
+ export type FitOnMount = "width" | "page" | "none";
29
+ export interface CreateSobreeOptions {
30
+ /** Initial content. See `SobreeContent`. Default: empty document. */
31
+ content?: SobreeContent;
32
+ /** Page setup. Falls back to A4 portrait with 1in margins. */
33
+ pageSetup?: PageSetup;
34
+ /**
35
+ * Plugins to mount. Each receives a `PluginContext` (editor +
36
+ * viewport + sobree + host) on `setup()` and returns a destroyer.
37
+ * Mounted in array order; destroyed in reverse on
38
+ * `editor.destroy()`. See `@sobree/keyboard`, `@sobree/block-tools`,
39
+ * `@sobree/zoom-controls` for stock factories. (`@sobree/collab-providers`
40
+ * lands in Phase 2 for Yjs persistence / collaboration.)
41
+ */
42
+ plugins?: SobreePlugin[];
43
+ /** Forwarded to the underlying Editor. */
44
+ changeDebounceMs?: number;
45
+ /**
46
+ * Y.Doc backing the document. The editor mirrors every mutation into
47
+ * this Y.Doc; embedders attach providers (`y-websocket`,
48
+ * `y-indexeddb`, `y-webrtc`, …) for persistence / collaboration.
49
+ * If absent, the editor creates one internally — still observable
50
+ * via `editor.editor.ydoc`.
51
+ */
52
+ ydoc?: import('yjs').Doc;
53
+ /**
54
+ * Optional content-hashed `BlobStore` for binary parts (Phase 3.2+).
55
+ * Without one, binary parts (images, fonts) ride inline in the
56
+ * Y.Doc; with one, they go through the BlobStore and the Y.Doc
57
+ * carries only hashes. See `EditorOptions.blobStore` for the full
58
+ * contract.
59
+ */
60
+ blobStore?: import('./blob').BlobStore;
61
+ /**
62
+ * Auto-fit the viewport to the first paper after mount. Default
63
+ * `"width"` — what most embedders want for a "looks right out of the
64
+ * box" first impression. Pass `"none"` if you're driving the
65
+ * viewport yourself.
66
+ */
67
+ fitOnMount?: FitOnMount;
68
+ }
69
+ /**
70
+ * The editor handle returned by `createSobree()`. Proxies the most-used
71
+ * methods of the underlying `Sobree` + `Editor` so embedders rarely
72
+ * need to reach through. Power users can use `.sobree` / `.editor` /
73
+ * `.viewport` directly.
74
+ *
75
+ * Plugin instances are NOT exposed on the handle — plugins self-manage
76
+ * after handoff. Reach through `.editor` for command bus / events.
77
+ */
78
+ export interface SobreeHandle {
79
+ readonly sobree: Sobree;
80
+ readonly editor: Editor;
81
+ readonly viewport: Viewport;
82
+ /**
83
+ * The Y.Doc backing the document. Provided so embedders can attach
84
+ * Yjs providers (`y-websocket`, `y-indexeddb`, `y-webrtc`) for
85
+ * persistence / collaboration without reaching through `editor.editor`.
86
+ * Same reference as `editor.editor.ydoc`.
87
+ */
88
+ readonly ydoc: import('yjs').Doc;
89
+ /**
90
+ * Resolves when the constructor's `content` has finished loading. For
91
+ * docx content this awaits the import; for everything else this is an
92
+ * already-resolved promise.
93
+ */
94
+ readonly ready: Promise<{
95
+ warnings: string[];
96
+ }>;
97
+ getDocument(): SobreeDocument;
98
+ setDocument(doc: SobreeDocument): void;
99
+ /** Replace the document with one parsed from a Markdown string (seed-quality). */
100
+ loadMarkdown(md: string): void;
101
+ /** Load a `.docx` file. Resolves with any import warnings. */
102
+ loadDocx(src: File | Blob | ArrayBuffer | Uint8Array): Promise<{
103
+ warnings: string[];
104
+ }>;
105
+ /**
106
+ * Serialise the current document to a `.docx` Blob. Uses bytes
107
+ * currently cached locally — if a `blobStore` is configured and
108
+ * some referenced parts aren't yet cached, call
109
+ * `await handle.ensurePartsLoaded()` first to pre-fetch them.
110
+ * Missing parts appear in `warnings`.
111
+ */
112
+ toDocx(): {
113
+ blob: Blob;
114
+ warnings: string[];
115
+ };
116
+ /**
117
+ * Wait for every currently-referenced binary part to be available
118
+ * in the local cache. No-op when no `blobStore` is configured.
119
+ */
120
+ ensurePartsLoaded(): Promise<void>;
121
+ getPageSetup(): PageSetup;
122
+ setPageSetup(partial: Partial<PageSetup>): void;
123
+ readonly commands: CommandBus;
124
+ on<E extends SobreeEvent>(event: E, cb: (payload: SobreeEventPayload[E]) => void): SobreeUnsubscribe;
125
+ destroy(): void;
126
+ }
127
+ /**
128
+ * Mount a fully-wired Sobree editor (Viewport + Sobree + user plugins)
129
+ * into `target` (CSS selector or HTMLElement). See `CreateSobreeOptions`
130
+ * and `SobreeHandle` for the full surface.
131
+ */
132
+ export declare function createSobree(target: string | HTMLElement, options?: CreateSobreeOptions): SobreeHandle;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * API-level types for talking to the Editor.
3
+ *
4
+ * These describe HOW you address content (positions, ranges, selections)
5
+ * and what guarantees you get back (block versions, edit results). They
6
+ * are JSON-clean so the same shapes work for in-process callers, the
7
+ * forthcoming WebSocket / MCP adapter, and tests.
8
+ *
9
+ * Kept separate from `src/doc/types.ts` (the AST itself) because these
10
+ * are runtime concerns — none of them are persisted to .docx.
11
+ *
12
+ * Design note on why there's no polymorphic `Position` type:
13
+ * - For block-scoped operations (insert / replace / delete) the input
14
+ * is a `BlockRef` paired with a method name that spells out the
15
+ * intent (`insertBlockBefore`, `insertBlockAfter`).
16
+ * - For inline-scoped operations (insert a run, move caret, apply
17
+ * attributes across a span) the input is an `InlinePosition` — a
18
+ * block ref plus a character offset inside that block.
19
+ * This keeps method signatures self-describing for LLM / MCP callers,
20
+ * no discriminated unions to construct.
21
+ */
22
+ /**
23
+ * Stable handle to a block, valid for the lifetime of an Editor instance.
24
+ * `id` is allocated by the Editor on first sight (sequential strings like
25
+ * `"b1"`, `"b2"`); `version` bumps on every modification. Versions reset
26
+ * to 0 when the document is reloaded (`setDocument`, `openDocx`).
27
+ *
28
+ * Pass this to mutating operations to opt in to optimistic locking. The
29
+ * operation fails with `"optimistic-lock"` if the live version no longer
30
+ * matches.
31
+ */
32
+ export interface BlockRef {
33
+ id: string;
34
+ version: number;
35
+ }
36
+ /** Version map for blocks an operation should also lock-check. */
37
+ export type BlockExpectations = Record<string, number>;
38
+ /**
39
+ * Address a point inside a block's content.
40
+ *
41
+ * `offset = 0` is the start of the block's content; `offset = blockLength`
42
+ * is the end. Non-text inlines (drawings, fields, hyperlinks) count as
43
+ * 1 each. A caret always lives inside a block — "before block N" is
44
+ * expressed as `{ block: blockN, offset: 0 }`; "after block N" is
45
+ * `{ block: blockN, offset: blockLength }`. For inserting NEW blocks
46
+ * adjacent to an existing one, use the `insertBlockBefore` /
47
+ * `insertBlockAfter` methods directly — they take a `BlockRef`, not a
48
+ * position.
49
+ */
50
+ export interface InlinePosition {
51
+ block: BlockRef;
52
+ offset: number;
53
+ }
54
+ /**
55
+ * Inclusive range from `from` to `to` — both inline positions. Single-
56
+ * block ranges have `from.block.id === to.block.id`.
57
+ *
58
+ * For operations that span blocks, pass `opts.expect` with the middle
59
+ * blocks' expected versions — the Range itself only pins the endpoints.
60
+ */
61
+ export interface Range {
62
+ from: InlinePosition;
63
+ to: InlinePosition;
64
+ }
65
+ /**
66
+ * The current cursor / selection state of the editor in model terms.
67
+ * `null` when nothing is selected (e.g. focus is outside the editor).
68
+ */
69
+ export type Selection = null | {
70
+ kind: "caret";
71
+ at: InlinePosition;
72
+ } | {
73
+ kind: "range";
74
+ range: Range;
75
+ };
76
+ /** Result envelope returned by every mutating operation. */
77
+ export type EditResult<T = void> = {
78
+ ok: true;
79
+ value: T;
80
+ affected: BlockRef[];
81
+ } | {
82
+ ok: false;
83
+ error: EditError;
84
+ };
85
+ /** Why an edit didn't apply. Discriminated union — agents switch on `code`. */
86
+ export type EditError = OptimisticLockError | {
87
+ code: "invalid-position";
88
+ details: string;
89
+ } | {
90
+ code: "unknown-block";
91
+ blockId: string;
92
+ } | {
93
+ code: "range-empty";
94
+ details: string;
95
+ } | {
96
+ code: "range-out-of-order";
97
+ details: string;
98
+ } | {
99
+ code: "invalid-state";
100
+ details: string;
101
+ };
102
+ export interface OptimisticLockError {
103
+ code: "optimistic-lock";
104
+ /**
105
+ * Per-block conflict info. `actual` is `null` when the block has been
106
+ * deleted between read and write — distinguishable from a stale
107
+ * version on the same block.
108
+ */
109
+ conflicts: Array<{
110
+ blockId: string;
111
+ expected: number;
112
+ actual: number | null;
113
+ }>;
114
+ }
115
+ /** Build an inline position inside `block` at the given character offset. */
116
+ export declare function inlineAt(block: BlockRef, offset: number): InlinePosition;
117
+ /** Construct a range from two positions. */
118
+ export declare function makeRange(from: InlinePosition, to: InlinePosition): Range;
119
+ /** Caret at a single position. */
120
+ export declare function caretAt(at: InlinePosition): Selection;
121
+ /** True if two inline positions are inside the same block (by id). */
122
+ export declare function sameBlock(a: InlinePosition, b: InlinePosition): boolean;
123
+ /** Zero-width range — `from` and `to` collapsed onto the same offset. */
124
+ export declare function isCollapsedRange(range: Range): boolean;
125
+ /** Caret selection OR collapsed range. */
126
+ export declare function isCaret(sel: Selection): boolean;
127
+ /** Build an `EditResult` for a successful operation. */
128
+ export declare function ok<T>(value: T, affected?: BlockRef[]): EditResult<T>;
129
+ /** Build an `EditResult` for a failed operation. */
130
+ export declare function fail(error: EditError): EditResult<never>;
131
+ /** Specialised builder for the common optimistic-lock failure case. */
132
+ export declare function lockConflict(conflicts: OptimisticLockError["conflicts"]): EditResult<never>;
@@ -0,0 +1,42 @@
1
+ import { Block, HeaderFooterRef, InlineRun, NamedStyle, PageMargins, PageSize, Paragraph, ParagraphProperties, RunProperties, SectionProperties, SobreeDocument, Table, TextRun } from './types';
2
+ /** A new, blank document with an A4 portrait section and the standard styles. */
3
+ export declare function emptyDocument(): SobreeDocument;
4
+ export declare function defaultSection(): SectionProperties;
5
+ export declare function defaultPageSize(): PageSize;
6
+ export declare function defaultMargins(): PageMargins;
7
+ /** A paragraph with no runs and no properties beyond the defaults. */
8
+ export declare function paragraph(runs?: InlineRun[], properties?: ParagraphProperties): Paragraph;
9
+ /** Heading paragraph (`Heading{level}` style, level clamped to 1..6). */
10
+ export declare function heading(level: number, runs?: InlineRun[], extra?: ParagraphProperties): Paragraph;
11
+ /** Plain text run with optional formatting. */
12
+ export declare function text(value: string, properties?: RunProperties): TextRun;
13
+ /** Convenience: emphasised (bold + italic) text run. */
14
+ export declare function emphasis(value: string, properties?: RunProperties): TextRun;
15
+ export declare function strong(value: string, properties?: RunProperties): TextRun;
16
+ export declare function softBreak(): InlineRun;
17
+ export declare function pageBreak(): InlineRun;
18
+ /** Default Word styles every doc declares so headings render correctly.
19
+ *
20
+ * Carries WORD-HARDCODED-DEFAULT typography — i.e. what Word uses
21
+ * when a docx has bare/empty styles.xml entries. That means single
22
+ * line spacing (1.0×) and zero space-before / space-after on Normal.
23
+ * Headings keep their bold + size on `runDefaults` but DON'T add
24
+ * spacing-before/after either — the original docx tells the renderer
25
+ * via per-paragraph `spacing` properties when something needs to
26
+ * breathe.
27
+ *
28
+ * This is the load-bearing constraint: a docx round-trip must not
29
+ * silently add (or remove) vertical rhythm the original didn't ask
30
+ * for. Embedders that want a different baseline (e.g. markdown output
31
+ * with 8pt-after for visible paragraph separation) override the
32
+ * Normal style on the document they construct — see `parseMarkdown`.
33
+ *
34
+ * Heading scale steps down from a prominent H1 at 24pt to body-sized
35
+ * H6 at 11pt. */
36
+ export declare function defaultStyles(): NamedStyle[];
37
+ /** Push a block onto the document body. Mutates `doc` and returns it. */
38
+ export declare function appendBlock(doc: SobreeDocument, block: Block): SobreeDocument;
39
+ /** Allocate a new header/footer reference id. Pure helper. */
40
+ export declare function makeHeaderFooterRef(type: "default" | "first" | "even", partId: string): HeaderFooterRef;
41
+ export declare function isParagraph(block: Block): block is Paragraph;
42
+ export declare function isTable(block: Block): block is Table;