@quillmark/wasm 0.109.0 → 0.109.1

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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.109.1 - 2026-08-24
4
+
5
+ - fix(typst): **a `field-region` claim around inline content no longer widens
6
+ the line.** Each of the helper's two bracketing markers was written
7
+ `#metadata(..) <__qm_region__>`, and the space before the label survived into
8
+ the inline flow. It cost 2.715pt per marker at a 12pt body size, so a claim
9
+ on a date or any mid-paragraph composition shifted the text around it by
10
+ 5.43pt, against the layout-neutral contract the helper documents.
11
+ `form-field`'s marker carries the same shape and drops the space too, rather
12
+ than keep depending on the `box` that follows it.
13
+
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
+
3
25
  ## v0.109.0 - 2026-08-24
4
26
 
5
27
  - **breaking** content: **`Normalized` is the precondition the projections
@@ -159,8 +181,18 @@
159
181
  that needs no discriminator — nearly all of them — keeps its exact bytes and
160
182
  its content hash. **Breaking for Rust consumers** that match `Container`
161
183
  exhaustively: `Quote` is now a struct variant, and `ListItem`/`Unknown` carry
162
- the extra field. On the TypeScript surface `instance` is optional; a consumer
163
- that never writes adjacent same-shape siblings needs no change.
184
+ the extra field.
185
+
186
+ On the TypeScript surface `instance` is optional — the wire omits a zero, so a
187
+ read shape cannot require it — and nothing there stops compiling. A consumer
188
+ that only reads needs no change. **A consumer that writes container paths owes
189
+ the field**, which is every codec flattening a tree: two `bullet_list` nodes in
190
+ a row are adjacent same-shape siblings, and omitting the discriminator lands
191
+ them welded. Such a host keeps producing what it produced on 0.108, so the
192
+ four defects above stay open for it until its codec stamps the discriminator.
193
+ Nothing reports the omission. Two adjacent lines with equal paths are one
194
+ container, which is also how a two-paragraph quote is spelled: the model
195
+ cannot tell a boundary a writer meant from one it did not.
164
196
 
165
197
  The block census counts what the projections see, so two adjacent runs of one
166
198
  shape now count two where they counted one: a quill declining `list` or
@@ -170,10 +202,13 @@
170
202
  A blob written here carries `instance` only where a document holds adjacent
171
203
  same-shape siblings, and a reader that predates the field ignores the key —
172
204
  so such a blob loads on 0.108 with the two runs welded, and re-saving there
173
- drops the boundary for good. The `@0.93.0` tag is unchanged because every
174
- blob written before this release re-encodes byte for byte; the forward
175
- direction is the one that costs, and only for the documents that spend the
176
- key.
205
+ drops the boundary for good. A 0.109 host whose codec drops the key on
206
+ write-back loses it the same way, with no version skew involved. The boundary
207
+ is written by whatever produced the row `from_markdown`, the CLI, any Rust
208
+ caller — and survives only as far as the next writer that carries it.
209
+ The `@0.93.0` tag is unchanged because every blob written before this release
210
+ re-encodes byte for byte; the forward direction is the one that costs, and
211
+ only for the documents that spend the key.
177
212
 
178
213
  - fix(blueprint): **a variant's `object` or `array<object>` cell expands per
179
214
  property.** The cell went through the scalar path, so it rendered as
@@ -248,25 +283,6 @@
248
283
  `island_to_value`, now emit their own keys in a different order. An unknown's `attrs` bag is
249
284
  untouched, as in 0.99, and nothing hashes the op wire.
250
285
 
251
- <!-- seed: commits since v0.108.3, confirm the entries above cover them, then delete this comment
252
- - docs: the 0.108 → 0.109 release record, and two misattached doc comments
253
- - docs: dense-prose pass over the branch
254
- - docs: dense-prose pass over the branch
255
- - fix: close the rest of the normalized-is-not-validated gap
256
- - fix(typst): open every block at the enclosing list indent
257
- - fix(content): walk export's block tree on a frame stack
258
- - docs: dense-prose pass over the branch
259
- - fix: hold the token's guarantee on error paths, and walk census iteratively
260
- - feat(content): make "normalized" a type the projections require
261
- - refactor: consume the shared container traversal
262
- - feat(content): one traversal for container runs and items
263
- - fix(content): an item boundary is a parent boundary
264
- - fix(content): clear a `continues` that crosses a container boundary
265
- - fix(content): give a container the instance its identity was missing
266
- - Bulk PR integration: #1347–#1356 (#1358)
267
- -->
268
-
269
-
270
286
  ## v0.108.3 - 2026-08-21
271
287
 
272
288
  - fix(typst): **a paragraph holding one bare `/` renders instead of failing the
@@ -95,17 +95,42 @@ export type ContentLineKind =
95
95
  * `ContentLine.kind`: an unrecognized container round-trips with opaque `attrs`
96
96
  * and renders transparently (its lines sit at the enclosing level).
97
97
  *
98
- * Two adjacent lines sit in the same container iff their whole path matches.
98
+ * Two adjacent lines sit in the same container iff their whole path matches, so
99
99
  * `instance` is what tells one container from an adjacent sibling of identical
100
- * shape — two consecutive quotes, or two consecutive lists — which contiguity
101
- * alone reads as one. Omit it (or write `0`) unless a path immediately above or
102
- * below is otherwise identical; a write is canonicalized to `0`/`1` on the way
103
- * in, so any distinct pair of values works. */
100
+ * shape — two consecutive quotes, two consecutive lists — which contiguity
101
+ * alone reads as one.
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`.
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. */
104
116
  export type ContentContainer =
105
117
  | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance?: number }
106
118
  | { container: "quote"; instance?: number }
107
119
  | { container: string; attrs: unknown; instance?: number };
108
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 }`.
126
+ *
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 =
130
+ | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance: number }
131
+ | { container: "quote"; instance: number }
132
+ | { container: string; attrs: unknown; instance: number };
133
+
109
134
  /** A mark over char range `[start, end)` into `Content.text`. The open `type`
110
135
  * arm blocks discriminant narrowing, so read a payload-carrying arm behind its
111
136
  * guard: `isLinkMark` (`url`) / `isAnchorMark` (`id`), from
@@ -231,7 +256,7 @@ export type LineOp =
231
256
  | { op: "split"; at: number }
232
257
  | { op: "join"; line: number }
233
258
  | ({ op: "setKind"; line: number } & ContentLineKind)
234
- | { op: "setContainers"; line: number; containers: ContentContainer[] }
259
+ | { op: "setContainers"; line: number; containers: ContentContainerInput[] }
235
260
  | { op: "setContinues"; line: number; continues: boolean };
236
261
 
237
262
  /**
Binary file
@@ -95,17 +95,42 @@ export type ContentLineKind =
95
95
  * `ContentLine.kind`: an unrecognized container round-trips with opaque `attrs`
96
96
  * and renders transparently (its lines sit at the enclosing level).
97
97
  *
98
- * Two adjacent lines sit in the same container iff their whole path matches.
98
+ * Two adjacent lines sit in the same container iff their whole path matches, so
99
99
  * `instance` is what tells one container from an adjacent sibling of identical
100
- * shape — two consecutive quotes, or two consecutive lists — which contiguity
101
- * alone reads as one. Omit it (or write `0`) unless a path immediately above or
102
- * below is otherwise identical; a write is canonicalized to `0`/`1` on the way
103
- * in, so any distinct pair of values works. */
100
+ * shape — two consecutive quotes, two consecutive lists — which contiguity
101
+ * alone reads as one.
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`.
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. */
104
116
  export type ContentContainer =
105
117
  | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance?: number }
106
118
  | { container: "quote"; instance?: number }
107
119
  | { container: string; attrs: unknown; instance?: number };
108
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 }`.
126
+ *
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 =
130
+ | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance: number }
131
+ | { container: "quote"; instance: number }
132
+ | { container: string; attrs: unknown; instance: number };
133
+
109
134
  /** A mark over char range `[start, end)` into `Content.text`. The open `type`
110
135
  * arm blocks discriminant narrowing, so read a payload-carrying arm behind its
111
136
  * guard: `isLinkMark` (`url`) / `isAnchorMark` (`id`), from
@@ -231,7 +256,7 @@ export type LineOp =
231
256
  | { op: "split"; at: number }
232
257
  | { op: "join"; line: number }
233
258
  | ({ op: "setKind"; line: number } & ContentLineKind)
234
- | { op: "setContainers"; line: number; containers: ContentContainer[] }
259
+ | { op: "setContainers"; line: number; containers: ContentContainerInput[] }
235
260
  | { op: "setContinues"; line: number; continues: boolean };
236
261
 
237
262
  /**
Binary file
package/core/wasm.d.ts CHANGED
@@ -95,17 +95,42 @@ export type ContentLineKind =
95
95
  * `ContentLine.kind`: an unrecognized container round-trips with opaque `attrs`
96
96
  * and renders transparently (its lines sit at the enclosing level).
97
97
  *
98
- * Two adjacent lines sit in the same container iff their whole path matches.
98
+ * Two adjacent lines sit in the same container iff their whole path matches, so
99
99
  * `instance` is what tells one container from an adjacent sibling of identical
100
- * shape — two consecutive quotes, or two consecutive lists — which contiguity
101
- * alone reads as one. Omit it (or write `0`) unless a path immediately above or
102
- * below is otherwise identical; a write is canonicalized to `0`/`1` on the way
103
- * in, so any distinct pair of values works. */
100
+ * shape — two consecutive quotes, two consecutive lists — which contiguity
101
+ * alone reads as one.
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`.
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. */
104
116
  export type ContentContainer =
105
117
  | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance?: number }
106
118
  | { container: "quote"; instance?: number }
107
119
  | { container: string; attrs: unknown; instance?: number };
108
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 }`.
126
+ *
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 =
130
+ | { container: "list_item"; ordered: boolean; start: number; ordinal: number; instance: number }
131
+ | { container: "quote"; instance: number }
132
+ | { container: string; attrs: unknown; instance: number };
133
+
109
134
  /** A mark over char range `[start, end)` into `Content.text`. The open `type`
110
135
  * arm blocks discriminant narrowing, so read a payload-carrying arm behind its
111
136
  * guard: `isLinkMark` (`url`) / `isAnchorMark` (`id`), from
@@ -231,7 +256,7 @@ export type LineOp =
231
256
  | { op: "split"; at: number }
232
257
  | { op: "join"; line: number }
233
258
  | ({ op: "setKind"; line: number } & ContentLineKind)
234
- | { op: "setContainers"; line: number; containers: ContentContainer[] }
259
+ | { op: "setContainers"; line: number; containers: ContentContainerInput[] }
235
260
  | { op: "setContinues"; line: number; continues: boolean };
236
261
 
237
262
  /**
package/core/wasm_bg.wasm CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/wasm",
3
- "version": "0.109.0",
3
+ "version": "0.109.1",
4
4
  "description": "WebAssembly bindings for Quillmark, a schema-driven document engine",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -130,6 +130,7 @@ export type {
130
130
  ContentLine,
131
131
  ContentLineKind,
132
132
  ContentContainer,
133
+ ContentContainerInput,
133
134
  ContentMark,
134
135
  ContentIsland,
135
136
  TableProps,
@@ -204,7 +205,8 @@ import type {
204
205
  ImageProps,
205
206
  ContentMark,
206
207
  ContentLine,
207
- ContentContainer
208
+ ContentContainer,
209
+ ContentContainerInput
208
210
  } from '../core/wasm.js';
209
211
 
210
212
  /** Narrow a {@link ContentIsland} to the pinned `table` arm (`props: TableProps`). */
@@ -278,6 +280,41 @@ export declare function isUnknownIsland(
278
280
  island: ContentIsland
279
281
  ): island is ContentIsland & { type: string; props: unknown };
280
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.
288
+
289
+ /**
290
+ * Stamp `instance` across one parent's blocks at one depth, in document order,
291
+ * returning containers ready to write.
292
+ *
293
+ * One entry per container RUN — a list, not a list item — and `null` for a
294
+ * block carrying no container at this depth. A bare paragraph between two lists
295
+ * is such a block, and separates them on its own. Every line of a run then
296
+ * carries that run's returned container, `ordinal` varying per item and
297
+ * `instance` held.
298
+ *
299
+ * The `instance` it stamps is canonical, so a document reads back the value it
300
+ * was written. `ordinal` stays the caller's, and a write is renumbered to a
301
+ * gapless index within its run.
302
+ *
303
+ * Which fields decide a weld is coarser than equality for a list: CommonMark
304
+ * reads only a list's first number, so `1. a` beside `3. b` welds despite the
305
+ * differing `start`.
306
+ *
307
+ * ```js
308
+ * const [outer, , inner] = assignInstances([listA, null, listB]);
309
+ * // outer.instance === 0, inner.instance === 0 — the paragraph parts them
310
+ * const [a, b] = assignInstances([listA, listB]);
311
+ * // a.instance === 0, b.instance === 1 — adjacent, one shape
312
+ * ```
313
+ */
314
+ export declare function assignInstances(
315
+ runs: (ContentContainer | null)[]
316
+ ): (ContentContainerInput | null)[];
317
+
281
318
  // The backend-neutral render contract, defined here rather than re-exported from
282
319
  // one private backend because no single backend owns the canonical API's types.
283
320
  // Every backend build must satisfy these shapes; `runtime.types.test-d.ts` keeps
@@ -510,6 +510,64 @@ export function isUnknownIsland(island) {
510
510
  return typeof island?.type === 'string' && !KNOWN_ISLAND_TYPES.has(island.type);
511
511
  }
512
512
 
513
+ // ── Container run boundaries ────────────────────────────────────────────────
514
+ // `ContentContainer.instance` is what keeps two adjacent runs of one shape
515
+ // apart, and only a writer knows where a boundary is: the flat `containers`
516
+ // form cannot tell a list ending beside another from one list of two items, so
517
+ // an omitted discriminator welds them and nothing reports it.
518
+ //
519
+ // WELD_KEYS is the rule `Container::same_weld` owns upstream: which fields two
520
+ // adjacent runs must share for the markdown projection to read them as one, and
521
+ // therefore for the canonical form to have to spend a discriminator. `start` is
522
+ // not among them, since CommonMark reads only a list's first number. A table
523
+ // rather than a switch, so `tests/known_names_drift.rs` can pin it against the
524
+ // Rust predicate.
525
+
526
+ const WELD_KEYS = { list_item: ['ordered'], quote: [] };
527
+
528
+ function sameJson(a, b) {
529
+ if (a === b) return true;
530
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
531
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
532
+ const ka = Object.keys(a);
533
+ return (
534
+ ka.length === Object.keys(b).length &&
535
+ ka.every((k) => Object.hasOwn(b, k) && sameJson(a[k], b[k]))
536
+ );
537
+ }
538
+
539
+ /**
540
+ * @param {import('../core/wasm.js').ContentContainer} a
541
+ * @param {import('../core/wasm.js').ContentContainer} b
542
+ * @returns {boolean}
543
+ */
544
+ function weldsWith(a, b) {
545
+ // A malformed value welds with nothing. The membership guards' posture:
546
+ // answer rather than throw.
547
+ if (typeof a?.container !== 'string' || a.container !== b?.container) return false;
548
+ // `hasOwn`, so a tag colliding with an `Object.prototype` member reaches the
549
+ // unknown branch rather than a function.
550
+ if (!Object.hasOwn(WELD_KEYS, a.container)) return sameJson(a.attrs, b.attrs);
551
+ return WELD_KEYS[a.container].every((k) => a[k] === b[k]);
552
+ }
553
+
554
+ /**
555
+ * @param {(import('../core/wasm.js').ContentContainer | null)[]} runs
556
+ * @returns {(import('../core/wasm.js').ContentContainerInput | null)[]}
557
+ */
558
+ export function assignInstances(runs) {
559
+ let prev = null;
560
+ return runs.map((run) => {
561
+ if (run == null) {
562
+ prev = null;
563
+ return null;
564
+ }
565
+ const instance = prev && weldsWith(prev, run) ? 1 - prev.instance : 0;
566
+ prev = { ...run, instance };
567
+ return prev;
568
+ });
569
+ }
570
+
513
571
  /**
514
572
  * Build a `load` thunk: dynamic-import a backend build, then instantiate it.
515
573
  *