@quillmark/wasm 0.110.0 → 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,72 @@
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
+
3
70
  ## v0.110.0 - 2026-08-26
4
71
 
5
72
  - **breaking** wasm: **the seam spells a container's `instance`, so the read
@@ -290,6 +290,18 @@ export type IslandOp =
290
290
  * Within each channel ops apply in sequence against the state the earlier ones
291
291
  * left: an island `insert`'s `at` counts earlier ops' slots, and `lineOps`
292
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.
293
305
  */
294
306
  export interface ChangeBundle {
295
307
  delta?: Delta;
@@ -720,6 +732,10 @@ export class Document {
720
732
  * Throws on an out-of-range card, a field that is not richtext, a malformed
721
733
  * bundle, or an op that applies out of bounds; the value is unchanged on a
722
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.
723
739
  */
724
740
  applyChange(addr: Addr | string, bundle: ChangeBundle): void;
725
741
  /**
@@ -1296,11 +1312,31 @@ export function formatDocPath(segs: DocPathSeg[]): string;
1296
1312
  */
1297
1313
  export function importMarkdown(markdown: string): Content;
1298
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
+
1299
1331
  /**
1300
1332
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
1301
1333
  * offset) through a `delta` to its new position, holding a caret stable across
1302
1334
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
1303
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`.
1304
1340
  */
1305
1341
  export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
1306
1342
 
@@ -1406,6 +1442,7 @@ export interface InitOutput {
1406
1442
  readonly livesession_supportsCanvas: (a: number) => number;
1407
1443
  readonly livesession_update: (a: number, b: number, c: number) => void;
1408
1444
  readonly livesession_warnings: (a: number, b: number) => void;
1445
+ readonly mapMarks: (a: number, b: number, c: number) => void;
1409
1446
  readonly mapPos: (a: number, b: number, c: number, d: number) => void;
1410
1447
  readonly parseDocPath: (a: number, b: number, c: number) => void;
1411
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
  */
@@ -2154,11 +2158,48 @@ export function importMarkdown(markdown) {
2154
2158
  }
2155
2159
  }
2156
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
+
2157
2194
  /**
2158
2195
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
2159
2196
  * offset) through a `delta` to its new position, holding a caret stable across
2160
2197
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
2161
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`.
2162
2203
  * @param {Delta} delta
2163
2204
  * @param {number} pos
2164
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;
@@ -290,6 +290,18 @@ export type IslandOp =
290
290
  * Within each channel ops apply in sequence against the state the earlier ones
291
291
  * left: an island `insert`'s `at` counts earlier ops' slots, and `lineOps`
292
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.
293
305
  */
294
306
  export interface ChangeBundle {
295
307
  delta?: Delta;
@@ -720,6 +732,10 @@ export class Document {
720
732
  * Throws on an out-of-range card, a field that is not richtext, a malformed
721
733
  * bundle, or an op that applies out of bounds; the value is unchanged on a
722
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.
723
739
  */
724
740
  applyChange(addr: Addr | string, bundle: ChangeBundle): void;
725
741
  /**
@@ -1296,11 +1312,31 @@ export function formatDocPath(segs: DocPathSeg[]): string;
1296
1312
  */
1297
1313
  export function importMarkdown(markdown: string): Content;
1298
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
+
1299
1331
  /**
1300
1332
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
1301
1333
  * offset) through a `delta` to its new position, holding a caret stable across
1302
1334
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
1303
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`.
1304
1340
  */
1305
1341
  export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
1306
1342
 
@@ -1406,6 +1442,7 @@ export interface InitOutput {
1406
1442
  readonly livesession_supportsCanvas: (a: number) => number;
1407
1443
  readonly livesession_update: (a: number, b: number, c: number) => void;
1408
1444
  readonly livesession_warnings: (a: number, b: number) => void;
1445
+ readonly mapMarks: (a: number, b: number, c: number) => void;
1409
1446
  readonly mapPos: (a: number, b: number, c: number, d: number) => void;
1410
1447
  readonly parseDocPath: (a: number, b: number, c: number) => void;
1411
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
  */
@@ -2154,11 +2158,48 @@ export function importMarkdown(markdown) {
2154
2158
  }
2155
2159
  }
2156
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
+
2157
2194
  /**
2158
2195
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
2159
2196
  * offset) through a `delta` to its new position, holding a caret stable across
2160
2197
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
2161
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`.
2162
2203
  * @param {Delta} delta
2163
2204
  * @param {number} pos
2164
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
@@ -290,6 +290,18 @@ export type IslandOp =
290
290
  * Within each channel ops apply in sequence against the state the earlier ones
291
291
  * left: an island `insert`'s `at` counts earlier ops' slots, and `lineOps`
292
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.
293
305
  */
294
306
  export interface ChangeBundle {
295
307
  delta?: Delta;
@@ -536,6 +548,10 @@ export class Document {
536
548
  * Throws on an out-of-range card, a field that is not richtext, a malformed
537
549
  * bundle, or an op that applies out of bounds; the value is unchanged on a
538
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.
539
555
  */
540
556
  applyChange(addr: Addr | string, bundle: ChangeBundle): void;
541
557
  /**
@@ -975,11 +991,31 @@ export function formatDocPath(segs: DocPathSeg[]): string;
975
991
  */
976
992
  export function importMarkdown(markdown: string): Content;
977
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
+
978
1010
  /**
979
1011
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
980
1012
  * offset) through a `delta` to its new position, holding a caret stable across
981
1013
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
982
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`.
983
1019
  */
984
1020
  export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
985
1021
 
@@ -1070,6 +1106,7 @@ export interface InitOutput {
1070
1106
  readonly exportMarkdown: (a: number, b: number) => void;
1071
1107
  readonly formatDocPath: (a: number, b: number) => void;
1072
1108
  readonly importMarkdown: (a: number, b: number, c: number) => void;
1109
+ readonly mapMarks: (a: number, b: number, c: number) => void;
1073
1110
  readonly mapPos: (a: number, b: number, c: number, d: number) => void;
1074
1111
  readonly parseDocPath: (a: number, b: number, c: number) => void;
1075
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
  */
@@ -1749,11 +1753,48 @@ export function importMarkdown(markdown) {
1749
1753
  }
1750
1754
  }
1751
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
+
1752
1789
  /**
1753
1790
  * Map a base content position (a USV index into `Content.text`, not a UTF-16
1754
1791
  * offset) through a `delta` to its new position, holding a caret stable across
1755
1792
  * a `revise`. `assoc` decides the side of a same-position insertion (`"after"`
1756
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`.
1757
1798
  * @param {Delta} delta
1758
1799
  * @param {number} pos
1759
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.110.0",
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
  }
@@ -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
  });