@quillmark/wasm 0.92.0 → 0.94.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.
@@ -24,10 +24,11 @@ export type PayloadItem =
24
24
  | { type: "comment"; text: string; inline?: boolean };
25
25
 
26
26
  /**
27
- * A single card block. The one shape exchanged in both directions: returned by
28
- * `Document.main` / `Document.cards` / `Document.removeCard` / `Quill.seedCard`,
29
- * and accepted by `Document.pushCard` / `Document.insertCard`. Build a fresh
30
- * one with `Document.makeCard`.
27
+ * A single card block, as read back from a document: returned by
28
+ * `Document.main` / `Document.cards` / `Document.removeCard` / `Quill.seedCard`
29
+ * / `Document.makeCard`. To feed a card *into* a document use `CardInput`
30
+ * (which `pushCard` / `insertCard` accept); every `Card` is a valid `CardInput`,
31
+ * so a card read from one document pushes straight into another.
31
32
  *
32
33
  * `$` system entries are hoisted to named fields: `kind` (the `$kind`, empty
33
34
  * string when none), optional `quill` (the `$quill` `name@version`, main card
@@ -42,13 +43,146 @@ export interface Card {
42
43
  ext?: Record<string, unknown>;
43
44
  seed?: Record<string, unknown>;
44
45
  payloadItems: PayloadItem[];
45
- body: string;
46
+ /**
47
+ * The card body as canonical `RichText` — the source-of-truth content model.
48
+ * Always this corpus shape on read, never a markdown string. For the markdown
49
+ * projection call the codec `exportMarkdown(card.body)`. Write a body back
50
+ * with `doc.install(addr, rt)` / `doc.revise(addr, md)`, or via `CardInput.body`.
51
+ */
52
+ body: RichText;
53
+ }
54
+
55
+ /**
56
+ * A card written *into* a document — the input twin of `Card`, accepted by
57
+ * `Document.pushCard` / `Document.insertCard`. Like `Card` but `body` also
58
+ * takes a markdown `string` (imported to the corpus, so a markdown / LLM writer
59
+ * needn't build the `RichText` shape), and every field but `kind` is optional —
60
+ * an absent field defaults (no payload items, an empty body). Write one inline
61
+ * (`{ kind, body }`) or build it with `Document.makeCard`.
62
+ */
63
+ export interface CardInput {
64
+ kind: string;
65
+ quill?: string;
66
+ id?: string;
67
+ ext?: Record<string, unknown>;
68
+ seed?: Record<string, unknown>;
69
+ payloadItems?: PayloadItem[];
70
+ body?: RichText | string;
71
+ }
72
+
73
+ /**
74
+ * Canonical richtext corpus — the content model for a card body (and richtext
75
+ * fields). One text sequence over a single coordinate space (Unicode scalar
76
+ * values): `text` plus line attributes, anchored `marks`, and embedded
77
+ * `islands`. Every edit is a splice; markdown is a projection, not the model.
78
+ * Mirrors `quillmark_richtext::serial`'s canonical JSON encoding.
79
+ */
80
+ export interface RichText {
81
+ text: string;
82
+ lines: RichTextLine[];
83
+ marks: RichTextMark[];
84
+ islands: RichTextIsland[];
46
85
  }
47
86
 
87
+ /** One `\n`-separated segment of `RichText.text`, in order. */
88
+ export type RichTextLine = {
89
+ containers: RichTextContainer[];
90
+ /** A within-block hard line break rather than a new block. Omitted (false) in the common case. */
91
+ continues?: boolean;
92
+ } & (
93
+ | { kind: "para" }
94
+ | { kind: "heading"; level: number }
95
+ | { kind: "code"; lang?: string }
96
+ | { kind: "island" }
97
+ | { kind: "rule" }
98
+ );
99
+
100
+ /** An ancestor block a line nests inside, outermost first. */
101
+ export type RichTextContainer =
102
+ | { container: "list_item"; ordered: boolean; start: number; ordinal: number }
103
+ | { container: "quote" };
104
+
105
+ /** A mark over char range `[start, end)` into `RichText.text`. */
106
+ export type RichTextMark = { start: number; end: number } & (
107
+ | { type: "strong" | "emph" | "underline" | "strike" | "code" }
108
+ | { type: "link"; url: string }
109
+ | { type: "anchor"; id: string }
110
+ | { type: string; attrs: unknown }
111
+ );
112
+
113
+ /** A structured object (table, figure, …) occupying one island slot in `RichText.text`. */
114
+ export interface RichTextIsland {
115
+ id: string;
116
+ type: string;
117
+ props: unknown;
118
+ /** How faithfully the markdown projection can carry this island. */
119
+ loss: "lossless" | "degraded" | "unrepresentable";
120
+ }
48
121
 
122
+ /**
123
+ * A richtext write address. An absent `field` targets the card body; an absent
124
+ * `card` targets the main card. `{}` is the main-card body; `{ card: 2 }` the
125
+ * body of the composable card at index 2; `{ field: "intro" }` the main card's
126
+ * `intro` richtext field; `{ card: 2, field: "intro" }` a card field.
127
+ */
128
+ export interface Addr {
129
+ card?: number;
130
+ field?: string;
131
+ }
49
132
 
50
133
  /**
51
- * Page dimensions in Typst points (1 pt = 1/72 inch).
134
+ * A text-splice change set over the USV corpus (CodeMirror `ChangeSet`
135
+ * semantics) — plain, structured-clone-able data. Returned by `revise` and by
136
+ * the `rebase` codec; map a stored position through it with `mapPos`.
137
+ */
138
+ export interface Delta {
139
+ ops: ({ retain: number } | { insert: string } | { delete: number })[];
140
+ }
141
+
142
+ /** Which side of a same-position insertion `mapPos` lands a point on. */
143
+ export type Assoc = "before" | "after";
144
+
145
+ /**
146
+ * A mark edit in post-text-delta coordinates. `add` / `remove` carry the
147
+ * `RichTextMark` vocabulary (`{ type, … }`); `removeAnchor` drops one identity
148
+ * anchor by id.
149
+ */
150
+ export type MarkOp =
151
+ | ({ op: "add" | "remove"; start: number; end: number } & (
152
+ | { type: "strong" | "emph" | "underline" | "strike" | "code" }
153
+ | { type: "link"; url: string }
154
+ | { type: "anchor"; id: string }
155
+ | { type: string; attrs: unknown }
156
+ ))
157
+ | { op: "removeAnchor"; id: string };
158
+
159
+ /** A line/block edit. `split`/`join` splice `\n`; `setKind`/`setContainers` touch metadata. */
160
+ export type LineOp =
161
+ | { op: "split"; at: number }
162
+ | { op: "join"; line: number }
163
+ | ({ op: "setKind"; line: number } & (
164
+ | { kind: "para" | "island" | "rule" }
165
+ | { kind: "heading"; level: number }
166
+ | { kind: "code"; lang?: string }
167
+ ))
168
+ | { op: "setContainers"; line: number; containers: RichTextContainer[] };
169
+
170
+ /**
171
+ * A committed corpus edit bundle for `applyChange`: a text `delta` (default no
172
+ * text change), then `lineOps`, then `markOps` (mark ranges are in post-delta
173
+ * coordinates). Every field is optional.
174
+ */
175
+ export interface ChangeBundle {
176
+ delta?: Delta;
177
+ lineOps?: LineOp[];
178
+ markOps?: MarkOp[];
179
+ }
180
+
181
+
182
+
183
+ /**
184
+ * Page dimensions in points (1 pt = 1/72 inch). Typst measures in Typst
185
+ * points; pdfform measures in PDF points — the same unit.
52
186
  *
53
187
  * Report-only: the painter sizes the canvas itself based on
54
188
  * `PaintOptions`. `pageSize` is exposed for callers that need page
@@ -61,10 +195,11 @@ export interface PageSize {
61
195
  }
62
196
 
63
197
  /**
64
- * Inputs to `RenderSession.paint`. Both fields are optional and default
198
+ * Inputs to `LiveSession.paint`. Both fields are optional and default
65
199
  * to `1`.
66
200
  *
67
- * - `layoutScale` — layout-space pixels per Typst point. For on-screen
201
+ * - `layoutScale` — layout-space pixels per point (Typst point / PDF
202
+ * point — the same 1/72″ unit). For on-screen
68
203
  * canvases this is CSS pixels per pt; the page's layout-pixel size is
69
204
  * `widthPt * layoutScale × heightPt * layoutScale`. The painter
70
205
  * surfaces these dimensions as `layoutWidth` / `layoutHeight` so
@@ -86,7 +221,7 @@ export interface PaintOptions {
86
221
  }
87
222
 
88
223
  /**
89
- * Returned by `RenderSession.paint`.
224
+ * Returned by `LiveSession.paint`.
90
225
  *
91
226
  * - `layoutWidth` / `layoutHeight` — layout-pixel dimensions of the
92
227
  * canvas's display box. For on-screen canvases this is CSS pixels:
@@ -98,12 +233,22 @@ export interface PaintOptions {
98
233
  * Equal to `round(layoutWidth * densityScale)` ×
99
234
  * `round(layoutHeight * densityScale)` *unless* the requested backing
100
235
  * exceeded the painter's safe maximum (16384 px per side), in which
101
- * case `densityScale` was clamped to fit. Detect clamping via
102
- * `pixelWidth < round(layoutWidth * densityScale)`.
236
+ * case `densityScale` was clamped to fit.
237
+ * - `clamped` `true` when that 16384-px clamp fired, so the page is
238
+ * painted at fewer device pixels than requested and renders soft at the
239
+ * same `canvas.style` size. Reads the clamp off the return value instead
240
+ * of the `pixelWidth < round(layoutWidth * densityScale)` derivation.
241
+ * - `effectiveDensityScale` — the `densityScale` actually applied: the
242
+ * requested value unless `clamped`, then reduced proportionally.
243
+ * `layoutScale * effectiveDensityScale` is the scale the backing store
244
+ * was rasterized at.
103
245
  *
104
246
  * The painter owns `canvas.width` / `canvas.height`; consumers must not
105
247
  * write to them. The painter does **not** touch `canvas.style.*`;
106
- * consumers own layout.
248
+ * consumers own layout. The write is a whole-backing-store `putImageData`,
249
+ * which bypasses the 2D context transform, `globalAlpha`, and clip: give
250
+ * each visible page its own `` — you cannot composite two pages, a
251
+ * sub-rect, or a context transform through `paint`.
107
252
  *
108
253
  * For `OffscreenCanvasRenderingContext2D` (Worker rasterization, no
109
254
  * DOM), `layoutWidth` / `layoutHeight` are informational — there's no
@@ -114,14 +259,18 @@ export interface PaintResult {
114
259
  layoutHeight: number;
115
260
  pixelWidth: number;
116
261
  pixelHeight: number;
262
+ clamped: boolean;
263
+ effectiveDensityScale: number;
117
264
  }
118
265
 
119
266
 
120
267
 
121
- /** UI layout hints for a single field. */
268
+ /** UI layout hints for a single field. Field display order is not a hint:
269
+ * key order in the schema's `fields`/`properties` objects is declaration
270
+ * order, the ordering contract. */
122
271
  export interface QuillFieldUi {
272
+ title?: string;
123
273
  group?: string;
124
- order?: number;
125
274
  compact?: boolean;
126
275
  multiline?: boolean;
127
276
  }
@@ -144,20 +293,28 @@ export interface QuillCardBody {
144
293
  * A field's *cell* is determined by `default`: a field with a `default`
145
294
  * is **Endorsed** (the rendered value is shippable as-is), while a field
146
295
  * without a `default` is **Unendorsed** (the blueprint carries a
147
- * `<must-fill>` sentinel and validation reports
148
- * `validation::field_absent` if the field is absent at validate
149
- * time — a non-fatal signal, since the render path zero-fills an absent
150
- * field). There is no separate `required` axis.
296
+ * `!must_fill` marker; a marker left in the document yields the non-fatal
297
+ * `validation::must_fill` warning from validate, and the render path
298
+ * zero-fills the field). There is no separate `required` axis.
151
299
  */
152
300
  export interface QuillFieldSchema {
153
- type: "string" | "number" | "integer" | "boolean" | "array" | "object" | "datetime" | "markdown";
301
+ type: "string" | "number" | "integer" | "boolean" | "array" | "object" | "datetime" | "richtext" | "plaintext" | "enum";
154
302
  description?: string;
155
303
  default?: unknown;
156
304
  example?: unknown;
305
+ /** Closed value domain. On `type: "enum"` declared as `values`; the
306
+ * deprecated `enum` modifier on `type: "string"` is accepted for one
307
+ * release. Both round-trip through this field. */
157
308
  enum?: string[];
309
+ /** Required on `type: "enum"`: the closed set of allowed string values. */
310
+ values?: string[];
158
311
  ui?: QuillFieldUi;
159
312
  properties?: Record<string, QuillFieldSchema>;
160
313
  items?: QuillFieldSchema;
314
+ /** Present (and `true`) on a `richtext` or `plaintext` field declared
315
+ * `inline` — the single-paragraph, container-free, island-free constraint.
316
+ * Core serializes `inline: true` into the schema JSON; absent otherwise. */
317
+ inline?: boolean;
161
318
  }
162
319
 
163
320
  /** Schema entry for the main card or a named card kind. */
@@ -202,6 +359,17 @@ export interface Artifact {
202
359
  mimeType: string;
203
360
  }
204
361
 
362
+ export interface ChangeSet {
363
+ pageCount: number;
364
+ dirtyPages: number[];
365
+ }
366
+
367
+ export interface CorpusHit {
368
+ field: string;
369
+ pos: number;
370
+ granularity?: HitGranularity;
371
+ }
372
+
205
373
  export interface Diagnostic {
206
374
  severity: Severity;
207
375
  code?: string;
@@ -212,6 +380,13 @@ export interface Diagnostic {
212
380
  sourceChain?: string[];
213
381
  }
214
382
 
383
+ export interface FieldRegion {
384
+ field: string;
385
+ page: number;
386
+ rect: [number, number, number, number];
387
+ span?: [number, number];
388
+ }
389
+
215
390
  export interface Location {
216
391
  file: string;
217
392
  line: number;
@@ -223,6 +398,7 @@ export interface RenderOptions {
223
398
  ppi?: number;
224
399
  pages?: number[];
225
400
  producer?: string;
401
+ regions?: boolean;
226
402
  }
227
403
 
228
404
  export interface RenderResult {
@@ -230,20 +406,45 @@ export interface RenderResult {
230
406
  warnings: Diagnostic[];
231
407
  outputFormat: OutputFormat;
232
408
  renderTimeMs: number;
409
+ regions: FieldRegion[];
233
410
  }
234
411
 
412
+ export type HitGranularity = "cluster" | "segment";
413
+
235
414
  export type OutputFormat = "pdf" | "svg" | "txt" | "png";
236
415
 
237
- export type Severity = "error" | "warning" | "note";
416
+ export type Severity = "error" | "warning";
238
417
 
239
418
 
240
419
  /**
241
420
  * Typed in-memory Quillmark document.
242
421
  */
243
422
  export class Document {
244
- private constructor();
245
423
  free(): void;
246
424
  [Symbol.dispose](): void;
425
+ /**
426
+ * Build a composable card of `kind`, typed-commit `fields` onto it, set its
427
+ * body from optional markdown, and append it — the ABI under
428
+ * `writer.addCard`. Fuses `makeCard` + typed commit + `pushCard`
429
+ * transactionally: the card is committed in full before it joins the
430
+ * document, so a rejected field (or an invalid kind or body) leaves the
431
+ * document untouched. Field errors throw the same per-field diagnostic
432
+ * bundle as [`commitFields`](Self::commit_fields), including an
433
+ * `[EditError::UnknownField]` per undeclared name; an invalid kind or body
434
+ * throws a single-entry bundle keyed `$kind` / `$body`.
435
+ */
436
+ addCard(quill: Quill, kind: string, fields?: Record<string, unknown>, body?: string): void;
437
+ /**
438
+ * **Apply** a committed corpus edit `bundle` (`{ delta?, lineOps?, markOps? }`)
439
+ * at `addr` — the editor splice: text delta first, then line ops, then mark
440
+ * ops (mark ranges in post-delta coordinates), each all-or-nothing. An absent
441
+ * `addr.field` targets the body, an absent `addr.card` the main card.
442
+ *
443
+ * Throws on an out-of-range card, a field that is not richtext, a malformed
444
+ * bundle, or an op that applies out of bounds (the value is unchanged on a
445
+ * failed apply).
446
+ */
447
+ applyChange(addr: Addr, bundle: ChangeBundle): void;
247
448
  /**
248
449
  * Authoring-ergonomics header introducing a blueprint to an LLM/MCP
249
450
  * consumer for the given `quillName`. Re-exposes core's canonical text for
@@ -252,6 +453,60 @@ export class Document {
252
453
  */
253
454
  static blueprintInstruction(quill_name: string): string;
254
455
  clone(): Document;
456
+ /**
457
+ * Typed field write on the composable card at `index` — the card-indexed
458
+ * twin of [`commitField`](Document::commit_field). Resolves the field's
459
+ * type from the card's `$kind` schema in `quill` and strict-commits it.
460
+ *
461
+ * Throws `[EditError::IndexOutOfRange]` when `index` is out of range, and
462
+ * the same typed-mismatch / name errors as `commitField` — including
463
+ * `[EditError::UnknownField]` for a field the card-kind schema does not
464
+ * declare (an unknown `$kind` has no schema, so every field is undeclared).
465
+ */
466
+ commitCardField(quill: Quill, index: number, name: string, value: any): void;
467
+ /**
468
+ * Batched twin of [`commitCardField`](Document::commit_card_field):
469
+ * typed-commit several fields on the card at `index` atomically, resolving
470
+ * each field's type from the card's `$kind` schema in `quill`. All-or-nothing
471
+ * with the same per-field-diagnostic contract as
472
+ * [`commitFields`](Document::commit_fields), including an
473
+ * `[EditError::UnknownField]` diagnostic per undeclared name. Throws
474
+ * `[EditError::IndexOutOfRange]` when `index` is out of range.
475
+ */
476
+ commitCardFields(quill: Quill, index: number, fields: Record<string, unknown>): void;
477
+ /**
478
+ * Typed field write on the main card, resolving the field's schema `type`
479
+ * from `quill` — the one write verb for **every** field type (richtext,
480
+ * scalar, array, object). The schema carries the `inline` constraint, so no
481
+ * type token or flag is passed. A richtext-typed field stores the canonical
482
+ * corpus, so identity marks (anchors, island ids) and corpus-only marks
483
+ * (e.g. `underline`) live on it and survive compiles and the storage DTO.
484
+ * Values use the encoding the seam already speaks: a corpus object
485
+ * or markdown string for richtext, a scalar/array/object otherwise.
486
+ *
487
+ * A field declared in the schema is strict-committed — a mismatch throws
488
+ * now, not at render. A name the schema does not declare throws
489
+ * `[EditError::UnknownField]` rather than falling to the opaque store: on
490
+ * the typed path it is a typo. Use [`setField`](Document::set_field) when
491
+ * opaque storage is the intent. Also throws `[EditError::FieldConform]` /
492
+ * `[EditError::FieldRichtextDecode]` / `[EditError::FieldRichtextNotInline]`
493
+ * on a typed mismatch and `[EditError::InvalidFieldName]` on a malformed
494
+ * name.
495
+ *
496
+ * The `quill` handle is passed per call because a `Document` carries only a
497
+ * `$quill` reference, not the resolved schema.
498
+ */
499
+ commitField(quill: Quill, name: string, value: any): void;
500
+ /**
501
+ * Batched twin of [`commitField`](Document::commit_field): typed-commit
502
+ * several main-card fields atomically, resolving each field's schema `type`
503
+ * from `quill`. All-or-nothing with the same per-field-diagnostic error
504
+ * contract as [`setFields`](Document::set_fields) — nothing is applied on
505
+ * error and the thrown error's `diagnostics` carry one entry per offending
506
+ * field, including an `[EditError::UnknownField]` for any name the schema
507
+ * does not declare, so a whole-form submit sees every typo in one pass.
508
+ */
509
+ commitFields(quill: Quill, fields: Record<string, unknown>): void;
255
510
  /**
256
511
  * Schema version this build writes via [`toJson`](Document::to_json).
257
512
  * Tracks the `Document` model version (not the running crate version):
@@ -290,11 +545,53 @@ export class Document {
290
545
  * Parse markdown into a typed Document. Throws on parse errors.
291
546
  */
292
547
  static fromMarkdown(markdown: string): Document;
548
+ /**
549
+ * Read a main-card field's stored value — the raw payload value (a corpus
550
+ * object for a richtext field, a scalar/array/object otherwise), or
551
+ * `undefined` when the field is absent. The quill-free read: reads need no
552
+ * schema, so they live on `Document`, not the typed writer. For the markdown
553
+ * projection of a richtext value use [`getMarkdown`](Self::get_markdown).
554
+ */
555
+ get(name: string): any;
556
+ /**
557
+ * The markdown projection of a main-card field (`name` given) or the main
558
+ * body (`name` omitted) — the on-demand, lossy export (corpus-only marks do
559
+ * not survive markdown), returning `""` for an absent field. Re-coins,
560
+ * lazily and by name, the projection the eager `fieldMarkdown` /
561
+ * `bodyMarkdown` getters dropped in #925; call it only when markdown is what
562
+ * you need out.
563
+ */
564
+ getMarkdown(name?: string): string;
293
565
  /**
294
566
  * Insert a card at `index` (must be in `0..=cards.length`). Accepts a
295
- * `Card` (see [`pushCard`](Self::push_card)).
567
+ * `CardInput` (see [`pushCard`](Self::push_card)).
568
+ */
569
+ insertCard(index: number, card: CardInput): void;
570
+ /**
571
+ * **Install** a richtext value at `addr` — **value semantics**, corpus only.
572
+ * Stores exactly `rt` (a canonical `RichText` corpus object); the identity
573
+ * anchors of any previous value are gone. An absent `addr.field` targets the
574
+ * body, an absent `addr.card` the main card. For "here's new markdown," use
575
+ * [`revise`](Document::revise); the cold-import path is spelled at the call
576
+ * site as `install(addr, importMarkdown(md))`, so anchor loss is visible in
577
+ * source.
578
+ *
579
+ * Throws on an out-of-range card, a malformed field name, or an `rt` that is
580
+ * not a canonical corpus object.
296
581
  */
297
- insertCard(index: number, card: Card): void;
582
+ install(addr: Addr, rt: RichText): void;
583
+ /**
584
+ * Replace this document's contents **in place** from a versioned storage
585
+ * DTO string — the mutating twin of the static
586
+ * [`fromJson`](Document::from_json) constructor. Parse-time `warnings` are
587
+ * cleared. Throws (leaving the document unchanged) on an invalid DTO.
588
+ *
589
+ * The cross-WASM-memory `Document` bridge: mutate a document on a
590
+ * backend-memory clone, then write the mutated state back into the caller's
591
+ * canonical document with this — the one way to update a live handle across
592
+ * the linear-memory seam without the caller re-binding its variable.
593
+ */
594
+ loadJson(json: string): void;
298
595
  /**
299
596
  * Build a fresh `Card` from a kind and a flat field map — the ergonomic
300
597
  * constructor for `pushCard` / `insertCard`. `fields` is an optional
@@ -308,12 +605,22 @@ export class Document {
308
605
  */
309
606
  moveCard(from: number, to: number): void;
310
607
  /**
311
- * Append a card to the end of the card list. Accepts a `Card` (the shape
312
- * returned by `cards` / `removeCard` / `quill.seedCard`); build a fresh
313
- * one with [`Document.makeCard`](Document::make_card). Throws if
314
- * `card.kind` is not a valid kind name.
608
+ * `new Document(quillRef)` a blank document: a main card carrying only
609
+ * `$quill`, an empty body, and no composable cards. The programmatic
610
+ * blank canvas: absent fields resolve at render time (`default`, else
611
+ * type-empty zero), so nothing the caller did not set reaches the
612
+ * output. For an example-filled starter use `Quill.seedDocument()`.
613
+ * Throws on an invalid quill reference. Mirrors Python `Document(quill_ref)`.
614
+ */
615
+ constructor(quill_ref: string);
616
+ /**
617
+ * Append a card to the end of the card list. Accepts a `CardInput` — a card
618
+ * read back (`cards` / `removeCard` / `quill.seedCard`), a
619
+ * [`makeCard`](Document::make_card) result, or a bare `{ kind, body }`
620
+ * (every returned `Card` is a valid `CardInput`). Throws if `card.kind` is
621
+ * not a valid kind name.
315
622
  */
316
- pushCard(card: Card): void;
623
+ pushCard(card: CardInput): void;
317
624
  /**
318
625
  * The canonical `$quill` reference grammar as author-facing text. Core is
319
626
  * the single source of truth: drive schema `describe` and validation
@@ -356,7 +663,7 @@ export class Document {
356
663
  removeExtNamespace(namespace: string): any;
357
664
  /**
358
665
  * Remove a payload field on the main card, returning the removed value or
359
- * `undefined`. Throws if `name` does not match `[a-z_][a-z0-9_]*`.
666
+ * `undefined`. Throws if `name` does not match `[A-Za-z_][A-Za-z0-9_]*`.
360
667
  */
361
668
  removeField(name: string): any;
362
669
  /**
@@ -365,7 +672,25 @@ export class Document {
365
672
  * kinds survive.
366
673
  */
367
674
  removeSeedNamespace(card_kind: string): any;
675
+ /**
676
+ * **Deprecated** — alias for `revise({}, markdown)`, kept one release cycle.
677
+ * Revise the main card's body from a markdown string (edit semantics: a
678
+ * `diff_import` that rebases surviving anchors). Discards the text delta;
679
+ * call [`revise`](Document::revise) to receive it.
680
+ */
368
681
  replaceBody(body: string): void;
682
+ /**
683
+ * **Revise** the richtext value at `addr` from a markdown string — **edit
684
+ * semantics**, the default write path, returning the text [`Delta`]. Imports
685
+ * the markdown, diffs it against the current value, rebases surviving
686
+ * identity anchors, and returns the change an editor bridge maps its own
687
+ * positions through (`mapPos`). An absent `addr.field` targets the body, an
688
+ * absent `addr.card` the main card; an absent field cold-imports from empty.
689
+ *
690
+ * Throws on an out-of-range card, a malformed field name, a present
691
+ * non-corpus field value, or an over-nested markdown input.
692
+ */
693
+ revise(addr: Addr, markdown: string): Delta;
369
694
  /**
370
695
  * Read the `schema` version tag from a raw storage DTO string without a
371
696
  * full parse, or `undefined`. Returns unknown future versions as-is —
@@ -385,6 +710,19 @@ export class Document {
385
710
  * Throws if out of range or `value` cannot be serialized.
386
711
  */
387
712
  setCardExtNamespace(index: number, namespace: string, value: any): void;
713
+ /**
714
+ * Set a field on the card at `index` — the card-indexed twin of
715
+ * [`setField`](Document::set_field). Stores the value opaquely.
716
+ * Throws if `index` is out of range, `name` is reserved or invalid.
717
+ */
718
+ setCardField(index: number, name: string, value: any): void;
719
+ /**
720
+ * Batched twin of [`setCardField`](Document::set_card_field): set
721
+ * several fields on the card at `index` atomically. Same all-or-nothing,
722
+ * one-diagnostic-per-field contract as [`setFields`](Document::set_fields).
723
+ * Throws if `index` is out of range.
724
+ */
725
+ setCardFields(index: number, fields: Record<string, unknown>): void;
388
726
  /**
389
727
  * Replace the kind of the card at `index`. Payload and body are untouched;
390
728
  * schema-aware migration is the caller's responsibility.
@@ -401,16 +739,25 @@ export class Document {
401
739
  /**
402
740
  * Merge `value` into the main card's `$ext` map under `namespace`, creating
403
741
  * the map when absent and replacing any existing value at that key. Sibling
404
- * namespaces are preserved, so independent consumers (`$ext.presentation`,
742
+ * namespaces are preserved, so independent consumers (`$ext.editor`,
405
743
  * `$ext.agent`, …) don't clobber each other.
406
744
  */
407
745
  setExtNamespace(namespace: string, value: any): void;
408
746
  /**
409
747
  * Update a payload field on the main card. Clears any existing `!must_fill` marker.
410
748
  *
411
- * Throws if `name` does not match `[a-z_][a-z0-9_]*`.
749
+ * Throws if `name` does not match `[A-Za-z_][A-Za-z0-9_]*`.
412
750
  */
413
751
  setField(name: string, value: any): void;
752
+ /**
753
+ * Set several main-card payload fields atomically from a plain object,
754
+ * clearing any `!must_fill` marker on each key. Nothing is applied on
755
+ * error; the thrown error's `diagnostics` array carries one entry per
756
+ * offending field (`path` = field name), so externally-sourced names
757
+ * (database columns, form keys) surface every violation in one pass.
758
+ * Mirrors Python `set_fields`.
759
+ */
760
+ setFields(fields: Record<string, unknown>): void;
414
761
  /**
415
762
  * Update a payload field on the main card and mark it as `!must_fill`.
416
763
  * Throws on invalid name (see [`setField`](Document::set_field)).
@@ -451,15 +798,6 @@ export class Document {
451
798
  * genuinely malformed markdown.
452
799
  */
453
800
  static tryFromJson(json: string): Document | undefined;
454
- /**
455
- * Replace the body of the card at `index`. Throws if out of range.
456
- */
457
- updateCardBody(index: number, body: string): void;
458
- /**
459
- * Update a field on the card at `index`.
460
- * Throws if `index` is out of range, `name` is reserved or invalid.
461
- */
462
- updateCardField(index: number, name: string, value: any): void;
463
801
  /**
464
802
  * Number of composable cards (excludes the main card). O(1).
465
803
  */
@@ -474,6 +812,129 @@ export class Document {
474
812
  readonly warnings: Diagnostic[];
475
813
  }
476
814
 
815
+ /**
816
+ * Live render session: reads (`render`, `paint`, `pageSize`, `regions`,
817
+ * `fieldAt`, `positionAt`, `locate`) serve the current compile. `apply(doc)`
818
+ * recompiles a whole document in place, transactionally (on throw every read
819
+ * keeps serving the last-good compile). Geometry reads reflect the current
820
+ * compile; anchoring a caret across edits is the editor's job — re-read
821
+ * geometry after each committed `apply`.
822
+ *
823
+ * **Empty documents.** A zero-page document yields a valid session
824
+ * (`pageCount === 0`); `paint(ctx, 0)` or `pageSize(0)` throws with
825
+ * `"page index 0 out of range (pageCount=0)"`. Branch on `pageCount === 0`
826
+ * rather than catching the error.
827
+ */
828
+ export class LiveSession {
829
+ private constructor();
830
+ free(): void;
831
+ [Symbol.dispose](): void;
832
+ /**
833
+ * Recompile the session against `doc` — the edit verb of a live preview.
834
+ * The document is compiled through the same schema pipeline as `open`
835
+ * (same quill), then applied transactionally: on throw every read
836
+ * (`render`, `paint`, `pageSize`, `regions`, `fieldAt`) keeps serving the last-good
837
+ * compile, and the session recovers on the next successful `apply`. On
838
+ * success reads serve the new compile; repaint `dirtyPages ∩ visible`.
839
+ */
840
+ apply(doc: Document): ChangeSet;
841
+ /**
842
+ * The schema field whose content is under a point on `page` — the
843
+ * forward (click → field) direction: hit-test a click against the
844
+ * compiled document and get back the field address to focus in the
845
+ * editor, or `undefined` off any field's ink. `x`/`y` are PDF points
846
+ * with a **bottom-left** origin, the same space as `FieldRegion.rect` —
847
+ * from a canvas click, invert the overlay transform documented on
848
+ * `FieldRegion`: `x = clickPx.x / renderScale`,
849
+ * `y = pageHeightPt - clickPx.y / renderScale`. Unlike `regions()`,
850
+ * *every* placement answers, not just the first.
851
+ */
852
+ fieldAt(page: number, x: number, y: number): string | undefined;
853
+ /**
854
+ * The whole-field highlight boxes for `field` — one union rect per page,
855
+ * over the field's `span`-bearing content segments. The convenience that
856
+ * owns the union `regions()` leaves derived: it keeps `regions()` the
857
+ * low-level disjoint truth (#829) and folds the span-filter + per-page
858
+ * union here, so a "highlight the focused field" consumer stops
859
+ * reimplementing it. **Content only** — a field placed solely as a scalar
860
+ * reference or a bound widget carries no `span` and returns `[]`; its box
861
+ * is a single `regions()` rect. Reflects the current compile, like
862
+ * `regions()`.
863
+ */
864
+ fieldBoxes(field: string): FieldRegion[];
865
+ /**
866
+ * A corpus position → **caret rect** — the reverse of `positionAt`: given
867
+ * a field and a USV offset into its `RichText`, return the box (in the
868
+ * same bottom-left PDF-point space as `FieldRegion.rect`) to draw a caret
869
+ * at, its `span` collapsed to `[pos, pos]`; `undefined` when the field
870
+ * places no tracked content or the offset maps to no drawn glyph.
871
+ */
872
+ locate(field: string, pos: number): FieldRegion | undefined;
873
+ /**
874
+ * Page dimensions in points (1 pt = 1/72 inch).
875
+ * Throws if the backend has no canvas painter or `page` is out of range.
876
+ */
877
+ pageSize(page: number): PageSize;
878
+ /**
879
+ * Paint `page` into a `CanvasRenderingContext2D` or
880
+ * `OffscreenCanvasRenderingContext2D`. The painter owns
881
+ * `canvas.width`/`height` (no `clearRect` needed); consumers own
882
+ * `canvas.style.*`. If `layoutScale * densityScale` exceeds 16384 px
883
+ * per side, `densityScale` is clamped — `PaintResult.clamped` reports it and
884
+ * `PaintResult.effectiveDensityScale` carries the density actually applied.
885
+ *
886
+ * `put_image_data` writes the whole backing store, bypassing the 2D
887
+ * context's transform, `globalAlpha`, and clip: the painter owns the entire
888
+ * canvas, so each visible page needs its own `` — you cannot composite
889
+ * two pages, a sub-rect, or a context transform through this call.
890
+ *
891
+ * Throws if the backend has no canvas painter, `page` is out of range,
892
+ * `ctx` is the wrong type, or either scale is non-finite or `<= 0`.
893
+ */
894
+ paint(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, page: number, opts: PaintOptions | undefined): PaintResult;
895
+ /**
896
+ * A point → **corpus position** — the fine-grained click direction:
897
+ * hit-test a point and get back the field *and* a USV offset into its
898
+ * `RichText` (for placing a caret or mapping a selection into the content
899
+ * model), or `undefined` off all content ink. `x`/`y` are PDF points,
900
+ * bottom-left origin — the same space as `fieldAt`. The offset is
901
+ * cluster-exact and degrades to the containing segment's start on
902
+ * origin-less ink (list markers, a code fence's interior). See
903
+ * `CorpusHit`.
904
+ */
905
+ positionAt(page: number, x: number, y: number): CorpusHit | undefined;
906
+ /**
907
+ * Schema-field geometry for this compiled session — each content field's
908
+ * **first placement** (one region per page it touches) plus widget and
909
+ * scalar-reference-site regions, keyed on the quill schema field path; a
910
+ * field may still appear more than once (group by `field`, see
911
+ * `FieldRegion`). A session-level query: no render, no byte artifact. An
912
+ * interactive preview reads it to scroll to / highlight the focused
913
+ * field over a `paint`-ed canvas; the click direction is `fieldAt`.
914
+ * Empty for backends that place no schema fields.
915
+ */
916
+ regions(): FieldRegion[];
917
+ render(opts?: RenderOptions | null): RenderResult;
918
+ /**
919
+ * The backend that produced this session (e.g. `"typst"`).
920
+ */
921
+ readonly backendId: string;
922
+ readonly pageCount: number;
923
+ /**
924
+ * `true` iff `paint` and `pageSize` will succeed for this session. Derived
925
+ * from the session's canvas seam, so it reflects exactly what `paint` will
926
+ * do — no separately captured flag.
927
+ */
928
+ readonly supportsCanvas: boolean;
929
+ /**
930
+ * Non-fatal diagnostics of the session's **current compile** (e.g. Typst
931
+ * font fallback) — set at open and refreshed by each committed `apply`;
932
+ * a failed apply keeps the last-good compile's warnings. Also appended
933
+ * to `RenderResult.warnings` on each `render()` call.
934
+ */
935
+ readonly warnings: Diagnostic[];
936
+ }
937
+
477
938
  export class Quill {
478
939
  private constructor();
479
940
  free(): void;
@@ -536,10 +997,10 @@ export class Quill {
536
997
  *
537
998
  * Forwards the canonical `validation::*` diagnostics — same `code`,
538
999
  * `path`, and `hint` the engine emits — including the non-fatal
539
- * `validation::field_absent` completeness signal that `render` demotes.
540
- * Field values, defaults, and order are not part of this surface: read
541
- * them from the `Document` payload and `Quill.schema` (fields carry
542
- * `ui.order`).
1000
+ * `validation::must_fill` warning for each `!must_fill` marker left in
1001
+ * the document. Field values, defaults, and order are not part of this
1002
+ * surface: read them from the `Document` payload and `Quill.schema`
1003
+ * (schema key order is display order).
543
1004
  */
544
1005
  validate(doc: Document): Diagnostic[];
545
1006
  /**
@@ -558,9 +1019,10 @@ export class Quill {
558
1019
  readonly metadata: QuillMetadata;
559
1020
  /**
560
1021
  * Document schema for the quill: the user-fillable fields plus their
561
- * `ui` hints (group / order / showWhen). The single field-metadata
562
- * surface — drives form editors and LLM/MCP consumers alike. Returns the
563
- * `QuillSchema` shape.
1022
+ * `ui` hints (title / group / compact / multiline). The single
1023
+ * field-metadata surface — drives form editors and LLM/MCP consumers
1024
+ * alike. Key order in `fields`/`properties` is declaration order — the
1025
+ * ordering contract. Returns the `QuillSchema` shape.
564
1026
  */
565
1027
  readonly schema: QuillSchema;
566
1028
  }
@@ -574,76 +1036,64 @@ export class Quillmark {
574
1036
  [Symbol.dispose](): void;
575
1037
  constructor();
576
1038
  /**
577
- * Open an iterative render session for `doc` against `quill`'s backend.
1039
+ * Open a live render session for `doc` against `quill`'s backend.
578
1040
  */
579
- open(quill: Quill, doc: Document): RenderSession;
1041
+ open(quill: Quill, doc: Document): LiveSession;
580
1042
  /**
581
1043
  * Render `doc` against `quill` in one shot. Convenience over `open` +
582
- * `RenderSession.render`: an unset `output_format` falls back to the
1044
+ * `LiveSession.render`: an unset `output_format` falls back to the
583
1045
  * backend's first supported format.
584
1046
  */
585
1047
  render(quill: Quill, doc: Document, opts?: RenderOptions | null): RenderResult;
586
1048
  /**
587
1049
  * The output formats `quill`'s backend can emit. Static capability —
588
- * resolves the backend but compiles nothing. Throws `UnsupportedBackend`
1050
+ * resolves the backend but compiles nothing. Throws `engine::backend_not_found`
589
1051
  * if no registered backend matches the quill's declared backend.
590
1052
  */
591
1053
  supportedFormats(quill: Quill): OutputFormat[];
592
1054
  /**
593
- * `true` iff `quill`'s backend can paint sessions to a canvas. Asked of
594
- * the real backend; `false` when the backend is unsupported or non-canvas.
595
- * Use as a precondition probe before mounting a canvas-based preview UI.
1055
+ * Pre-session hint: `true` iff `quill`'s backend can paint sessions to a
1056
+ * canvas, derived from the backend's output formats; `false` when the
1057
+ * backend is unsupported. Use as a cheap precondition probe before mounting
1058
+ * a canvas-based preview UI; the authoritative answer is the session's
1059
+ * `supportsCanvas` getter once `open()` has been called.
596
1060
  */
597
1061
  supportsCanvas(quill: Quill): boolean;
598
1062
  }
599
1063
 
600
1064
  /**
601
- * Iterative render handle backed by an immutable compiled snapshot.
602
- *
603
- * **Empty documents.** A zero-page document yields a valid session
604
- * (`pageCount === 0`); `paint(ctx, 0)` or `pageSize(0)` throws with
605
- * `"page index 0 out of range (pageCount=0)"`. Branch on `pageCount === 0`
606
- * rather than catching the error.
1065
+ * Export a canonical `RichText` corpus to its markdown projection — the pure
1066
+ * codec that replaces the eager `bodyMarkdown` / `fieldMarkdown` precomputes
1067
+ * (`exportMarkdown(card.body)`). Throws if `rt` is not a canonical corpus.
607
1068
  */
608
- export class RenderSession {
609
- private constructor();
610
- free(): void;
611
- [Symbol.dispose](): void;
612
- /**
613
- * Page dimensions in Typst points (1 pt = 1/72 inch).
614
- * Throws if the backend has no canvas painter or `page` is out of range.
615
- */
616
- pageSize(page: number): PageSize;
617
- /**
618
- * Paint `page` into a `CanvasRenderingContext2D` or
619
- * `OffscreenCanvasRenderingContext2D`. The painter owns
620
- * `canvas.width`/`height` (no `clearRect` needed); consumers own
621
- * `canvas.style.*`. If `layoutScale * densityScale` exceeds 16384 px
622
- * per side, `densityScale` is clamped — detect via `PaintResult.pixelWidth`.
623
- *
624
- * Throws if the backend has no canvas painter, `page` is out of range,
625
- * `ctx` is the wrong type, or either scale is non-finite or `<= 0`.
626
- */
627
- paint(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, page: number, opts: PaintOptions | undefined): PaintResult;
628
- render(opts?: RenderOptions | null): RenderResult;
629
- /**
630
- * The backend that produced this session (e.g. `"typst"`).
631
- */
632
- readonly backendId: string;
633
- readonly pageCount: number;
634
- /**
635
- * `true` iff `paint` and `pageSize` will succeed for this session. The
636
- * backend's canvas capability, captured at open time.
637
- */
638
- readonly supportsCanvas: boolean;
639
- /**
640
- * Non-fatal diagnostics emitted when opening the session. Also appended
641
- * to `RenderResult.warnings` on each `render()` call.
642
- */
643
- readonly warnings: Diagnostic[];
644
- }
1069
+ export function exportMarkdown(rt: RichText): string;
1070
+
1071
+ /**
1072
+ * Import a markdown string to a canonical `RichText` corpus — the pure,
1073
+ * document-free codec. Pair with `install(addr, importMarkdown(md))` to spell
1074
+ * the cold (anchor-losing) write at the call site; prefer `revise` for edit
1075
+ * semantics. Throws on an over-nested input.
1076
+ */
1077
+ export function importMarkdown(markdown: string): RichText;
645
1078
 
646
1079
  /**
647
1080
  * Initialize the WASM module with panic hooks for better error messages
648
1081
  */
649
1082
  export function init(): void;
1083
+
1084
+ /**
1085
+ * Map a base corpus position through a `delta` to its new position — the pure
1086
+ * position-mapping codec an editor bridge composes to hold a caret stable
1087
+ * across a `revise`. `assoc` decides the side of a same-position insertion
1088
+ * (`"after"` moves past it). Throws on a malformed `delta`.
1089
+ */
1090
+ export function mapPos(delta: Delta, pos: number, assoc: Assoc): number;
1091
+
1092
+ /**
1093
+ * Rebase `markdown` onto a `base` corpus — the pure, document-free twin of
1094
+ * `revise`: cold-import + `diff_import`, returning the new `corpus` and the
1095
+ * text `delta` (surviving anchors rebased). Use it to compute a revise without
1096
+ * a document in hand; `revise(addr, md)` fuses this with the store for
1097
+ * atomicity. Throws on an over-nested markdown input or a non-corpus `base`.
1098
+ */
1099
+ export function rebase(base: RichText, markdown: string): { corpus: RichText; delta: Delta };