@quillmark/wasm 0.96.0 → 0.97.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.97.0 - 2026-07-24
4
+
5
+ - core: one take_item primitive behind the three payload removers
6
+ - core: fold the incoming-card guard shared by push_card/insert_card
7
+ - tests: dense-prose fix on a muddy reinstatement comment
8
+ - canon/docs: §Card-id identity — the third twin; wasm surface follows
9
+ - core: card $id is the durable handle — unique per document, guarded
10
+ - wasm: add island/mark discriminant guards; USV docs on mapPos/rebase
11
+ - ci: lint the wasm binding's rustdoc to catch intra-doc breakage
12
+ - Canonize anchor-id policy: caller-supplied, unique, invariant
13
+ - Fix stale resolve() docs: body is a sibling, not a `$body` row
14
+ - docs: prune per pruning pass — archive old migrations, cut duplication
15
+ - docs: public integration pages for the high/medium doc gaps
16
+ - docs: dense-prose sweep and accuracy fixes in docs/ and canon
17
+ - docs(wasm): fix broken [`Delta`] intra-doc links (#1034)
18
+ - wasm: rename Document.get → getStored so the verbatim read carries its lane
19
+
20
+
3
21
  ## v0.96.0 - 2026-07-23
4
22
 
5
23
  - docs(spec): keep plate JSON out of the markdown spec
package/README.md CHANGED
@@ -255,9 +255,10 @@ Build a fresh card from a flat field map with
255
255
  `{ card?, field? }`, absent `card` = main, absent `field` = body — and a bare
256
256
  string is shorthand for `{ field }`. So `doc.storeField("qty", 3)` targets the
257
257
  main card's `qty`, `doc.storeField({ card: 2, field: "qty" }, 3)` a composable
258
- card's. Reads are total over the field axis (`get` → `undefined`, `isFill` → `false` for
258
+ card's. Reads are total over the field axis (`getStored` → `undefined`, `isFill` → `false` for
259
259
  an absent field; only an out-of-range card throws); field writes throw on a body
260
- address. `getMarkdown` is the body markdown read (a `CardAddr`; a field's
260
+ address. `getStored` is the verbatim transport read, distinct from the interpreted
261
+ `quill.reader(doc).get`; `getMarkdown` is the body markdown read (a `CardAddr`; a field's
261
262
  markdown is read through `quill.reader(doc).get(field)`). Card-scoped verbs take a
262
263
  `CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
263
264
 
@@ -102,7 +102,14 @@ export type ContentContainer =
102
102
  | { container: "list_item"; ordered: boolean; start: number; ordinal: number }
103
103
  | { container: "quote" };
104
104
 
105
- /** A mark over char range `[start, end)` into `Content.text`. */
105
+ /** A mark over char range `[start, end)` into `Content.text`. The open `type`
106
+ * arm blocks discriminant narrowing (as on `ContentIsland`), so read a
107
+ * payload-carrying arm behind its guard — `isLinkMark` (`url`) / `isAnchorMark`
108
+ * (`id`), from `@quillmark/wasm/runtime`; the bare arms carry no payload. An
109
+ * `anchor`'s `id` is a caller-supplied, opaque handle, unique per `Content` and
110
+ * invariant while the mark lives (positions rebase, the id never does); it has no
111
+ * markdown projection and survives only through the edit lane. See DOCUMENT_STORAGE
112
+ * § Anchor-id identity. */
106
113
  export type ContentMark = { start: number; end: number } & (
107
114
  | { type: "strong" | "emph" | "underline" | "strike" | "code" }
108
115
  | { type: "link"; url: string }
@@ -138,7 +145,8 @@ export interface ImageProps {
138
145
  * open set: the engine pins `props` as `TableProps` for `table` and `ImageProps`
139
146
  * for `image`; an island of any other type round-trips with opaque `props`. Like
140
147
  * `ContentMark`, the open `type` arm means a discriminant check does not itself
141
- * narrow `props` — key off `type` and read `props` as the matching shape. */
148
+ * narrow `props` — read `props` as the matching shape behind the `isTableIsland` /
149
+ * `isImageIsland` guards (from `@quillmark/wasm/runtime`), which narrow it. */
142
150
  export type ContentIsland = {
143
151
  id: string;
144
152
  /** How faithfully the markdown projection can carry this island. */
@@ -191,7 +199,10 @@ export type Assoc = "before" | "after";
191
199
  /**
192
200
  * A mark edit in final-text coordinates (post-delta, post-line-op). `add` /
193
201
  * `remove` carry the `ContentMark` vocabulary (`{ type, … }`); `removeAnchor`
194
- * drops one identity anchor by id.
202
+ * drops one identity anchor by id. An `add` of an `anchor` requires a non-empty
203
+ * `id` not already live in the field — a collision or the empty id throws
204
+ * (ids are caller-supplied and unique per `Content`; DOCUMENT_STORAGE
205
+ * § Anchor-id identity).
195
206
  */
196
207
  export type MarkOp =
197
208
  | ({ op: "add" | "remove"; start: number; end: number } & (
@@ -580,10 +591,10 @@ export class Document {
580
591
  */
581
592
  card(index: number): Card;
582
593
  /**
583
- * The index of the first composable card whose `$id` equals `id`, or
584
- * `undefined` when none carries it. Resolves the canonical durable address
594
+ * The index of the composable card whose `$id` equals `id`, or
595
+ * `undefined` when none carries it. Resolves the durable card handle
585
596
  * without a hand-rolled scan over [`cards`](Self::cards); `$id` is
586
- * non-unique by design, so the first match wins.
597
+ * unique per document, so at most one card matches.
587
598
  */
588
599
  cardIndexById(id: string): number | undefined;
589
600
  clone(): Document;
@@ -625,17 +636,6 @@ export class Document {
625
636
  * Parse markdown into a typed Document. Throws on parse errors.
626
637
  */
627
638
  static fromMarkdown(markdown: string): Document;
628
- /**
629
- * Read the value at `addr` — the raw stored payload value of a field (a
630
- * content object for a richtext field, a scalar/array/object otherwise), or
631
- * the **body content** when `addr.field` is absent. A bare string is `Addr`
632
- * shorthand for `{ field }`. Reads are total over the field axis: an absent
633
- * field is `undefined`; only an out-of-range `addr.card` throws
634
- * `edit::index_out_of_range`. Reads need no schema, so they live on
635
- * `Document`, not the typed writer; for the markdown projection of a
636
- * richtext value use [`getMarkdown`](Self::get_markdown).
637
- */
638
- get(addr: Addr | string): unknown;
639
639
  /**
640
640
  * The whole `$ext` map at `addr` (a card address, absent `card` = main), or
641
641
  * `undefined` when the card carries none. The fine-grained `$ext` read —
@@ -662,6 +662,19 @@ export class Document {
662
662
  * type (#978). An out-of-range `addr.card` throws.
663
663
  */
664
664
  getMarkdown(addr?: CardAddr): string;
665
+ /**
666
+ * Read the **verbatim stored value** at `addr` — the raw payload value of a
667
+ * field (a content object for a richtext field, a scalar/array/object
668
+ * otherwise), or the **body content** when `addr.field` is absent. A bare
669
+ * string is `Addr` shorthand for `{ field }`. Reads are total over the field
670
+ * axis: an absent field is `undefined`; only an out-of-range `addr.card`
671
+ * throws `edit::index_out_of_range`. Needs no schema, so it lives on
672
+ * `Document` — the read echo of the verbatim `store*` write, distinct from
673
+ * the interpreted schema-plane [`reader.get`](Self::reader_get). For the
674
+ * markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
675
+ * `reader.get` (a field's declared type).
676
+ */
677
+ getStored(addr: Addr | string): unknown;
665
678
  /**
666
679
  * Insert a card — the single insertion verb: `at` absent appends, a number
667
680
  * inserts at that index (must be in `0..=cards.length`). Accepts a
@@ -762,7 +775,7 @@ export class Document {
762
775
  removeSeedNamespace(card_kind: string): any;
763
776
  /**
764
777
  * **Revise** the richtext value at `addr` from a markdown string — **edit
765
- * semantics**, the default write path, returning the text [`Delta`]. Imports
778
+ * semantics**, the default write path, returning the text `Delta`. Imports
766
779
  * the markdown, diffs it against the current value, rebases surviving
767
780
  * identity anchors, and returns the change an editor bridge maps its own
768
781
  * positions through (`mapPos`). An absent `addr.field` targets the body, an
@@ -1029,7 +1042,8 @@ export class Quill {
1029
1042
  * The resolved-value view of `doc` against this quill's schema — for every
1030
1043
  * declared field the value the render projection would use and the
1031
1044
  * `FieldSource` rung it came from (`"authored" | "default" | "zero"`), in
1032
- * one call. The card body rides the `fields` map under the `$body` key.
1045
+ * one call. The card body is a `body` sibling on its card (row `name`
1046
+ * `"body"`), never a row in `fields` — `null` when the kind enables no body.
1033
1047
  *
1034
1048
  * Value and provenance only: completeness and errors stay `validate`'s
1035
1049
  * (a consumer merges it with its own diagnostic producers regardless), and
@@ -1178,10 +1192,11 @@ export function importMarkdown(markdown: string): Content;
1178
1192
  export function init(): void;
1179
1193
 
1180
1194
  /**
1181
- * Map a base content position through a `delta` to its new position the pure
1182
- * position-mapping codec an editor bridge composes to hold a caret stable
1183
- * across a `revise`. `assoc` decides the side of a same-position insertion
1184
- * (`"after"` moves past it). Throws on a malformed `delta`.
1195
+ * Map a base content position a USV index into `Content.text`, not a UTF-16
1196
+ * offset through a `delta` to its new USV position: the pure position-mapping
1197
+ * codec an editor bridge composes to hold a caret stable across a `revise`.
1198
+ * `assoc` decides the side of a same-position insertion (`"after"` moves past
1199
+ * it). Throws on a malformed `delta`.
1185
1200
  */
1186
1201
  export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
1187
1202
 
@@ -1197,8 +1212,9 @@ export function parseDocPath(path: string): DocPathSeg[];
1197
1212
  /**
1198
1213
  * Rebase `markdown` onto a `base` content — the pure, document-free twin of
1199
1214
  * `revise`: cold-import + `diff_import`, returning the new `content` and the
1200
- * text `delta` (surviving anchors rebased). Use it to compute a revise without
1201
- * a document in hand; `revise(addr, md)` fuses this with the store for
1202
- * atomicity. Throws on an over-nested markdown input or a non-content `base`.
1215
+ * text `delta` (its offsets USV indices into `Content.text`, surviving anchors
1216
+ * rebased). Use it to compute a revise without a document in hand; `revise(addr,
1217
+ * md)` fuses this with the store for atomicity. Throws on an over-nested
1218
+ * markdown input or a non-content `base`.
1203
1219
  */
1204
1220
  export function rebase(base: Content, markdown: string): { content: Content; delta: Delta };
@@ -131,7 +131,7 @@ export class Document {
131
131
  /**
132
132
  * Interpreted read at `addr`, resolving the field's declared `type` from
133
133
  * `quill` — the stable ABI under the runtime `reader.get` / `reader.card(i).get`.
134
- * The schema-plane twin of the quill-free [`get`](Self::get): a `richtext`
134
+ * The schema-plane twin of the quill-free [`getStored`](Self::get_stored): a `richtext`
135
135
  * field returns its markdown projection, every other declared type its
136
136
  * canonical value verbatim, so a consumer holding the quill reads by field
137
137
  * meaning rather than by wire shape.
@@ -176,7 +176,7 @@ export class Document {
176
176
  * surviving anchors rebase (as [`revise`](Self::revise)), then the diffed
177
177
  * result is schema-conformed, so a `richtext(inline)` field rejects a
178
178
  * multi-block result with `edit::field_richtext_not_inline`. Returns the
179
- * text [`Delta`].
179
+ * text `Delta`.
180
180
  *
181
181
  * `addr` must name a field (a bare string is `{ field }`); a body address
182
182
  * throws (a body carries no field schema — use [`revise`](Self::revise)). A
@@ -290,10 +290,10 @@ export class Document {
290
290
  return ret >>> 0;
291
291
  }
292
292
  /**
293
- * The index of the first composable card whose `$id` equals `id`, or
294
- * `undefined` when none carries it. Resolves the canonical durable address
293
+ * The index of the composable card whose `$id` equals `id`, or
294
+ * `undefined` when none carries it. Resolves the durable card handle
295
295
  * without a hand-rolled scan over [`cards`](Self::cards); `$id` is
296
- * non-unique by design, so the first match wins.
296
+ * unique per document, so at most one card matches.
297
297
  * @param {string} id
298
298
  * @returns {number | undefined}
299
299
  */
@@ -457,33 +457,6 @@ export class Document {
457
457
  wasm.__wbindgen_add_to_stack_pointer(16);
458
458
  }
459
459
  }
460
- /**
461
- * Read the value at `addr` — the raw stored payload value of a field (a
462
- * content object for a richtext field, a scalar/array/object otherwise), or
463
- * the **body content** when `addr.field` is absent. A bare string is `Addr`
464
- * shorthand for `{ field }`. Reads are total over the field axis: an absent
465
- * field is `undefined`; only an out-of-range `addr.card` throws
466
- * `edit::index_out_of_range`. Reads need no schema, so they live on
467
- * `Document`, not the typed writer; for the markdown projection of a
468
- * richtext value use [`getMarkdown`](Self::get_markdown).
469
- * @param {Addr | string} addr
470
- * @returns {unknown}
471
- */
472
- get(addr) {
473
- try {
474
- const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
475
- wasm.document_get(retptr, this.__wbg_ptr, addHeapObject(addr));
476
- var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
477
- var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
478
- var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
479
- if (r2) {
480
- throw takeObject(r1);
481
- }
482
- return takeObject(r0);
483
- } finally {
484
- wasm.__wbindgen_add_to_stack_pointer(16);
485
- }
486
- }
487
460
  /**
488
461
  * The whole `$ext` map at `addr` (a card address, absent `card` = main), or
489
462
  * `undefined` when the card carries none. The fine-grained `$ext` read —
@@ -561,6 +534,35 @@ export class Document {
561
534
  wasm.__wbindgen_add_to_stack_pointer(16);
562
535
  }
563
536
  }
537
+ /**
538
+ * Read the **verbatim stored value** at `addr` — the raw payload value of a
539
+ * field (a content object for a richtext field, a scalar/array/object
540
+ * otherwise), or the **body content** when `addr.field` is absent. A bare
541
+ * string is `Addr` shorthand for `{ field }`. Reads are total over the field
542
+ * axis: an absent field is `undefined`; only an out-of-range `addr.card`
543
+ * throws `edit::index_out_of_range`. Needs no schema, so it lives on
544
+ * `Document` — the read echo of the verbatim `store*` write, distinct from
545
+ * the interpreted schema-plane [`reader.get`](Self::reader_get). For the
546
+ * markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
547
+ * `reader.get` (a field's declared type).
548
+ * @param {Addr | string} addr
549
+ * @returns {unknown}
550
+ */
551
+ getStored(addr) {
552
+ try {
553
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
554
+ wasm.document_getStored(retptr, this.__wbg_ptr, addHeapObject(addr));
555
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
556
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
557
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
558
+ if (r2) {
559
+ throw takeObject(r1);
560
+ }
561
+ return takeObject(r0);
562
+ } finally {
563
+ wasm.__wbindgen_add_to_stack_pointer(16);
564
+ }
565
+ }
564
566
  /**
565
567
  * Insert a card — the single insertion verb: `at` absent appends, a number
566
568
  * inserts at that index (must be in `0..=cards.length`). Accepts a
@@ -917,7 +919,7 @@ export class Document {
917
919
  }
918
920
  /**
919
921
  * **Revise** the richtext value at `addr` from a markdown string — **edit
920
- * semantics**, the default write path, returning the text [`Delta`]. Imports
922
+ * semantics**, the default write path, returning the text `Delta`. Imports
921
923
  * the markdown, diffs it against the current value, rebases surviving
922
924
  * identity anchors, and returns the change an editor bridge maps its own
923
925
  * positions through (`mapPos`). An absent `addr.field` targets the body, an
@@ -1691,7 +1693,8 @@ export class Quill {
1691
1693
  * The resolved-value view of `doc` against this quill's schema — for every
1692
1694
  * declared field the value the render projection would use and the
1693
1695
  * `FieldSource` rung it came from (`"authored" | "default" | "zero"`), in
1694
- * one call. The card body rides the `fields` map under the `$body` key.
1696
+ * one call. The card body is a `body` sibling on its card (row `name`
1697
+ * `"body"`), never a row in `fields` — `null` when the kind enables no body.
1695
1698
  *
1696
1699
  * Value and provenance only: completeness and errors stay `validate`'s
1697
1700
  * (a consumer merges it with its own diagnostic producers regardless), and
@@ -2064,10 +2067,11 @@ export function init() {
2064
2067
  }
2065
2068
 
2066
2069
  /**
2067
- * Map a base content position through a `delta` to its new position the pure
2068
- * position-mapping codec an editor bridge composes to hold a caret stable
2069
- * across a `revise`. `assoc` decides the side of a same-position insertion
2070
- * (`"after"` moves past it). Throws on a malformed `delta`.
2070
+ * Map a base content position a USV index into `Content.text`, not a UTF-16
2071
+ * offset through a `delta` to its new USV position: the pure position-mapping
2072
+ * codec an editor bridge composes to hold a caret stable across a `revise`.
2073
+ * `assoc` decides the side of a same-position insertion (`"after"` moves past
2074
+ * it). Throws on a malformed `delta`.
2071
2075
  * @param {Delta} delta
2072
2076
  * @param {number} pos
2073
2077
  * @param {Assoc} assoc
@@ -2119,9 +2123,10 @@ export function parseDocPath(path) {
2119
2123
  /**
2120
2124
  * Rebase `markdown` onto a `base` content — the pure, document-free twin of
2121
2125
  * `revise`: cold-import + `diff_import`, returning the new `content` and the
2122
- * text `delta` (surviving anchors rebased). Use it to compute a revise without
2123
- * a document in hand; `revise(addr, md)` fuses this with the store for
2124
- * atomicity. Throws on an over-nested markdown input or a non-content `base`.
2126
+ * text `delta` (its offsets USV indices into `Content.text`, surviving anchors
2127
+ * rebased). Use it to compute a revise without a document in hand; `revise(addr,
2128
+ * md)` fuses this with the store for atomicity. Throws on an over-nested
2129
+ * markdown input or a non-content `base`.
2125
2130
  * @param {Content} base
2126
2131
  * @param {string} markdown
2127
2132
  * @returns {{ content: Content; delta: Delta }}
Binary file
@@ -23,10 +23,10 @@ export const document_formatDiagnostic: (a: number, b: number) => void;
23
23
  export const document_formatRules: (a: number) => void;
24
24
  export const document_fromJson: (a: number, b: number, c: number) => void;
25
25
  export const document_fromMarkdown: (a: number, b: number, c: number) => void;
26
- export const document_get: (a: number, b: number, c: number) => void;
27
26
  export const document_getExt: (a: number, b: number, c: number) => void;
28
27
  export const document_getExtNamespace: (a: number, b: number, c: number, d: number, e: number) => void;
29
28
  export const document_getMarkdown: (a: number, b: number, c: number) => void;
29
+ export const document_getStored: (a: number, b: number, c: number) => void;
30
30
  export const document_insertCard: (a: number, b: number, c: number, d: number) => void;
31
31
  export const document_install: (a: number, b: number, c: number, d: number) => void;
32
32
  export const document_isFill: (a: number, b: number, c: number) => void;
@@ -102,7 +102,14 @@ export type ContentContainer =
102
102
  | { container: "list_item"; ordered: boolean; start: number; ordinal: number }
103
103
  | { container: "quote" };
104
104
 
105
- /** A mark over char range `[start, end)` into `Content.text`. */
105
+ /** A mark over char range `[start, end)` into `Content.text`. The open `type`
106
+ * arm blocks discriminant narrowing (as on `ContentIsland`), so read a
107
+ * payload-carrying arm behind its guard — `isLinkMark` (`url`) / `isAnchorMark`
108
+ * (`id`), from `@quillmark/wasm/runtime`; the bare arms carry no payload. An
109
+ * `anchor`'s `id` is a caller-supplied, opaque handle, unique per `Content` and
110
+ * invariant while the mark lives (positions rebase, the id never does); it has no
111
+ * markdown projection and survives only through the edit lane. See DOCUMENT_STORAGE
112
+ * § Anchor-id identity. */
106
113
  export type ContentMark = { start: number; end: number } & (
107
114
  | { type: "strong" | "emph" | "underline" | "strike" | "code" }
108
115
  | { type: "link"; url: string }
@@ -138,7 +145,8 @@ export interface ImageProps {
138
145
  * open set: the engine pins `props` as `TableProps` for `table` and `ImageProps`
139
146
  * for `image`; an island of any other type round-trips with opaque `props`. Like
140
147
  * `ContentMark`, the open `type` arm means a discriminant check does not itself
141
- * narrow `props` — key off `type` and read `props` as the matching shape. */
148
+ * narrow `props` — read `props` as the matching shape behind the `isTableIsland` /
149
+ * `isImageIsland` guards (from `@quillmark/wasm/runtime`), which narrow it. */
142
150
  export type ContentIsland = {
143
151
  id: string;
144
152
  /** How faithfully the markdown projection can carry this island. */
@@ -191,7 +199,10 @@ export type Assoc = "before" | "after";
191
199
  /**
192
200
  * A mark edit in final-text coordinates (post-delta, post-line-op). `add` /
193
201
  * `remove` carry the `ContentMark` vocabulary (`{ type, … }`); `removeAnchor`
194
- * drops one identity anchor by id.
202
+ * drops one identity anchor by id. An `add` of an `anchor` requires a non-empty
203
+ * `id` not already live in the field — a collision or the empty id throws
204
+ * (ids are caller-supplied and unique per `Content`; DOCUMENT_STORAGE
205
+ * § Anchor-id identity).
195
206
  */
196
207
  export type MarkOp =
197
208
  | ({ op: "add" | "remove"; start: number; end: number } & (
@@ -580,10 +591,10 @@ export class Document {
580
591
  */
581
592
  card(index: number): Card;
582
593
  /**
583
- * The index of the first composable card whose `$id` equals `id`, or
584
- * `undefined` when none carries it. Resolves the canonical durable address
594
+ * The index of the composable card whose `$id` equals `id`, or
595
+ * `undefined` when none carries it. Resolves the durable card handle
585
596
  * without a hand-rolled scan over [`cards`](Self::cards); `$id` is
586
- * non-unique by design, so the first match wins.
597
+ * unique per document, so at most one card matches.
587
598
  */
588
599
  cardIndexById(id: string): number | undefined;
589
600
  clone(): Document;
@@ -625,17 +636,6 @@ export class Document {
625
636
  * Parse markdown into a typed Document. Throws on parse errors.
626
637
  */
627
638
  static fromMarkdown(markdown: string): Document;
628
- /**
629
- * Read the value at `addr` — the raw stored payload value of a field (a
630
- * content object for a richtext field, a scalar/array/object otherwise), or
631
- * the **body content** when `addr.field` is absent. A bare string is `Addr`
632
- * shorthand for `{ field }`. Reads are total over the field axis: an absent
633
- * field is `undefined`; only an out-of-range `addr.card` throws
634
- * `edit::index_out_of_range`. Reads need no schema, so they live on
635
- * `Document`, not the typed writer; for the markdown projection of a
636
- * richtext value use [`getMarkdown`](Self::get_markdown).
637
- */
638
- get(addr: Addr | string): unknown;
639
639
  /**
640
640
  * The whole `$ext` map at `addr` (a card address, absent `card` = main), or
641
641
  * `undefined` when the card carries none. The fine-grained `$ext` read —
@@ -662,6 +662,19 @@ export class Document {
662
662
  * type (#978). An out-of-range `addr.card` throws.
663
663
  */
664
664
  getMarkdown(addr?: CardAddr): string;
665
+ /**
666
+ * Read the **verbatim stored value** at `addr` — the raw payload value of a
667
+ * field (a content object for a richtext field, a scalar/array/object
668
+ * otherwise), or the **body content** when `addr.field` is absent. A bare
669
+ * string is `Addr` shorthand for `{ field }`. Reads are total over the field
670
+ * axis: an absent field is `undefined`; only an out-of-range `addr.card`
671
+ * throws `edit::index_out_of_range`. Needs no schema, so it lives on
672
+ * `Document` — the read echo of the verbatim `store*` write, distinct from
673
+ * the interpreted schema-plane [`reader.get`](Self::reader_get). For the
674
+ * markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
675
+ * `reader.get` (a field's declared type).
676
+ */
677
+ getStored(addr: Addr | string): unknown;
665
678
  /**
666
679
  * Insert a card — the single insertion verb: `at` absent appends, a number
667
680
  * inserts at that index (must be in `0..=cards.length`). Accepts a
@@ -762,7 +775,7 @@ export class Document {
762
775
  removeSeedNamespace(card_kind: string): any;
763
776
  /**
764
777
  * **Revise** the richtext value at `addr` from a markdown string — **edit
765
- * semantics**, the default write path, returning the text [`Delta`]. Imports
778
+ * semantics**, the default write path, returning the text `Delta`. Imports
766
779
  * the markdown, diffs it against the current value, rebases surviving
767
780
  * identity anchors, and returns the change an editor bridge maps its own
768
781
  * positions through (`mapPos`). An absent `addr.field` targets the body, an
@@ -1029,7 +1042,8 @@ export class Quill {
1029
1042
  * The resolved-value view of `doc` against this quill's schema — for every
1030
1043
  * declared field the value the render projection would use and the
1031
1044
  * `FieldSource` rung it came from (`"authored" | "default" | "zero"`), in
1032
- * one call. The card body rides the `fields` map under the `$body` key.
1045
+ * one call. The card body is a `body` sibling on its card (row `name`
1046
+ * `"body"`), never a row in `fields` — `null` when the kind enables no body.
1033
1047
  *
1034
1048
  * Value and provenance only: completeness and errors stay `validate`'s
1035
1049
  * (a consumer merges it with its own diagnostic producers regardless), and
@@ -1178,10 +1192,11 @@ export function importMarkdown(markdown: string): Content;
1178
1192
  export function init(): void;
1179
1193
 
1180
1194
  /**
1181
- * Map a base content position through a `delta` to its new position the pure
1182
- * position-mapping codec an editor bridge composes to hold a caret stable
1183
- * across a `revise`. `assoc` decides the side of a same-position insertion
1184
- * (`"after"` moves past it). Throws on a malformed `delta`.
1195
+ * Map a base content position a USV index into `Content.text`, not a UTF-16
1196
+ * offset through a `delta` to its new USV position: the pure position-mapping
1197
+ * codec an editor bridge composes to hold a caret stable across a `revise`.
1198
+ * `assoc` decides the side of a same-position insertion (`"after"` moves past
1199
+ * it). Throws on a malformed `delta`.
1185
1200
  */
1186
1201
  export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
1187
1202
 
@@ -1197,8 +1212,9 @@ export function parseDocPath(path: string): DocPathSeg[];
1197
1212
  /**
1198
1213
  * Rebase `markdown` onto a `base` content — the pure, document-free twin of
1199
1214
  * `revise`: cold-import + `diff_import`, returning the new `content` and the
1200
- * text `delta` (surviving anchors rebased). Use it to compute a revise without
1201
- * a document in hand; `revise(addr, md)` fuses this with the store for
1202
- * atomicity. Throws on an over-nested markdown input or a non-content `base`.
1215
+ * text `delta` (its offsets USV indices into `Content.text`, surviving anchors
1216
+ * rebased). Use it to compute a revise without a document in hand; `revise(addr,
1217
+ * md)` fuses this with the store for atomicity. Throws on an over-nested
1218
+ * markdown input or a non-content `base`.
1203
1219
  */
1204
1220
  export function rebase(base: Content, markdown: string): { content: Content; delta: Delta };
@@ -131,7 +131,7 @@ export class Document {
131
131
  /**
132
132
  * Interpreted read at `addr`, resolving the field's declared `type` from
133
133
  * `quill` — the stable ABI under the runtime `reader.get` / `reader.card(i).get`.
134
- * The schema-plane twin of the quill-free [`get`](Self::get): a `richtext`
134
+ * The schema-plane twin of the quill-free [`getStored`](Self::get_stored): a `richtext`
135
135
  * field returns its markdown projection, every other declared type its
136
136
  * canonical value verbatim, so a consumer holding the quill reads by field
137
137
  * meaning rather than by wire shape.
@@ -176,7 +176,7 @@ export class Document {
176
176
  * surviving anchors rebase (as [`revise`](Self::revise)), then the diffed
177
177
  * result is schema-conformed, so a `richtext(inline)` field rejects a
178
178
  * multi-block result with `edit::field_richtext_not_inline`. Returns the
179
- * text [`Delta`].
179
+ * text `Delta`.
180
180
  *
181
181
  * `addr` must name a field (a bare string is `{ field }`); a body address
182
182
  * throws (a body carries no field schema — use [`revise`](Self::revise)). A
@@ -290,10 +290,10 @@ export class Document {
290
290
  return ret >>> 0;
291
291
  }
292
292
  /**
293
- * The index of the first composable card whose `$id` equals `id`, or
294
- * `undefined` when none carries it. Resolves the canonical durable address
293
+ * The index of the composable card whose `$id` equals `id`, or
294
+ * `undefined` when none carries it. Resolves the durable card handle
295
295
  * without a hand-rolled scan over [`cards`](Self::cards); `$id` is
296
- * non-unique by design, so the first match wins.
296
+ * unique per document, so at most one card matches.
297
297
  * @param {string} id
298
298
  * @returns {number | undefined}
299
299
  */
@@ -457,33 +457,6 @@ export class Document {
457
457
  wasm.__wbindgen_add_to_stack_pointer(16);
458
458
  }
459
459
  }
460
- /**
461
- * Read the value at `addr` — the raw stored payload value of a field (a
462
- * content object for a richtext field, a scalar/array/object otherwise), or
463
- * the **body content** when `addr.field` is absent. A bare string is `Addr`
464
- * shorthand for `{ field }`. Reads are total over the field axis: an absent
465
- * field is `undefined`; only an out-of-range `addr.card` throws
466
- * `edit::index_out_of_range`. Reads need no schema, so they live on
467
- * `Document`, not the typed writer; for the markdown projection of a
468
- * richtext value use [`getMarkdown`](Self::get_markdown).
469
- * @param {Addr | string} addr
470
- * @returns {unknown}
471
- */
472
- get(addr) {
473
- try {
474
- const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
475
- wasm.document_get(retptr, this.__wbg_ptr, addHeapObject(addr));
476
- var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
477
- var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
478
- var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
479
- if (r2) {
480
- throw takeObject(r1);
481
- }
482
- return takeObject(r0);
483
- } finally {
484
- wasm.__wbindgen_add_to_stack_pointer(16);
485
- }
486
- }
487
460
  /**
488
461
  * The whole `$ext` map at `addr` (a card address, absent `card` = main), or
489
462
  * `undefined` when the card carries none. The fine-grained `$ext` read —
@@ -561,6 +534,35 @@ export class Document {
561
534
  wasm.__wbindgen_add_to_stack_pointer(16);
562
535
  }
563
536
  }
537
+ /**
538
+ * Read the **verbatim stored value** at `addr` — the raw payload value of a
539
+ * field (a content object for a richtext field, a scalar/array/object
540
+ * otherwise), or the **body content** when `addr.field` is absent. A bare
541
+ * string is `Addr` shorthand for `{ field }`. Reads are total over the field
542
+ * axis: an absent field is `undefined`; only an out-of-range `addr.card`
543
+ * throws `edit::index_out_of_range`. Needs no schema, so it lives on
544
+ * `Document` — the read echo of the verbatim `store*` write, distinct from
545
+ * the interpreted schema-plane [`reader.get`](Self::reader_get). For the
546
+ * markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
547
+ * `reader.get` (a field's declared type).
548
+ * @param {Addr | string} addr
549
+ * @returns {unknown}
550
+ */
551
+ getStored(addr) {
552
+ try {
553
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
554
+ wasm.document_getStored(retptr, this.__wbg_ptr, addHeapObject(addr));
555
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
556
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
557
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
558
+ if (r2) {
559
+ throw takeObject(r1);
560
+ }
561
+ return takeObject(r0);
562
+ } finally {
563
+ wasm.__wbindgen_add_to_stack_pointer(16);
564
+ }
565
+ }
564
566
  /**
565
567
  * Insert a card — the single insertion verb: `at` absent appends, a number
566
568
  * inserts at that index (must be in `0..=cards.length`). Accepts a
@@ -917,7 +919,7 @@ export class Document {
917
919
  }
918
920
  /**
919
921
  * **Revise** the richtext value at `addr` from a markdown string — **edit
920
- * semantics**, the default write path, returning the text [`Delta`]. Imports
922
+ * semantics**, the default write path, returning the text `Delta`. Imports
921
923
  * the markdown, diffs it against the current value, rebases surviving
922
924
  * identity anchors, and returns the change an editor bridge maps its own
923
925
  * positions through (`mapPos`). An absent `addr.field` targets the body, an
@@ -1691,7 +1693,8 @@ export class Quill {
1691
1693
  * The resolved-value view of `doc` against this quill's schema — for every
1692
1694
  * declared field the value the render projection would use and the
1693
1695
  * `FieldSource` rung it came from (`"authored" | "default" | "zero"`), in
1694
- * one call. The card body rides the `fields` map under the `$body` key.
1696
+ * one call. The card body is a `body` sibling on its card (row `name`
1697
+ * `"body"`), never a row in `fields` — `null` when the kind enables no body.
1695
1698
  *
1696
1699
  * Value and provenance only: completeness and errors stay `validate`'s
1697
1700
  * (a consumer merges it with its own diagnostic producers regardless), and
@@ -2064,10 +2067,11 @@ export function init() {
2064
2067
  }
2065
2068
 
2066
2069
  /**
2067
- * Map a base content position through a `delta` to its new position the pure
2068
- * position-mapping codec an editor bridge composes to hold a caret stable
2069
- * across a `revise`. `assoc` decides the side of a same-position insertion
2070
- * (`"after"` moves past it). Throws on a malformed `delta`.
2070
+ * Map a base content position a USV index into `Content.text`, not a UTF-16
2071
+ * offset through a `delta` to its new USV position: the pure position-mapping
2072
+ * codec an editor bridge composes to hold a caret stable across a `revise`.
2073
+ * `assoc` decides the side of a same-position insertion (`"after"` moves past
2074
+ * it). Throws on a malformed `delta`.
2071
2075
  * @param {Delta} delta
2072
2076
  * @param {number} pos
2073
2077
  * @param {Assoc} assoc
@@ -2119,9 +2123,10 @@ export function parseDocPath(path) {
2119
2123
  /**
2120
2124
  * Rebase `markdown` onto a `base` content — the pure, document-free twin of
2121
2125
  * `revise`: cold-import + `diff_import`, returning the new `content` and the
2122
- * text `delta` (surviving anchors rebased). Use it to compute a revise without
2123
- * a document in hand; `revise(addr, md)` fuses this with the store for
2124
- * atomicity. Throws on an over-nested markdown input or a non-content `base`.
2126
+ * text `delta` (its offsets USV indices into `Content.text`, surviving anchors
2127
+ * rebased). Use it to compute a revise without a document in hand; `revise(addr,
2128
+ * md)` fuses this with the store for atomicity. Throws on an over-nested
2129
+ * markdown input or a non-content `base`.
2125
2130
  * @param {Content} base
2126
2131
  * @param {string} markdown
2127
2132
  * @returns {{ content: Content; delta: Delta }}
Binary file
@@ -23,10 +23,10 @@ export const document_formatDiagnostic: (a: number, b: number) => void;
23
23
  export const document_formatRules: (a: number) => void;
24
24
  export const document_fromJson: (a: number, b: number, c: number) => void;
25
25
  export const document_fromMarkdown: (a: number, b: number, c: number) => void;
26
- export const document_get: (a: number, b: number, c: number) => void;
27
26
  export const document_getExt: (a: number, b: number, c: number) => void;
28
27
  export const document_getExtNamespace: (a: number, b: number, c: number, d: number, e: number) => void;
29
28
  export const document_getMarkdown: (a: number, b: number, c: number) => void;
29
+ export const document_getStored: (a: number, b: number, c: number) => void;
30
30
  export const document_insertCard: (a: number, b: number, c: number, d: number) => void;
31
31
  export const document_install: (a: number, b: number, c: number, d: number) => void;
32
32
  export const document_isFill: (a: number, b: number, c: number) => void;
package/core/wasm.d.ts CHANGED
@@ -102,7 +102,14 @@ export type ContentContainer =
102
102
  | { container: "list_item"; ordered: boolean; start: number; ordinal: number }
103
103
  | { container: "quote" };
104
104
 
105
- /** A mark over char range `[start, end)` into `Content.text`. */
105
+ /** A mark over char range `[start, end)` into `Content.text`. The open `type`
106
+ * arm blocks discriminant narrowing (as on `ContentIsland`), so read a
107
+ * payload-carrying arm behind its guard — `isLinkMark` (`url`) / `isAnchorMark`
108
+ * (`id`), from `@quillmark/wasm/runtime`; the bare arms carry no payload. An
109
+ * `anchor`'s `id` is a caller-supplied, opaque handle, unique per `Content` and
110
+ * invariant while the mark lives (positions rebase, the id never does); it has no
111
+ * markdown projection and survives only through the edit lane. See DOCUMENT_STORAGE
112
+ * § Anchor-id identity. */
106
113
  export type ContentMark = { start: number; end: number } & (
107
114
  | { type: "strong" | "emph" | "underline" | "strike" | "code" }
108
115
  | { type: "link"; url: string }
@@ -138,7 +145,8 @@ export interface ImageProps {
138
145
  * open set: the engine pins `props` as `TableProps` for `table` and `ImageProps`
139
146
  * for `image`; an island of any other type round-trips with opaque `props`. Like
140
147
  * `ContentMark`, the open `type` arm means a discriminant check does not itself
141
- * narrow `props` — key off `type` and read `props` as the matching shape. */
148
+ * narrow `props` — read `props` as the matching shape behind the `isTableIsland` /
149
+ * `isImageIsland` guards (from `@quillmark/wasm/runtime`), which narrow it. */
142
150
  export type ContentIsland = {
143
151
  id: string;
144
152
  /** How faithfully the markdown projection can carry this island. */
@@ -191,7 +199,10 @@ export type Assoc = "before" | "after";
191
199
  /**
192
200
  * A mark edit in final-text coordinates (post-delta, post-line-op). `add` /
193
201
  * `remove` carry the `ContentMark` vocabulary (`{ type, … }`); `removeAnchor`
194
- * drops one identity anchor by id.
202
+ * drops one identity anchor by id. An `add` of an `anchor` requires a non-empty
203
+ * `id` not already live in the field — a collision or the empty id throws
204
+ * (ids are caller-supplied and unique per `Content`; DOCUMENT_STORAGE
205
+ * § Anchor-id identity).
195
206
  */
196
207
  export type MarkOp =
197
208
  | ({ op: "add" | "remove"; start: number; end: number } & (
@@ -451,10 +462,10 @@ export class Document {
451
462
  */
452
463
  card(index: number): Card;
453
464
  /**
454
- * The index of the first composable card whose `$id` equals `id`, or
455
- * `undefined` when none carries it. Resolves the canonical durable address
465
+ * The index of the composable card whose `$id` equals `id`, or
466
+ * `undefined` when none carries it. Resolves the durable card handle
456
467
  * without a hand-rolled scan over [`cards`](Self::cards); `$id` is
457
- * non-unique by design, so the first match wins.
468
+ * unique per document, so at most one card matches.
458
469
  */
459
470
  cardIndexById(id: string): number | undefined;
460
471
  clone(): Document;
@@ -496,17 +507,6 @@ export class Document {
496
507
  * Parse markdown into a typed Document. Throws on parse errors.
497
508
  */
498
509
  static fromMarkdown(markdown: string): Document;
499
- /**
500
- * Read the value at `addr` — the raw stored payload value of a field (a
501
- * content object for a richtext field, a scalar/array/object otherwise), or
502
- * the **body content** when `addr.field` is absent. A bare string is `Addr`
503
- * shorthand for `{ field }`. Reads are total over the field axis: an absent
504
- * field is `undefined`; only an out-of-range `addr.card` throws
505
- * `edit::index_out_of_range`. Reads need no schema, so they live on
506
- * `Document`, not the typed writer; for the markdown projection of a
507
- * richtext value use [`getMarkdown`](Self::get_markdown).
508
- */
509
- get(addr: Addr | string): unknown;
510
510
  /**
511
511
  * The whole `$ext` map at `addr` (a card address, absent `card` = main), or
512
512
  * `undefined` when the card carries none. The fine-grained `$ext` read —
@@ -533,6 +533,19 @@ export class Document {
533
533
  * type (#978). An out-of-range `addr.card` throws.
534
534
  */
535
535
  getMarkdown(addr?: CardAddr): string;
536
+ /**
537
+ * Read the **verbatim stored value** at `addr` — the raw payload value of a
538
+ * field (a content object for a richtext field, a scalar/array/object
539
+ * otherwise), or the **body content** when `addr.field` is absent. A bare
540
+ * string is `Addr` shorthand for `{ field }`. Reads are total over the field
541
+ * axis: an absent field is `undefined`; only an out-of-range `addr.card`
542
+ * throws `edit::index_out_of_range`. Needs no schema, so it lives on
543
+ * `Document` — the read echo of the verbatim `store*` write, distinct from
544
+ * the interpreted schema-plane [`reader.get`](Self::reader_get). For the
545
+ * markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
546
+ * `reader.get` (a field's declared type).
547
+ */
548
+ getStored(addr: Addr | string): unknown;
536
549
  /**
537
550
  * Insert a card — the single insertion verb: `at` absent appends, a number
538
551
  * inserts at that index (must be in `0..=cards.length`). Accepts a
@@ -633,7 +646,7 @@ export class Document {
633
646
  removeSeedNamespace(card_kind: string): any;
634
647
  /**
635
648
  * **Revise** the richtext value at `addr` from a markdown string — **edit
636
- * semantics**, the default write path, returning the text [`Delta`]. Imports
649
+ * semantics**, the default write path, returning the text `Delta`. Imports
637
650
  * the markdown, diffs it against the current value, rebases surviving
638
651
  * identity anchors, and returns the change an editor bridge maps its own
639
652
  * positions through (`mapPos`). An absent `addr.field` targets the body, an
@@ -776,7 +789,8 @@ export class Quill {
776
789
  * The resolved-value view of `doc` against this quill's schema — for every
777
790
  * declared field the value the render projection would use and the
778
791
  * `FieldSource` rung it came from (`"authored" | "default" | "zero"`), in
779
- * one call. The card body rides the `fields` map under the `$body` key.
792
+ * one call. The card body is a `body` sibling on its card (row `name`
793
+ * `"body"`), never a row in `fields` — `null` when the kind enables no body.
780
794
  *
781
795
  * Value and provenance only: completeness and errors stay `validate`'s
782
796
  * (a consumer merges it with its own diagnostic producers regardless), and
@@ -891,10 +905,11 @@ export function importMarkdown(markdown: string): Content;
891
905
  export function init(): void;
892
906
 
893
907
  /**
894
- * Map a base content position through a `delta` to its new position the pure
895
- * position-mapping codec an editor bridge composes to hold a caret stable
896
- * across a `revise`. `assoc` decides the side of a same-position insertion
897
- * (`"after"` moves past it). Throws on a malformed `delta`.
908
+ * Map a base content position a USV index into `Content.text`, not a UTF-16
909
+ * offset through a `delta` to its new USV position: the pure position-mapping
910
+ * codec an editor bridge composes to hold a caret stable across a `revise`.
911
+ * `assoc` decides the side of a same-position insertion (`"after"` moves past
912
+ * it). Throws on a malformed `delta`.
898
913
  */
899
914
  export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
900
915
 
@@ -910,8 +925,9 @@ export function parseDocPath(path: string): DocPathSeg[];
910
925
  /**
911
926
  * Rebase `markdown` onto a `base` content — the pure, document-free twin of
912
927
  * `revise`: cold-import + `diff_import`, returning the new `content` and the
913
- * text `delta` (surviving anchors rebased). Use it to compute a revise without
914
- * a document in hand; `revise(addr, md)` fuses this with the store for
915
- * atomicity. Throws on an over-nested markdown input or a non-content `base`.
928
+ * text `delta` (its offsets USV indices into `Content.text`, surviving anchors
929
+ * rebased). Use it to compute a revise without a document in hand; `revise(addr,
930
+ * md)` fuses this with the store for atomicity. Throws on an over-nested
931
+ * markdown input or a non-content `base`.
916
932
  */
917
933
  export function rebase(base: Content, markdown: string): { content: Content; delta: Delta };
package/core/wasm_bg.js CHANGED
@@ -131,7 +131,7 @@ export class Document {
131
131
  /**
132
132
  * Interpreted read at `addr`, resolving the field's declared `type` from
133
133
  * `quill` — the stable ABI under the runtime `reader.get` / `reader.card(i).get`.
134
- * The schema-plane twin of the quill-free [`get`](Self::get): a `richtext`
134
+ * The schema-plane twin of the quill-free [`getStored`](Self::get_stored): a `richtext`
135
135
  * field returns its markdown projection, every other declared type its
136
136
  * canonical value verbatim, so a consumer holding the quill reads by field
137
137
  * meaning rather than by wire shape.
@@ -176,7 +176,7 @@ export class Document {
176
176
  * surviving anchors rebase (as [`revise`](Self::revise)), then the diffed
177
177
  * result is schema-conformed, so a `richtext(inline)` field rejects a
178
178
  * multi-block result with `edit::field_richtext_not_inline`. Returns the
179
- * text [`Delta`].
179
+ * text `Delta`.
180
180
  *
181
181
  * `addr` must name a field (a bare string is `{ field }`); a body address
182
182
  * throws (a body carries no field schema — use [`revise`](Self::revise)). A
@@ -290,10 +290,10 @@ export class Document {
290
290
  return ret >>> 0;
291
291
  }
292
292
  /**
293
- * The index of the first composable card whose `$id` equals `id`, or
294
- * `undefined` when none carries it. Resolves the canonical durable address
293
+ * The index of the composable card whose `$id` equals `id`, or
294
+ * `undefined` when none carries it. Resolves the durable card handle
295
295
  * without a hand-rolled scan over [`cards`](Self::cards); `$id` is
296
- * non-unique by design, so the first match wins.
296
+ * unique per document, so at most one card matches.
297
297
  * @param {string} id
298
298
  * @returns {number | undefined}
299
299
  */
@@ -457,33 +457,6 @@ export class Document {
457
457
  wasm.__wbindgen_add_to_stack_pointer(16);
458
458
  }
459
459
  }
460
- /**
461
- * Read the value at `addr` — the raw stored payload value of a field (a
462
- * content object for a richtext field, a scalar/array/object otherwise), or
463
- * the **body content** when `addr.field` is absent. A bare string is `Addr`
464
- * shorthand for `{ field }`. Reads are total over the field axis: an absent
465
- * field is `undefined`; only an out-of-range `addr.card` throws
466
- * `edit::index_out_of_range`. Reads need no schema, so they live on
467
- * `Document`, not the typed writer; for the markdown projection of a
468
- * richtext value use [`getMarkdown`](Self::get_markdown).
469
- * @param {Addr | string} addr
470
- * @returns {unknown}
471
- */
472
- get(addr) {
473
- try {
474
- const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
475
- wasm.document_get(retptr, this.__wbg_ptr, addHeapObject(addr));
476
- var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
477
- var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
478
- var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
479
- if (r2) {
480
- throw takeObject(r1);
481
- }
482
- return takeObject(r0);
483
- } finally {
484
- wasm.__wbindgen_add_to_stack_pointer(16);
485
- }
486
- }
487
460
  /**
488
461
  * The whole `$ext` map at `addr` (a card address, absent `card` = main), or
489
462
  * `undefined` when the card carries none. The fine-grained `$ext` read —
@@ -561,6 +534,35 @@ export class Document {
561
534
  wasm.__wbindgen_add_to_stack_pointer(16);
562
535
  }
563
536
  }
537
+ /**
538
+ * Read the **verbatim stored value** at `addr` — the raw payload value of a
539
+ * field (a content object for a richtext field, a scalar/array/object
540
+ * otherwise), or the **body content** when `addr.field` is absent. A bare
541
+ * string is `Addr` shorthand for `{ field }`. Reads are total over the field
542
+ * axis: an absent field is `undefined`; only an out-of-range `addr.card`
543
+ * throws `edit::index_out_of_range`. Needs no schema, so it lives on
544
+ * `Document` — the read echo of the verbatim `store*` write, distinct from
545
+ * the interpreted schema-plane [`reader.get`](Self::reader_get). For the
546
+ * markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
547
+ * `reader.get` (a field's declared type).
548
+ * @param {Addr | string} addr
549
+ * @returns {unknown}
550
+ */
551
+ getStored(addr) {
552
+ try {
553
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
554
+ wasm.document_getStored(retptr, this.__wbg_ptr, addHeapObject(addr));
555
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
556
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
557
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
558
+ if (r2) {
559
+ throw takeObject(r1);
560
+ }
561
+ return takeObject(r0);
562
+ } finally {
563
+ wasm.__wbindgen_add_to_stack_pointer(16);
564
+ }
565
+ }
564
566
  /**
565
567
  * Insert a card — the single insertion verb: `at` absent appends, a number
566
568
  * inserts at that index (must be in `0..=cards.length`). Accepts a
@@ -917,7 +919,7 @@ export class Document {
917
919
  }
918
920
  /**
919
921
  * **Revise** the richtext value at `addr` from a markdown string — **edit
920
- * semantics**, the default write path, returning the text [`Delta`]. Imports
922
+ * semantics**, the default write path, returning the text `Delta`. Imports
921
923
  * the markdown, diffs it against the current value, rebases surviving
922
924
  * identity anchors, and returns the change an editor bridge maps its own
923
925
  * positions through (`mapPos`). An absent `addr.field` targets the body, an
@@ -1375,7 +1377,8 @@ export class Quill {
1375
1377
  * The resolved-value view of `doc` against this quill's schema — for every
1376
1378
  * declared field the value the render projection would use and the
1377
1379
  * `FieldSource` rung it came from (`"authored" | "default" | "zero"`), in
1378
- * one call. The card body rides the `fields` map under the `$body` key.
1380
+ * one call. The card body is a `body` sibling on its card (row `name`
1381
+ * `"body"`), never a row in `fields` — `null` when the kind enables no body.
1379
1382
  *
1380
1383
  * Value and provenance only: completeness and errors stay `validate`'s
1381
1384
  * (a consumer merges it with its own diagnostic producers regardless), and
@@ -1638,10 +1641,11 @@ export function init() {
1638
1641
  }
1639
1642
 
1640
1643
  /**
1641
- * Map a base content position through a `delta` to its new position the pure
1642
- * position-mapping codec an editor bridge composes to hold a caret stable
1643
- * across a `revise`. `assoc` decides the side of a same-position insertion
1644
- * (`"after"` moves past it). Throws on a malformed `delta`.
1644
+ * Map a base content position a USV index into `Content.text`, not a UTF-16
1645
+ * offset through a `delta` to its new USV position: the pure position-mapping
1646
+ * codec an editor bridge composes to hold a caret stable across a `revise`.
1647
+ * `assoc` decides the side of a same-position insertion (`"after"` moves past
1648
+ * it). Throws on a malformed `delta`.
1645
1649
  * @param {Delta} delta
1646
1650
  * @param {number} pos
1647
1651
  * @param {Assoc} assoc
@@ -1693,9 +1697,10 @@ export function parseDocPath(path) {
1693
1697
  /**
1694
1698
  * Rebase `markdown` onto a `base` content — the pure, document-free twin of
1695
1699
  * `revise`: cold-import + `diff_import`, returning the new `content` and the
1696
- * text `delta` (surviving anchors rebased). Use it to compute a revise without
1697
- * a document in hand; `revise(addr, md)` fuses this with the store for
1698
- * atomicity. Throws on an over-nested markdown input or a non-content `base`.
1700
+ * text `delta` (its offsets USV indices into `Content.text`, surviving anchors
1701
+ * rebased). Use it to compute a revise without a document in hand; `revise(addr,
1702
+ * md)` fuses this with the store for atomicity. Throws on an over-nested
1703
+ * markdown input or a non-content `base`.
1699
1704
  * @param {Content} base
1700
1705
  * @param {string} markdown
1701
1706
  * @returns {{ content: Content; delta: Delta }}
package/core/wasm_bg.wasm CHANGED
Binary file
@@ -21,10 +21,10 @@ export const document_formatDiagnostic: (a: number, b: number) => void;
21
21
  export const document_formatRules: (a: number) => void;
22
22
  export const document_fromJson: (a: number, b: number, c: number) => void;
23
23
  export const document_fromMarkdown: (a: number, b: number, c: number) => void;
24
- export const document_get: (a: number, b: number, c: number) => void;
25
24
  export const document_getExt: (a: number, b: number, c: number) => void;
26
25
  export const document_getExtNamespace: (a: number, b: number, c: number, d: number, e: number) => void;
27
26
  export const document_getMarkdown: (a: number, b: number, c: number) => void;
27
+ export const document_getStored: (a: number, b: number, c: number) => void;
28
28
  export const document_insertCard: (a: number, b: number, c: number, d: number) => void;
29
29
  export const document_install: (a: number, b: number, c: number, d: number) => void;
30
30
  export const document_isFill: (a: number, b: number, c: number) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/wasm",
3
- "version": "0.96.0",
3
+ "version": "0.97.0",
4
4
  "description": "WebAssembly bindings for Quillmark, a schema-driven document engine",
5
5
  "type": "module",
6
6
  "license": "MIT OR Apache-2.0",
@@ -79,8 +79,9 @@ export type {
79
79
  } from '../core/wasm.js';
80
80
 
81
81
  // The resolved-value view — the return shape of `quill.resolve(doc)`. Value
82
- // + source rung per declared field (the body rides the fields map under
83
- // `$body`); diagnostics stay `quill.validate`, guidance stays `quill.schema`.
82
+ // + source rung per declared field (the body is a `body` sibling on its card,
83
+ // never a row in `fields`); diagnostics stay `quill.validate`, guidance stays
84
+ // `quill.schema`.
84
85
  // Declared in the core build's generated `.d.ts` via a
85
86
  // `typescript_custom_section`; re-exported here so the single public entry
86
87
  // point names them.
@@ -120,6 +121,37 @@ export interface QuillmarkError extends Error {
120
121
  */
121
122
  export declare function isQuillmarkError(e: unknown): e is QuillmarkError;
122
123
 
124
+ // ── Open-set discriminant guards ────────────────────────────────────────────
125
+ // `ContentIsland.type` / `ContentMark.type` are open sets — each union has a
126
+ // residual `{ type: string; … }` arm, so a bare discriminant check never narrows
127
+ // the payload (TS keeps the residual arm live, since a `string` can equal the
128
+ // literal). These guards are the checked narrowing path for the pinned arms; an
129
+ // unrecognized `type` fails every guard and keeps its opaque payload. Only the
130
+ // payload-carrying arms get a guard — the bare marks
131
+ // (`strong`/`emph`/`underline`/`strike`/`code`) narrow to nothing.
132
+
133
+ import type { ContentIsland, TableProps, ImageProps, ContentMark } from '../core/wasm.js';
134
+
135
+ /** Narrow a {@link ContentIsland} to the pinned `table` arm (`props: TableProps`). */
136
+ export declare function isTableIsland(
137
+ island: ContentIsland
138
+ ): island is ContentIsland & { type: 'table'; props: TableProps };
139
+
140
+ /** Narrow a {@link ContentIsland} to the pinned `image` arm (`props: ImageProps`). */
141
+ export declare function isImageIsland(
142
+ island: ContentIsland
143
+ ): island is ContentIsland & { type: 'image'; props: ImageProps };
144
+
145
+ /** Narrow a {@link ContentMark} to the `link` arm (carries `url`). */
146
+ export declare function isLinkMark(
147
+ mark: ContentMark
148
+ ): mark is ContentMark & { type: 'link'; url: string };
149
+
150
+ /** Narrow a {@link ContentMark} to the `anchor` arm (carries `id`). */
151
+ export declare function isAnchorMark(
152
+ mark: ContentMark
153
+ ): mark is ContentMark & { type: 'anchor'; id: string };
154
+
123
155
  // ── Canonical render-side types ─────────────────────────────────────────────
124
156
  // These are the BACKEND-NEUTRAL render contract of the plural-backend API. They
125
157
  // are defined HERE (not re-exported from one private backend) because no single
@@ -630,7 +662,7 @@ export declare class CardWriter {
630
662
  * text, every other type its canonical value verbatim. Holds both handles by
631
663
  * reference and owns neither — nothing to `free()`.
632
664
  *
633
- * The schema authority is the point: unlike the quill-free transport `Document.get`,
665
+ * The schema authority is the point: unlike the quill-free transport `Document.getStored`,
634
666
  * a name the schema does not declare throws `UnknownField` (a typo) rather than
635
667
  * reading back `undefined`, and a content field holding a value that does not
636
668
  * decode throws `FieldRichtextDecode`. A field's markdown lives here, not on the
@@ -98,6 +98,49 @@ export function isQuillmarkError(e) {
98
98
  return e instanceof Error && Array.isArray(/** @type {any} */ (e).diagnostics);
99
99
  }
100
100
 
101
+ // ── Open-set discriminant guards ────────────────────────────────────────────
102
+ // `ContentIsland.type` and `ContentMark.type` are OPEN sets: each union carries
103
+ // a residual `{ type: string; … }` arm, so a bare `x.type === 'table'` check
104
+ // never narrows the payload — TS keeps the residual arm live (a `string` can be
105
+ // `'table'`), leaving `props` / the mark payload opaque at every consumer. These
106
+ // are the checked narrowing path: on the true branch the payload's pinned shape
107
+ // is asserted. Only the payload-carrying arms get a guard — an island always
108
+ // carries `props`, a `link` mark carries `url`, an `anchor` mark carries `id`;
109
+ // the bare marks (`strong`/`emph`/`underline`/`strike`/`code`) narrow to
110
+ // nothing. An unrecognized `type` fails every guard and keeps its opaque payload.
111
+
112
+ /**
113
+ * @param {import('../core/wasm.js').ContentIsland} island
114
+ * @returns {island is import('../core/wasm.js').ContentIsland & { type: 'table'; props: import('../core/wasm.js').TableProps }}
115
+ */
116
+ export function isTableIsland(island) {
117
+ return island.type === 'table';
118
+ }
119
+
120
+ /**
121
+ * @param {import('../core/wasm.js').ContentIsland} island
122
+ * @returns {island is import('../core/wasm.js').ContentIsland & { type: 'image'; props: import('../core/wasm.js').ImageProps }}
123
+ */
124
+ export function isImageIsland(island) {
125
+ return island.type === 'image';
126
+ }
127
+
128
+ /**
129
+ * @param {import('../core/wasm.js').ContentMark} mark
130
+ * @returns {mark is import('../core/wasm.js').ContentMark & { type: 'link'; url: string }}
131
+ */
132
+ export function isLinkMark(mark) {
133
+ return mark.type === 'link';
134
+ }
135
+
136
+ /**
137
+ * @param {import('../core/wasm.js').ContentMark} mark
138
+ * @returns {mark is import('../core/wasm.js').ContentMark & { type: 'anchor'; id: string }}
139
+ */
140
+ export function isAnchorMark(mark) {
141
+ return mark.type === 'anchor';
142
+ }
143
+
101
144
  // Backend builds are NEVER statically imported here — that would pull a
102
145
  // multi-MB binary into the eager graph and defeat lazy loading. Each entry is a
103
146
  // DESCRIPTOR: `load` is a thunk returning a dynamic `import()` (a backend's
@@ -754,7 +797,7 @@ Quill.prototype.writer = function writer(doc) {
754
797
  };
755
798
 
756
799
  // ── Typed-reader sugar: the schema-plane read surface ──────────────────────────
757
- // The read twin of the writer above. The transport `Document.get` is schema-free
800
+ // The read twin of the writer above. The transport `Document.getStored` is schema-free
758
801
  // — a `Document` cannot say which fields are richtext, so an unknown field name
759
802
  // reads back `undefined` rather than as the typo it is. Binding the quill's
760
803
  // schema (`_readerGet` takes the handle, like the `commit*` verbs) lets one `get`