@quillmark/wasm 0.109.1 → 0.111.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,120 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.111.0 - 2026-08-30
4
+
5
+ - feat(wasm): **`mapMarks(content, bundle)` answers where a `ChangeBundle`'s
6
+ text-moving channels leave a field's marks**, the coordinates its `markOps`
7
+ are written in. Each of `delta`, `islandOps` and `lineOps` rebases the marks
8
+ already in the field — a range's `start` takes assoc `after` and its `end`
9
+ `before`, a zero-width mark takes `before` — and that rule reached the
10
+ boundary only as a comment on a private method, so an editor deciding which
11
+ `markOps` to emit had to reimplement it, and one that read the range rule as
12
+ the whole rule drifted an anchor a character on text typed at the anchor's own
13
+ position. `Content::map_marks` and `Content::apply_field_change` walk one
14
+ channel list, so the prediction and the store cannot answer a position
15
+ differently, and the answer is normalized as the store's is, so a bundle
16
+ carrying no `markOps` names the marks the field will hold. The rule is stated
17
+ on `ChangeBundle` and in `BINDINGS.md`.
18
+
19
+ - fix(core): **a `plaintext` field declared `inline: true` keeps the flag on the
20
+ declaration wire.** `FieldSchema::serialize` projected the flag back out of
21
+ the type enum with a `RichText { inline: true }` match only, so
22
+ `type: plaintext, inline: true` serialized as `{"type":"plaintext"}` — WASM
23
+ `quill.schema()`, the Python binding, and the CLI `schema` command all lost
24
+ the single-line constraint, and a serde round-trip degraded the field to
25
+ `inline: false`.
26
+ - fix(core): **a comment on a CRLF line no longer carries its `\r` into the
27
+ emitted document.** The prescan splits on `\n`, so a trailing- or own-line
28
+ comment slice ran to end-of-line including the `\r`; it rode through the DTO
29
+ and wire and was written back verbatim, against `to_markdown`'s "line endings:
30
+ `\n` only". Field values were never affected. A `\r` *inside* a comment still
31
+ reaches emit.
32
+ - fix(facade): **loading a path that names no directory says so.** The walk
33
+ answered a missing root with an empty tree, so `quill_from_path("/typo")`
34
+ failed later with `Quill.yaml not found in file tree`, pointing at the
35
+ bundle's contents instead of the path. Python's `Quill.from_path` surfaced
36
+ that directly; the CLI's pre-checks are gone but for `validate`'s
37
+ missing-`Quill.yaml` one, gated on the directory existing so it names the
38
+ bundle a real directory lacks without shadowing the loader's answer for a
39
+ typo. `validate`'s load-failure summary names no file, that branch covering a
40
+ missing directory too.
41
+ - fix(content): **`LineOp::SetKind` refuses a heading level outside `1..=6`.**
42
+ The arm checked kind/text agreement but not the level, so a Rust caller could
43
+ apply `Heading { level: 9 }` and leave a content whose `validate()` fails and
44
+ whose export emits `#########` — read back as a literal-hash paragraph on the
45
+ next import. The JSON wires already range-checked it. New
46
+ `ApplyError::BadHeadingLevel`.
47
+ - fix(typst): **diagnostic columns count characters, not bytes.** Any multi-byte
48
+ character earlier on the source line inflated the reported column, which an
49
+ editor reads as a jump target.
50
+ - fix(pdf): **a non-finite widget `/Rect` is refused (`pdf::bad_rect`) rather
51
+ than written.** `form.json` rect values deserialize as plain `f32` and
52
+ saturate to `inf`, and `flip_rect` arithmetic can reach `NaN`; pdf-writer
53
+ prints a non-finite float verbatim, so `stamp` returned `Ok` with `inf`/`NaN`
54
+ tokens in the output — no PDF number grammar admits them. `flatten` guards the
55
+ geometry it draws. Matches the posture `font_size` already took. `regions_of`
56
+ is still unguarded.
57
+ - fix(pdf): **a base PDF that already carries an `/AcroForm` is refused
58
+ (`pdf::existing_acroform`).** The catalog rewrite appended a second
59
+ `/AcroForm` key without looking, leaving a dict the spec does not define and
60
+ the old form's widgets live in the preserved page `/Annots`. Stripping was
61
+ already the documented authoring rule; it is now checked.
62
+ - fix(pdf): **`fonts_used` registers only the faces a `/DA` names.** Only `Text`
63
+ and `Choice` widgets write one, so a checkbox or signature spec carrying
64
+ `Times`/`Courier` emitted an unreferenced Type1 object and a dead `/DR /Font`
65
+ entry into every stamped PDF.
66
+ - refactor(core): the unreachable null arm in the `Date`/`DateTime` coercion is
67
+ deleted (`conform_value` returns on any null before the type match), and
68
+ `Version` derives the ordering its field order already spells.
69
+
70
+ ## v0.110.0 - 2026-08-26
71
+
72
+ - **breaking** wasm: **the seam spells a container's `instance`, so the read
73
+ type can require it and every write lane reports an omission.** 0.109 gave
74
+ `Container` the discriminator that tells one container from an adjacent
75
+ sibling of identical shape, and left it an obligation no checker asked for;
76
+ the op lane got a requirement, the whole-`Content` lane — `overwrite`,
77
+ `CardInput.body`, the one a codec flattening a tree writes through — did not.
78
+ It could not: one encoder served storage and the bindings, writing the key
79
+ only where it was non-zero, and a field a read may omit is a field a write
80
+ cannot require. `serial::to_seam_value` is that encoder with every `instance`
81
+ spelled, and the bindings take it on every lane typed `Content`
82
+ (`reader.getContent{,At}`, `getStored` on a body, `importMarkdown`, `rebase`,
83
+ and the card wire behind `document.main` / `cards` / `card(i)` / `removeCard`
84
+ / `makeCard` / `seedMain` / `seedCard`). `ContentContainer.instance` becomes
85
+ required, `ContentContainerInput` is deleted, and `LineOp.setContainers` takes
86
+ `ContentContainer` — a net-smaller surface than 0.109's. The break is every
87
+ hand-built container literal, which is the one a type checker does report.
88
+ A checker still cannot report a `0` stamped on every run, which is the write
89
+ that welds them: `assignInstances` is the rule, not the type. Storage keeps
90
+ the omission and stored blobs re-encode byte for byte; so do the render lanes,
91
+ and so does what the seam types `unknown` (`getStored` on a field,
92
+ `PayloadItem.value`), which answers with the stored bytes verbatim. Content
93
+ parsed from a stored document is the one shape that needs a cast.
94
+ [Guide](docs/migrations/0.109-to-0.110.md)
95
+ - fix(core): **`Quill::validate` refuses only what the render floor refuses.**
96
+ Validation ran its own read-side type dispatch over the authored value while
97
+ the render door validated the *coerced* one, so five of seven types had values
98
+ that rendered and were fatally `validation::type_mismatch` at once — a bare
99
+ scalar for an `array`, `"3"` for an `integer`, `1` for a `boolean`, a
100
+ length-1 array for a `string` or `date`. `airmark`'s `usaf_memo@0.2` declares
101
+ `letterhead_caption` as an `array`, and the bare scalar a starter template
102
+ spells it with — a valid spelling of a one-element list — audited as fatally
103
+ invalid across every document seeded from it, each rendering correctly.
104
+
105
+ `validate_value` now conforms each document value through `conform_value` at
106
+ `Leniency::Render` before judging it, so a type has one predicate and **a
107
+ fatal `validation::*` diagnostic means the document does not render**.
108
+ Conforming runs per node, so one refused element no longer mistypes its
109
+ siblings: `counts: [true, "abc"]` under `integer` items is one mismatch, at
110
+ `counts[1]`. Two consequences for a consumer routing on codes: a value the
111
+ floor adopts raises nothing where it previously raised
112
+ `validation::type_mismatch`, and a bare scalar the floor stringifies into an
113
+ `enum` field is now domain-checked on that string, so `grade: 5` against
114
+ `values: [alpha, beta]` is `validation::enum_violation` where it was
115
+ previously silent — the diagnostic the render door already raised. Schema
116
+ literals (`example:`, `default:`) stay strict.
117
+
3
118
  ## v0.109.1 - 2026-08-24
4
119
 
5
120
  - fix(typst): **a `field-region` claim around inline content no longer widens
@@ -11,17 +126,6 @@
11
126
  `form-field`'s marker carries the same shape and drops the space too, rather
12
127
  than keep depending on the `box` that follows it.
13
128
 
14
- <!-- seed: commits since v0.109.0, confirm the entries above cover them, then delete this comment
15
- - docs: dense-prose pass over the branch
16
- - fix(typst): a marker's label is not a space in the inline flow
17
- - refactor(wasm): keep the weld rule internal, and pin what states it
18
- - docs: dense-prose pass over the branch
19
- - docs: classify 0.109's container break by consumer role
20
- - feat(wasm)!: make a written container path spell its instance
21
- - feat(content): name the rule a container's discriminator is minted against
22
- -->
23
-
24
-
25
129
  ## v0.109.0 - 2026-08-24
26
130
 
27
131
  - **breaking** content: **`Normalized` is the precondition the projections
@@ -100,33 +100,24 @@ export type ContentLineKind =
100
100
  * shape — two consecutive quotes, two consecutive lists — which contiguity
101
101
  * alone reads as one.
102
102
  *
103
- * **A writer owes it.** Give adjacent sibling runs of one shape distinct
104
- * values, or they arrive as one: a second list's items come back as
105
- * continuation paragraphs of the first, markers gone. Nothing reports that,
106
- * since the flat form cannot tell a boundary you meant from one you did not. A
107
- * codec flattening a tree stamps the field with `assignInstances` from
108
- * `@quillmark/wasm/runtime` rather than by hand. Any distinct pair of values
109
- * works; a write is canonicalized to `0`/`1`.
103
+ * **A writer owes a distinct value per adjacent sibling run**, not merely a
104
+ * value. Runs of one shape sharing one arrive as one: a second list's items come
105
+ * back as continuation paragraphs of the first, markers gone. The field is
106
+ * required, so a checker reports the omission; it cannot report a `0` stamped on
107
+ * both, which is the same write. A codec flattening a tree takes them from
108
+ * `assignInstances` in `@quillmark/wasm/runtime` rather than by hand. Any
109
+ * distinct pair works; a write is canonicalized to `0`/`1`.
110
110
  *
111
- * Reading is not the mirror of writing. The field is absent where it is `0`,
112
- * and it appears on pairs no writer had to spell. `1.` beside a list starting
113
- * at `3` differs by `start`, so those runs arrive apart with nothing written
114
- * and the canonical form spends a discriminator anyway, because Markdown reads
115
- * only a list's first number. */
116
- export type ContentContainer =
117
- | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance?: number }
118
- | { container: "quote"; instance?: number }
119
- | { container: string; attrs: unknown; instance?: number };
120
-
121
- /** A container path on a lane that only ever carries host-built values:
122
- * `ContentContainer` with `instance` spelled out rather than defaulted. The
123
- * field that decides whether two adjacent runs weld cannot be omitted by
124
- * accident here. `assignInstances` returns this shape, and a path copied off a
125
- * line carries its own instance through — `{ ...c, instance: c.instance ?? 0 }`.
111
+ * Reading is not the mirror of writing. Every read spells the field, the `0` on
112
+ * a container with nothing to be told apart from included. A read also carries a
113
+ * discriminator on pairs no writer had to spell: `1.` beside a list starting at
114
+ * `3` differs by `start`, so those runs arrive apart with nothing written, and
115
+ * the canonical form spends one anyway because Markdown reads only a list's
116
+ * first number.
126
117
  *
127
- * `Content` itself keeps the optional field: it is a read shape as much as a
128
- * write one, and the wire omits a zero. */
129
- export type ContentContainerInput =
118
+ * Content parsed from a stored document is the one shape that arrives without
119
+ * it storage omits a zero — and needs a cast. */
120
+ export type ContentContainer =
130
121
  | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance: number }
131
122
  | { container: "quote"; instance: number }
132
123
  | { container: string; attrs: unknown; instance: number };
@@ -256,7 +247,7 @@ export type LineOp =
256
247
  | { op: "split"; at: number }
257
248
  | { op: "join"; line: number }
258
249
  | ({ op: "setKind"; line: number } & ContentLineKind)
259
- | { op: "setContainers"; line: number; containers: ContentContainerInput[] }
250
+ | { op: "setContainers"; line: number; containers: ContentContainer[] }
260
251
  | { op: "setContinues"; line: number; continues: boolean };
261
252
 
262
253
  /**
@@ -299,6 +290,18 @@ export type IslandOp =
299
290
  * Within each channel ops apply in sequence against the state the earlier ones
300
291
  * left: an island `insert`'s `at` counts earlier ops' slots, and `lineOps`
301
292
  * positions and indices renumber through earlier `split`/`join`.
293
+ *
294
+ * **Mark rebase.** `delta`, `islandOps` and `lineOps` each move text, and each
295
+ * rebases the marks already in the field by one rule: a range mark's `start`
296
+ * takes assoc `after` and its `end` `before`, so an insertion at either edge
297
+ * grows text *outside* the span; a **zero-width** mark takes `before`, so an
298
+ * insertion at its own position leaves it put. That last case is the one
299
+ * position where the two assocs differ, and where an anchor most often sits.
300
+ *
301
+ * `markOps` name the result, so a caller emitting them predicts this rebase.
302
+ * `mapMarks(content, bundle)` runs it instead: pass the bundle's text-moving
303
+ * channels, diff the marks it returns against the ones you intend, and emit
304
+ * only the difference. Reproducing the rule by hand is a second copy to drift.
302
305
  */
303
306
  export interface ChangeBundle {
304
307
  delta?: Delta;
@@ -729,6 +732,10 @@ export class Document {
729
732
  * Throws on an out-of-range card, a field that is not richtext, a malformed
730
733
  * bundle, or an op that applies out of bounds; the value is unchanged on a
731
734
  * failed apply.
735
+ *
736
+ * Each text-moving channel rebases the marks already in the field, by the
737
+ * rule on `ChangeBundle`; `mapMarks` answers where they land, so a caller
738
+ * building `markOps` need not predict it.
732
739
  */
733
740
  applyChange(addr: Addr | string, bundle: ChangeBundle): void;
734
741
  /**
@@ -818,6 +825,10 @@ export class Document {
818
825
  * is at rest; one from the transport door may rest as authored until it is
819
826
  * conformed, and this read reports what is there. For the `Content` either
820
827
  * way use `reader.getContent`.
828
+ *
829
+ * The body arm is typed `Content` and answers in the seam form, spelling
830
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
831
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
821
832
  */
822
833
  getStored(addr: Addr | string): unknown;
823
834
  /**
@@ -1301,11 +1312,31 @@ export function formatDocPath(segs: DocPathSeg[]): string;
1301
1312
  */
1302
1313
  export function importMarkdown(markdown: string): Content;
1303
1314
 
1315
+ /**
1316
+ * Where `bundle`'s text-moving channels (`delta`, then `islandOps`, then
1317
+ * `lineOps`) leave `content`'s marks: the final-text coordinates the bundle's
1318
+ * `markOps` are written in, under the rebase rule stated on `ChangeBundle`.
1319
+ * The document-free read an editor diffs against to decide which `markOps` to
1320
+ * emit, rather than reproducing that rule in its own language.
1321
+ *
1322
+ * `bundle.markOps` are ignored. The answer is normalized, as the store's is:
1323
+ * marks a text move drops (out of range, zero-width formatting) are absent,
1324
+ * and same-kind runs a move left adjacent arrive already unioned, so a bundle
1325
+ * carrying no `markOps` names the marks the field will hold.
1326
+ * Throws on a non-content `content`, a malformed bundle, or an op that applies
1327
+ * out of bounds: `applyChange`'s errors on the same ops.
1328
+ */
1329
+ export function mapMarks(content: Content, bundle: ChangeBundle): ContentMark[];
1330
+
1304
1331
  /**
1305
1332
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
1306
1333
  * offset) through a `delta` to its new position, holding a caret stable across
1307
1334
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
1308
1335
  * moves past it). Throws on a malformed `delta`.
1336
+ *
1337
+ * This maps a position the *caller* holds. For the marks already in a field,
1338
+ * `mapMarks` applies the store's own assoc rule across every channel of a
1339
+ * `ChangeBundle`.
1309
1340
  */
1310
1341
  export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
1311
1342
 
@@ -1411,6 +1442,7 @@ export interface InitOutput {
1411
1442
  readonly livesession_supportsCanvas: (a: number) => number;
1412
1443
  readonly livesession_update: (a: number, b: number, c: number) => void;
1413
1444
  readonly livesession_warnings: (a: number, b: number) => void;
1445
+ readonly mapMarks: (a: number, b: number, c: number) => void;
1414
1446
  readonly mapPos: (a: number, b: number, c: number, d: number) => void;
1415
1447
  readonly parseDocPath: (a: number, b: number, c: number) => void;
1416
1448
  readonly quill_backendId: (a: number, b: number) => void;
@@ -279,6 +279,10 @@ export class Document {
279
279
  * Throws on an out-of-range card, a field that is not richtext, a malformed
280
280
  * bundle, or an op that applies out of bounds; the value is unchanged on a
281
281
  * failed apply.
282
+ *
283
+ * Each text-moving channel rebases the marks already in the field, by the
284
+ * rule on `ChangeBundle`; `mapMarks` answers where they land, so a caller
285
+ * building `markOps` need not predict it.
282
286
  * @param {Addr | string} addr
283
287
  * @param {ChangeBundle} bundle
284
288
  */
@@ -608,6 +612,10 @@ export class Document {
608
612
  * is at rest; one from the transport door may rest as authored until it is
609
613
  * conformed, and this read reports what is there. For the `Content` either
610
614
  * way use `reader.getContent`.
615
+ *
616
+ * The body arm is typed `Content` and answers in the seam form, spelling
617
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
618
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
611
619
  * @param {Addr | string} addr
612
620
  * @returns {unknown}
613
621
  */
@@ -2150,11 +2158,48 @@ export function importMarkdown(markdown) {
2150
2158
  }
2151
2159
  }
2152
2160
 
2161
+ /**
2162
+ * Where `bundle`'s text-moving channels (`delta`, then `islandOps`, then
2163
+ * `lineOps`) leave `content`'s marks: the final-text coordinates the bundle's
2164
+ * `markOps` are written in, under the rebase rule stated on `ChangeBundle`.
2165
+ * The document-free read an editor diffs against to decide which `markOps` to
2166
+ * emit, rather than reproducing that rule in its own language.
2167
+ *
2168
+ * `bundle.markOps` are ignored. The answer is normalized, as the store's is:
2169
+ * marks a text move drops (out of range, zero-width formatting) are absent,
2170
+ * and same-kind runs a move left adjacent arrive already unioned, so a bundle
2171
+ * carrying no `markOps` names the marks the field will hold.
2172
+ * Throws on a non-content `content`, a malformed bundle, or an op that applies
2173
+ * out of bounds: `applyChange`'s errors on the same ops.
2174
+ * @param {Content} content
2175
+ * @param {ChangeBundle} bundle
2176
+ * @returns {ContentMark[]}
2177
+ */
2178
+ export function mapMarks(content, bundle) {
2179
+ try {
2180
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
2181
+ wasm.mapMarks(retptr, addHeapObject(content), addHeapObject(bundle));
2182
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
2183
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
2184
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
2185
+ if (r2) {
2186
+ throw takeObject(r1);
2187
+ }
2188
+ return takeObject(r0);
2189
+ } finally {
2190
+ wasm.__wbindgen_add_to_stack_pointer(16);
2191
+ }
2192
+ }
2193
+
2153
2194
  /**
2154
2195
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
2155
2196
  * offset) through a `delta` to its new position, holding a caret stable across
2156
2197
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
2157
2198
  * moves past it). Throws on a malformed `delta`.
2199
+ *
2200
+ * This maps a position the *caller* holds. For the marks already in a field,
2201
+ * `mapMarks` applies the store's own assoc rule across every channel of a
2202
+ * `ChangeBundle`.
2158
2203
  * @param {Delta} delta
2159
2204
  * @param {number} pos
2160
2205
  * @param {Assoc} assoc
Binary file
@@ -76,6 +76,7 @@ export const livesession_render: (a: number, b: number, c: number) => void;
76
76
  export const livesession_supportsCanvas: (a: number) => number;
77
77
  export const livesession_update: (a: number, b: number, c: number) => void;
78
78
  export const livesession_warnings: (a: number, b: number) => void;
79
+ export const mapMarks: (a: number, b: number, c: number) => void;
79
80
  export const mapPos: (a: number, b: number, c: number, d: number) => void;
80
81
  export const parseDocPath: (a: number, b: number, c: number) => void;
81
82
  export const quill_backendId: (a: number, b: number) => void;
@@ -100,33 +100,24 @@ export type ContentLineKind =
100
100
  * shape — two consecutive quotes, two consecutive lists — which contiguity
101
101
  * alone reads as one.
102
102
  *
103
- * **A writer owes it.** Give adjacent sibling runs of one shape distinct
104
- * values, or they arrive as one: a second list's items come back as
105
- * continuation paragraphs of the first, markers gone. Nothing reports that,
106
- * since the flat form cannot tell a boundary you meant from one you did not. A
107
- * codec flattening a tree stamps the field with `assignInstances` from
108
- * `@quillmark/wasm/runtime` rather than by hand. Any distinct pair of values
109
- * works; a write is canonicalized to `0`/`1`.
103
+ * **A writer owes a distinct value per adjacent sibling run**, not merely a
104
+ * value. Runs of one shape sharing one arrive as one: a second list's items come
105
+ * back as continuation paragraphs of the first, markers gone. The field is
106
+ * required, so a checker reports the omission; it cannot report a `0` stamped on
107
+ * both, which is the same write. A codec flattening a tree takes them from
108
+ * `assignInstances` in `@quillmark/wasm/runtime` rather than by hand. Any
109
+ * distinct pair works; a write is canonicalized to `0`/`1`.
110
110
  *
111
- * Reading is not the mirror of writing. The field is absent where it is `0`,
112
- * and it appears on pairs no writer had to spell. `1.` beside a list starting
113
- * at `3` differs by `start`, so those runs arrive apart with nothing written
114
- * and the canonical form spends a discriminator anyway, because Markdown reads
115
- * only a list's first number. */
116
- export type ContentContainer =
117
- | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance?: number }
118
- | { container: "quote"; instance?: number }
119
- | { container: string; attrs: unknown; instance?: number };
120
-
121
- /** A container path on a lane that only ever carries host-built values:
122
- * `ContentContainer` with `instance` spelled out rather than defaulted. The
123
- * field that decides whether two adjacent runs weld cannot be omitted by
124
- * accident here. `assignInstances` returns this shape, and a path copied off a
125
- * line carries its own instance through — `{ ...c, instance: c.instance ?? 0 }`.
111
+ * Reading is not the mirror of writing. Every read spells the field, the `0` on
112
+ * a container with nothing to be told apart from included. A read also carries a
113
+ * discriminator on pairs no writer had to spell: `1.` beside a list starting at
114
+ * `3` differs by `start`, so those runs arrive apart with nothing written, and
115
+ * the canonical form spends one anyway because Markdown reads only a list's
116
+ * first number.
126
117
  *
127
- * `Content` itself keeps the optional field: it is a read shape as much as a
128
- * write one, and the wire omits a zero. */
129
- export type ContentContainerInput =
118
+ * Content parsed from a stored document is the one shape that arrives without
119
+ * it storage omits a zero — and needs a cast. */
120
+ export type ContentContainer =
130
121
  | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance: number }
131
122
  | { container: "quote"; instance: number }
132
123
  | { container: string; attrs: unknown; instance: number };
@@ -256,7 +247,7 @@ export type LineOp =
256
247
  | { op: "split"; at: number }
257
248
  | { op: "join"; line: number }
258
249
  | ({ op: "setKind"; line: number } & ContentLineKind)
259
- | { op: "setContainers"; line: number; containers: ContentContainerInput[] }
250
+ | { op: "setContainers"; line: number; containers: ContentContainer[] }
260
251
  | { op: "setContinues"; line: number; continues: boolean };
261
252
 
262
253
  /**
@@ -299,6 +290,18 @@ export type IslandOp =
299
290
  * Within each channel ops apply in sequence against the state the earlier ones
300
291
  * left: an island `insert`'s `at` counts earlier ops' slots, and `lineOps`
301
292
  * positions and indices renumber through earlier `split`/`join`.
293
+ *
294
+ * **Mark rebase.** `delta`, `islandOps` and `lineOps` each move text, and each
295
+ * rebases the marks already in the field by one rule: a range mark's `start`
296
+ * takes assoc `after` and its `end` `before`, so an insertion at either edge
297
+ * grows text *outside* the span; a **zero-width** mark takes `before`, so an
298
+ * insertion at its own position leaves it put. That last case is the one
299
+ * position where the two assocs differ, and where an anchor most often sits.
300
+ *
301
+ * `markOps` name the result, so a caller emitting them predicts this rebase.
302
+ * `mapMarks(content, bundle)` runs it instead: pass the bundle's text-moving
303
+ * channels, diff the marks it returns against the ones you intend, and emit
304
+ * only the difference. Reproducing the rule by hand is a second copy to drift.
302
305
  */
303
306
  export interface ChangeBundle {
304
307
  delta?: Delta;
@@ -729,6 +732,10 @@ export class Document {
729
732
  * Throws on an out-of-range card, a field that is not richtext, a malformed
730
733
  * bundle, or an op that applies out of bounds; the value is unchanged on a
731
734
  * failed apply.
735
+ *
736
+ * Each text-moving channel rebases the marks already in the field, by the
737
+ * rule on `ChangeBundle`; `mapMarks` answers where they land, so a caller
738
+ * building `markOps` need not predict it.
732
739
  */
733
740
  applyChange(addr: Addr | string, bundle: ChangeBundle): void;
734
741
  /**
@@ -818,6 +825,10 @@ export class Document {
818
825
  * is at rest; one from the transport door may rest as authored until it is
819
826
  * conformed, and this read reports what is there. For the `Content` either
820
827
  * way use `reader.getContent`.
828
+ *
829
+ * The body arm is typed `Content` and answers in the seam form, spelling
830
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
831
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
821
832
  */
822
833
  getStored(addr: Addr | string): unknown;
823
834
  /**
@@ -1301,11 +1312,31 @@ export function formatDocPath(segs: DocPathSeg[]): string;
1301
1312
  */
1302
1313
  export function importMarkdown(markdown: string): Content;
1303
1314
 
1315
+ /**
1316
+ * Where `bundle`'s text-moving channels (`delta`, then `islandOps`, then
1317
+ * `lineOps`) leave `content`'s marks: the final-text coordinates the bundle's
1318
+ * `markOps` are written in, under the rebase rule stated on `ChangeBundle`.
1319
+ * The document-free read an editor diffs against to decide which `markOps` to
1320
+ * emit, rather than reproducing that rule in its own language.
1321
+ *
1322
+ * `bundle.markOps` are ignored. The answer is normalized, as the store's is:
1323
+ * marks a text move drops (out of range, zero-width formatting) are absent,
1324
+ * and same-kind runs a move left adjacent arrive already unioned, so a bundle
1325
+ * carrying no `markOps` names the marks the field will hold.
1326
+ * Throws on a non-content `content`, a malformed bundle, or an op that applies
1327
+ * out of bounds: `applyChange`'s errors on the same ops.
1328
+ */
1329
+ export function mapMarks(content: Content, bundle: ChangeBundle): ContentMark[];
1330
+
1304
1331
  /**
1305
1332
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
1306
1333
  * offset) through a `delta` to its new position, holding a caret stable across
1307
1334
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
1308
1335
  * moves past it). Throws on a malformed `delta`.
1336
+ *
1337
+ * This maps a position the *caller* holds. For the marks already in a field,
1338
+ * `mapMarks` applies the store's own assoc rule across every channel of a
1339
+ * `ChangeBundle`.
1309
1340
  */
1310
1341
  export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
1311
1342
 
@@ -1411,6 +1442,7 @@ export interface InitOutput {
1411
1442
  readonly livesession_supportsCanvas: (a: number) => number;
1412
1443
  readonly livesession_update: (a: number, b: number, c: number) => void;
1413
1444
  readonly livesession_warnings: (a: number, b: number) => void;
1445
+ readonly mapMarks: (a: number, b: number, c: number) => void;
1414
1446
  readonly mapPos: (a: number, b: number, c: number, d: number) => void;
1415
1447
  readonly parseDocPath: (a: number, b: number, c: number) => void;
1416
1448
  readonly quill_backendId: (a: number, b: number) => void;
@@ -279,6 +279,10 @@ export class Document {
279
279
  * Throws on an out-of-range card, a field that is not richtext, a malformed
280
280
  * bundle, or an op that applies out of bounds; the value is unchanged on a
281
281
  * failed apply.
282
+ *
283
+ * Each text-moving channel rebases the marks already in the field, by the
284
+ * rule on `ChangeBundle`; `mapMarks` answers where they land, so a caller
285
+ * building `markOps` need not predict it.
282
286
  * @param {Addr | string} addr
283
287
  * @param {ChangeBundle} bundle
284
288
  */
@@ -608,6 +612,10 @@ export class Document {
608
612
  * is at rest; one from the transport door may rest as authored until it is
609
613
  * conformed, and this read reports what is there. For the `Content` either
610
614
  * way use `reader.getContent`.
615
+ *
616
+ * The body arm is typed `Content` and answers in the seam form, spelling
617
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
618
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
611
619
  * @param {Addr | string} addr
612
620
  * @returns {unknown}
613
621
  */
@@ -2150,11 +2158,48 @@ export function importMarkdown(markdown) {
2150
2158
  }
2151
2159
  }
2152
2160
 
2161
+ /**
2162
+ * Where `bundle`'s text-moving channels (`delta`, then `islandOps`, then
2163
+ * `lineOps`) leave `content`'s marks: the final-text coordinates the bundle's
2164
+ * `markOps` are written in, under the rebase rule stated on `ChangeBundle`.
2165
+ * The document-free read an editor diffs against to decide which `markOps` to
2166
+ * emit, rather than reproducing that rule in its own language.
2167
+ *
2168
+ * `bundle.markOps` are ignored. The answer is normalized, as the store's is:
2169
+ * marks a text move drops (out of range, zero-width formatting) are absent,
2170
+ * and same-kind runs a move left adjacent arrive already unioned, so a bundle
2171
+ * carrying no `markOps` names the marks the field will hold.
2172
+ * Throws on a non-content `content`, a malformed bundle, or an op that applies
2173
+ * out of bounds: `applyChange`'s errors on the same ops.
2174
+ * @param {Content} content
2175
+ * @param {ChangeBundle} bundle
2176
+ * @returns {ContentMark[]}
2177
+ */
2178
+ export function mapMarks(content, bundle) {
2179
+ try {
2180
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
2181
+ wasm.mapMarks(retptr, addHeapObject(content), addHeapObject(bundle));
2182
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
2183
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
2184
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
2185
+ if (r2) {
2186
+ throw takeObject(r1);
2187
+ }
2188
+ return takeObject(r0);
2189
+ } finally {
2190
+ wasm.__wbindgen_add_to_stack_pointer(16);
2191
+ }
2192
+ }
2193
+
2153
2194
  /**
2154
2195
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
2155
2196
  * offset) through a `delta` to its new position, holding a caret stable across
2156
2197
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
2157
2198
  * moves past it). Throws on a malformed `delta`.
2199
+ *
2200
+ * This maps a position the *caller* holds. For the marks already in a field,
2201
+ * `mapMarks` applies the store's own assoc rule across every channel of a
2202
+ * `ChangeBundle`.
2158
2203
  * @param {Delta} delta
2159
2204
  * @param {number} pos
2160
2205
  * @param {Assoc} assoc
Binary file
@@ -76,6 +76,7 @@ export const livesession_render: (a: number, b: number, c: number) => void;
76
76
  export const livesession_supportsCanvas: (a: number) => number;
77
77
  export const livesession_update: (a: number, b: number, c: number) => void;
78
78
  export const livesession_warnings: (a: number, b: number) => void;
79
+ export const mapMarks: (a: number, b: number, c: number) => void;
79
80
  export const mapPos: (a: number, b: number, c: number, d: number) => void;
80
81
  export const parseDocPath: (a: number, b: number, c: number) => void;
81
82
  export const quill_backendId: (a: number, b: number) => void;
package/core/wasm.d.ts CHANGED
@@ -100,33 +100,24 @@ export type ContentLineKind =
100
100
  * shape — two consecutive quotes, two consecutive lists — which contiguity
101
101
  * alone reads as one.
102
102
  *
103
- * **A writer owes it.** Give adjacent sibling runs of one shape distinct
104
- * values, or they arrive as one: a second list's items come back as
105
- * continuation paragraphs of the first, markers gone. Nothing reports that,
106
- * since the flat form cannot tell a boundary you meant from one you did not. A
107
- * codec flattening a tree stamps the field with `assignInstances` from
108
- * `@quillmark/wasm/runtime` rather than by hand. Any distinct pair of values
109
- * works; a write is canonicalized to `0`/`1`.
103
+ * **A writer owes a distinct value per adjacent sibling run**, not merely a
104
+ * value. Runs of one shape sharing one arrive as one: a second list's items come
105
+ * back as continuation paragraphs of the first, markers gone. The field is
106
+ * required, so a checker reports the omission; it cannot report a `0` stamped on
107
+ * both, which is the same write. A codec flattening a tree takes them from
108
+ * `assignInstances` in `@quillmark/wasm/runtime` rather than by hand. Any
109
+ * distinct pair works; a write is canonicalized to `0`/`1`.
110
110
  *
111
- * Reading is not the mirror of writing. The field is absent where it is `0`,
112
- * and it appears on pairs no writer had to spell. `1.` beside a list starting
113
- * at `3` differs by `start`, so those runs arrive apart with nothing written
114
- * and the canonical form spends a discriminator anyway, because Markdown reads
115
- * only a list's first number. */
116
- export type ContentContainer =
117
- | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance?: number }
118
- | { container: "quote"; instance?: number }
119
- | { container: string; attrs: unknown; instance?: number };
120
-
121
- /** A container path on a lane that only ever carries host-built values:
122
- * `ContentContainer` with `instance` spelled out rather than defaulted. The
123
- * field that decides whether two adjacent runs weld cannot be omitted by
124
- * accident here. `assignInstances` returns this shape, and a path copied off a
125
- * line carries its own instance through — `{ ...c, instance: c.instance ?? 0 }`.
111
+ * Reading is not the mirror of writing. Every read spells the field, the `0` on
112
+ * a container with nothing to be told apart from included. A read also carries a
113
+ * discriminator on pairs no writer had to spell: `1.` beside a list starting at
114
+ * `3` differs by `start`, so those runs arrive apart with nothing written, and
115
+ * the canonical form spends one anyway because Markdown reads only a list's
116
+ * first number.
126
117
  *
127
- * `Content` itself keeps the optional field: it is a read shape as much as a
128
- * write one, and the wire omits a zero. */
129
- export type ContentContainerInput =
118
+ * Content parsed from a stored document is the one shape that arrives without
119
+ * it storage omits a zero — and needs a cast. */
120
+ export type ContentContainer =
130
121
  | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance: number }
131
122
  | { container: "quote"; instance: number }
132
123
  | { container: string; attrs: unknown; instance: number };
@@ -256,7 +247,7 @@ export type LineOp =
256
247
  | { op: "split"; at: number }
257
248
  | { op: "join"; line: number }
258
249
  | ({ op: "setKind"; line: number } & ContentLineKind)
259
- | { op: "setContainers"; line: number; containers: ContentContainerInput[] }
250
+ | { op: "setContainers"; line: number; containers: ContentContainer[] }
260
251
  | { op: "setContinues"; line: number; continues: boolean };
261
252
 
262
253
  /**
@@ -299,6 +290,18 @@ export type IslandOp =
299
290
  * Within each channel ops apply in sequence against the state the earlier ones
300
291
  * left: an island `insert`'s `at` counts earlier ops' slots, and `lineOps`
301
292
  * positions and indices renumber through earlier `split`/`join`.
293
+ *
294
+ * **Mark rebase.** `delta`, `islandOps` and `lineOps` each move text, and each
295
+ * rebases the marks already in the field by one rule: a range mark's `start`
296
+ * takes assoc `after` and its `end` `before`, so an insertion at either edge
297
+ * grows text *outside* the span; a **zero-width** mark takes `before`, so an
298
+ * insertion at its own position leaves it put. That last case is the one
299
+ * position where the two assocs differ, and where an anchor most often sits.
300
+ *
301
+ * `markOps` name the result, so a caller emitting them predicts this rebase.
302
+ * `mapMarks(content, bundle)` runs it instead: pass the bundle's text-moving
303
+ * channels, diff the marks it returns against the ones you intend, and emit
304
+ * only the difference. Reproducing the rule by hand is a second copy to drift.
302
305
  */
303
306
  export interface ChangeBundle {
304
307
  delta?: Delta;
@@ -545,6 +548,10 @@ export class Document {
545
548
  * Throws on an out-of-range card, a field that is not richtext, a malformed
546
549
  * bundle, or an op that applies out of bounds; the value is unchanged on a
547
550
  * failed apply.
551
+ *
552
+ * Each text-moving channel rebases the marks already in the field, by the
553
+ * rule on `ChangeBundle`; `mapMarks` answers where they land, so a caller
554
+ * building `markOps` need not predict it.
548
555
  */
549
556
  applyChange(addr: Addr | string, bundle: ChangeBundle): void;
550
557
  /**
@@ -634,6 +641,10 @@ export class Document {
634
641
  * is at rest; one from the transport door may rest as authored until it is
635
642
  * conformed, and this read reports what is there. For the `Content` either
636
643
  * way use `reader.getContent`.
644
+ *
645
+ * The body arm is typed `Content` and answers in the seam form, spelling
646
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
647
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
637
648
  */
638
649
  getStored(addr: Addr | string): unknown;
639
650
  /**
@@ -980,11 +991,31 @@ export function formatDocPath(segs: DocPathSeg[]): string;
980
991
  */
981
992
  export function importMarkdown(markdown: string): Content;
982
993
 
994
+ /**
995
+ * Where `bundle`'s text-moving channels (`delta`, then `islandOps`, then
996
+ * `lineOps`) leave `content`'s marks: the final-text coordinates the bundle's
997
+ * `markOps` are written in, under the rebase rule stated on `ChangeBundle`.
998
+ * The document-free read an editor diffs against to decide which `markOps` to
999
+ * emit, rather than reproducing that rule in its own language.
1000
+ *
1001
+ * `bundle.markOps` are ignored. The answer is normalized, as the store's is:
1002
+ * marks a text move drops (out of range, zero-width formatting) are absent,
1003
+ * and same-kind runs a move left adjacent arrive already unioned, so a bundle
1004
+ * carrying no `markOps` names the marks the field will hold.
1005
+ * Throws on a non-content `content`, a malformed bundle, or an op that applies
1006
+ * out of bounds: `applyChange`'s errors on the same ops.
1007
+ */
1008
+ export function mapMarks(content: Content, bundle: ChangeBundle): ContentMark[];
1009
+
983
1010
  /**
984
1011
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
985
1012
  * offset) through a `delta` to its new position, holding a caret stable across
986
1013
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
987
1014
  * moves past it). Throws on a malformed `delta`.
1015
+ *
1016
+ * This maps a position the *caller* holds. For the marks already in a field,
1017
+ * `mapMarks` applies the store's own assoc rule across every channel of a
1018
+ * `ChangeBundle`.
988
1019
  */
989
1020
  export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
990
1021
 
@@ -1075,6 +1106,7 @@ export interface InitOutput {
1075
1106
  readonly exportMarkdown: (a: number, b: number) => void;
1076
1107
  readonly formatDocPath: (a: number, b: number) => void;
1077
1108
  readonly importMarkdown: (a: number, b: number, c: number) => void;
1109
+ readonly mapMarks: (a: number, b: number, c: number) => void;
1078
1110
  readonly mapPos: (a: number, b: number, c: number, d: number) => void;
1079
1111
  readonly parseDocPath: (a: number, b: number, c: number) => void;
1080
1112
  readonly quill_backendId: (a: number, b: number) => void;
package/core/wasm.js CHANGED
@@ -279,6 +279,10 @@ export class Document {
279
279
  * Throws on an out-of-range card, a field that is not richtext, a malformed
280
280
  * bundle, or an op that applies out of bounds; the value is unchanged on a
281
281
  * failed apply.
282
+ *
283
+ * Each text-moving channel rebases the marks already in the field, by the
284
+ * rule on `ChangeBundle`; `mapMarks` answers where they land, so a caller
285
+ * building `markOps` need not predict it.
282
286
  * @param {Addr | string} addr
283
287
  * @param {ChangeBundle} bundle
284
288
  */
@@ -608,6 +612,10 @@ export class Document {
608
612
  * is at rest; one from the transport door may rest as authored until it is
609
613
  * conformed, and this read reports what is there. For the `Content` either
610
614
  * way use `reader.getContent`.
615
+ *
616
+ * The body arm is typed `Content` and answers in the seam form, spelling
617
+ * every `ContentContainer.instance`. A field arm echoes the stored bytes,
618
+ * which omit a zero: verbatim is the contract, and is why it is `unknown`.
611
619
  * @param {Addr | string} addr
612
620
  * @returns {unknown}
613
621
  */
@@ -1745,11 +1753,48 @@ export function importMarkdown(markdown) {
1745
1753
  }
1746
1754
  }
1747
1755
 
1756
+ /**
1757
+ * Where `bundle`'s text-moving channels (`delta`, then `islandOps`, then
1758
+ * `lineOps`) leave `content`'s marks: the final-text coordinates the bundle's
1759
+ * `markOps` are written in, under the rebase rule stated on `ChangeBundle`.
1760
+ * The document-free read an editor diffs against to decide which `markOps` to
1761
+ * emit, rather than reproducing that rule in its own language.
1762
+ *
1763
+ * `bundle.markOps` are ignored. The answer is normalized, as the store's is:
1764
+ * marks a text move drops (out of range, zero-width formatting) are absent,
1765
+ * and same-kind runs a move left adjacent arrive already unioned, so a bundle
1766
+ * carrying no `markOps` names the marks the field will hold.
1767
+ * Throws on a non-content `content`, a malformed bundle, or an op that applies
1768
+ * out of bounds: `applyChange`'s errors on the same ops.
1769
+ * @param {Content} content
1770
+ * @param {ChangeBundle} bundle
1771
+ * @returns {ContentMark[]}
1772
+ */
1773
+ export function mapMarks(content, bundle) {
1774
+ try {
1775
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
1776
+ wasm.mapMarks(retptr, addHeapObject(content), addHeapObject(bundle));
1777
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
1778
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
1779
+ var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
1780
+ if (r2) {
1781
+ throw takeObject(r1);
1782
+ }
1783
+ return takeObject(r0);
1784
+ } finally {
1785
+ wasm.__wbindgen_add_to_stack_pointer(16);
1786
+ }
1787
+ }
1788
+
1748
1789
  /**
1749
1790
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
1750
1791
  * offset) through a `delta` to its new position, holding a caret stable across
1751
1792
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
1752
1793
  * moves past it). Throws on a malformed `delta`.
1794
+ *
1795
+ * This maps a position the *caller* holds. For the marks already in a field,
1796
+ * `mapMarks` applies the store's own assoc rule across every channel of a
1797
+ * `ChangeBundle`.
1753
1798
  * @param {Delta} delta
1754
1799
  * @param {number} pos
1755
1800
  * @param {Assoc} assoc
package/core/wasm_bg.wasm CHANGED
Binary file
@@ -61,6 +61,7 @@ export const document_warnings: (a: number, b: number) => void;
61
61
  export const exportMarkdown: (a: number, b: number) => void;
62
62
  export const formatDocPath: (a: number, b: number) => void;
63
63
  export const importMarkdown: (a: number, b: number, c: number) => void;
64
+ export const mapMarks: (a: number, b: number, c: number) => void;
64
65
  export const mapPos: (a: number, b: number, c: number, d: number) => void;
65
66
  export const parseDocPath: (a: number, b: number, c: number) => void;
66
67
  export const quill_backendId: (a: number, b: number) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/wasm",
3
- "version": "0.109.1",
3
+ "version": "0.111.0",
4
4
  "description": "WebAssembly bindings for Quillmark, a schema-driven document engine",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -24,6 +24,7 @@ import type {
24
24
  exportMarkdown,
25
25
  rebase,
26
26
  mapPos,
27
+ mapMarks,
27
28
  parseDocPath,
28
29
  formatDocPath
29
30
  } from '../core/wasm.js';
@@ -42,6 +43,7 @@ export interface CoreSurface {
42
43
  exportMarkdown: typeof exportMarkdown;
43
44
  rebase: typeof rebase;
44
45
  mapPos: typeof mapPos;
46
+ mapMarks: typeof mapMarks;
45
47
  parseDocPath: typeof parseDocPath;
46
48
  formatDocPath: typeof formatDocPath;
47
49
  }
@@ -130,7 +132,6 @@ export type {
130
132
  ContentLine,
131
133
  ContentLineKind,
132
134
  ContentContainer,
133
- ContentContainerInput,
134
135
  ContentMark,
135
136
  ContentIsland,
136
137
  TableProps,
@@ -205,8 +206,7 @@ import type {
205
206
  ImageProps,
206
207
  ContentMark,
207
208
  ContentLine,
208
- ContentContainer,
209
- ContentContainerInput
209
+ ContentContainer
210
210
  } from '../core/wasm.js';
211
211
 
212
212
  /** Narrow a {@link ContentIsland} to the pinned `table` arm (`props: TableProps`). */
@@ -247,7 +247,7 @@ export declare function isListItemContainer(
247
247
  ordered: boolean;
248
248
  start: number;
249
249
  ordinal: number;
250
- instance?: number;
250
+ instance: number;
251
251
  };
252
252
 
253
253
  // The guards above answer "is this arm X". These four answer "is this a value
@@ -280,11 +280,11 @@ export declare function isUnknownIsland(
280
280
  island: ContentIsland
281
281
  ): island is ContentIsland & { type: string; props: unknown };
282
282
 
283
- // `ContentContainer.instance` is a field a writer owes and, outside
284
- // `ContentContainerInput`, no checker asks for. Adjacent runs of one shape that
285
- // share it arrive welded. Nothing reports that: the flat `containers` form
286
- // cannot tell it from one container spanning two paragraphs. This carries the
287
- // rule a codec would otherwise re-derive.
283
+ // `ContentContainer.instance` is required, so a checker reports an omission; it
284
+ // cannot report a `0` stamped on every run, which is the same write. Adjacent
285
+ // runs of one shape sharing a value arrive welded, and nothing reports that
286
+ // either: the flat `containers` form cannot tell it from one container spanning
287
+ // two paragraphs. This carries the rule a codec would otherwise re-derive.
288
288
 
289
289
  /**
290
290
  * Stamp `instance` across one parent's blocks at one depth, in document order,
@@ -313,7 +313,7 @@ export declare function isUnknownIsland(
313
313
  */
314
314
  export declare function assignInstances(
315
315
  runs: (ContentContainer | null)[]
316
- ): (ContentContainerInput | null)[];
316
+ ): (ContentContainer | null)[];
317
317
 
318
318
  // The backend-neutral render contract, defined here rather than re-exported from
319
319
  // one private backend because no single backend owns the canonical API's types.
@@ -40,7 +40,7 @@ import initCore, { Quill, Document } from '../core/wasm.js';
40
40
  // itself), a `node:fs` read under Node, whose `fetch` rejects `file:` URLs.
41
41
  // Resolution-time, so `node:fs` never enters a browser graph.
42
42
  import { toModuleSource } from '#quillmark-env';
43
- import { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
43
+ import { importMarkdown, exportMarkdown, rebase, mapPos, mapMarks } from '../core/wasm.js';
44
44
  import { parseDocPath, formatDocPath } from '../core/wasm.js';
45
45
 
46
46
  // ── Initialization ──────────────────────────────────────────────────────────
@@ -90,6 +90,7 @@ const CORE_SURFACE = Object.freeze({
90
90
  exportMarkdown,
91
91
  rebase,
92
92
  mapPos,
93
+ mapMarks,
93
94
  parseDocPath,
94
95
  formatDocPath
95
96
  });
@@ -553,7 +554,7 @@ function weldsWith(a, b) {
553
554
 
554
555
  /**
555
556
  * @param {(import('../core/wasm.js').ContentContainer | null)[]} runs
556
- * @returns {(import('../core/wasm.js').ContentContainerInput | null)[]}
557
+ * @returns {(import('../core/wasm.js').ContentContainer | null)[]}
557
558
  */
558
559
  export function assignInstances(runs) {
559
560
  let prev = null;