@quillmark/wasm 0.114.0 → 0.115.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,177 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.115.0 - 2026-09-22
4
+
5
+ Upgrade path: [0.114 → 0.115](docs/migrations/0.114-to-0.115.md).
6
+
7
+ ### The quill authoring contract
8
+
9
+ - feat(core)!: **a `matrix` member mapping takes the ordinary ladder.** Two
10
+ rules decided what a stored member spelling meant — a bare scalar is the tick,
11
+ and a mapping naming no `held` is held — and the second retires. A member is
12
+ an `object` of a synthesized `held: {type: boolean, default: false}` beside
13
+ its columns, so a mapping is the member object already and the tick it names
14
+ no key for resolves as every other absent cell does, to its `default:`. The
15
+ old rule overrode that ladder for one key in one type, and bought a spelling
16
+ convenience for it: two mappings one key apart meant opposite ticks, the
17
+ shorter one ticked, and any producer building the member object without
18
+ conforming — an editor writing a column before the tick, a serde-built
19
+ payload, `{}` — set a tick it never wrote. `{ detail: X }` is now unheld with
20
+ its detail retained, which is what `{ held: false, detail: X }` already meant;
21
+ the bare scalar and every mapping naming `held` are unchanged. What remains is
22
+ exactly `conform_variant`'s bare-scalar rule, so `SCHEMAS.md` §Matrix lists
23
+ three things as the type's own rather than four. The flip is silent — the
24
+ value is well-typed under both readings, and a defaultless column's
25
+ `validation::must_fill` leaves with the tick it was gated on — so the upgrade
26
+ guide leads with it. (#1860)
27
+ - feat(core): **a typed dictionary's property opens a variant world.**
28
+ `variants:` was card-level, so a conditional block had to sit at the top of a
29
+ card even where the fields it governs belong to a dictionary. The rule was
30
+ never about depth: what stays one level deep is the gap between a field's
31
+ *declared* shape and its *live* one, and a dictionary's own shape is the
32
+ schema's — so a world opened there deepens the address and leaves the gap
33
+ alone. `header.classification` reads, binds, branches and strands exactly as
34
+ `classification` does, one step down. Every other position would deepen the
35
+ gap and still refuses one: an array element, whose liveness would vary per
36
+ index against a form bound once; a matrix column, inside a grid the page
37
+ prints in full; and another variant's cell, which would make the plate's one
38
+ branch a tree. `quill::variant_placement` now names the position carrying the
39
+ ban rather than restating the old rule, and the ban is **sticky** — an object
40
+ inside any of them inherits it instead of laundering a world into a position
41
+ that cannot hold one. The surfaces that dispatch on the container before
42
+ walking the type needed nothing (coercion, validation, the render floor,
43
+ conditional `must_fill`, the transform schema, `acroform` binding); the three
44
+ that scanned card fields now descend, so `validation::out_of_variant` finds a
45
+ strand at `main.header.classification.controlled_by`, the blueprint seats a
46
+ world's cells — live or commented — at the dictionary's indent, and the seed
47
+ resolves a nested discriminant before walking its field set.
48
+ - feat(core)!: **`ui.layout: table` contracts its columns.** The key was checked
49
+ for its outer shape alone — `type: array` whose `items` is an `object` — so a
50
+ row holding a container loaded clean and no editor could draw it: the author
51
+ wrote a key, the loader took it, and a surface ignored it with no signal at
52
+ either end. A column that is not a leaf is now
53
+ `quill::table_column_not_flat`, naming the column (`appendices[].entries`)
54
+ rather than the field. This splits the key along a seam it always had: a
55
+ **contract** that every column is a leaf, settled at load, and a **request**
56
+ to draw a grid, which an editor still declines for a width or a cell renderer
57
+ it does not have. Shape leaves the decline list, since shape was never a
58
+ consumer's judgement to make. The boundary is containment, not height — prose
59
+ is a column whatever its `inline`, because how tall a cell renders is the
60
+ editor's call and what it contains is not. `FieldType::is_leaf` is the one
61
+ definition, an exhaustive match a new type cannot skip, and `acroform`'s
62
+ `is_scalar_or_prose` is gone in favor of it: the same question had two
63
+ answers in two crates. Part of #1859.
64
+ - feat(core,wasm,python): **the empty document has a constructor.**
65
+ `Quill::empty_document()` is `Document::new` under the quill's own reference,
66
+ so the three canonical documents are three constructors beside
67
+ `QuillConfig::blueprint()` and `Quill::seed_document()`, and no caller spells
68
+ `name@version` to reach the one a plate is bound to render. `quiver_test.rs`
69
+ reads it rather than formatting the two-line markdown itself. The bindings
70
+ carry it as `Quill.emptyDocument()` / `Quill.empty_document()`.
71
+ - feat(cli)!: **`quillmark validate` compiles the plate.** It renders the empty
72
+ document, the blueprint and the seed through the quill's backend at the
73
+ backend's first declared format, and reports each failure as a
74
+ `cli::canonical_document_failed` error naming which of the three it is,
75
+ followed by the backend's own diagnostics. The contract that a plate renders
76
+ the empty document was checked for the fixtures quiver alone, and `render`
77
+ with no input file renders the seed, the fullest of the three: a plate that
78
+ reads a blank badly passed both. A quill whose plate does not compile now
79
+ exits `1` where it exited `0`, and a backend that does not resolve is
80
+ `cli::backend_unresolved` followed by the engine's own diagnostic. A render's
81
+ warnings join the `-v` list, and `validate --no-render` is the configuration
82
+ read on its own. Closes #1844.
83
+
84
+ ### The content model
85
+
86
+ - refactor(content)!: **an island stores its value, not its provenance.** `Loss`
87
+ had one minting rule — a table cell dropping an inline image's url — and no
88
+ reader: export dispatches on an island's `type`, the Typst emitter and the
89
+ acroform binder never looked, and an `islandOps` `set` stored whatever class
90
+ the op named. Two cells written `| a cat |` and `| ![a cat](cat.png) |` mint
91
+ equal `props` and differ only in the markdown behind them, which the
92
+ projection has no syntax to carry back out, so the class survived neither a
93
+ `to_markdown` → `from_markdown` hop nor re-derivation from the row holding it
94
+ — which is why the content fixed point had to be stated modulo it. An
95
+ island's canonical form is `{id, type, props}`, the fixed point is exact, and
96
+ the closed vocabularies cover the four discriminators a projection dispatches
97
+ on. `Loss`, `Island::loss`, `Island::with_loss`, `IslandType::default_loss`,
98
+ `ContentIsland.loss` and `ContentLossClass` are deleted; a stored row spelling
99
+ `loss` opens with the key ignored and absent from what comes back, and a
100
+ `loss` on an authored island or an `islandOps` entry is ignored rather than
101
+ refused. A host that surfaced the class surfaces the drop at the import
102
+ instead, where the markdown it is about is still in hand. Closes #1757.
103
+ - refactor(content,core,wasm,python)!: **a block island's line is a `para`, and
104
+ `LineKind::Island` is gone.** The kind was a second copy of a fact the island
105
+ beside it already carried: markdown writes a `table` as a block and an `image`
106
+ inline, so `IslandType::block_only` settled which line a lone slot sat on and
107
+ `normalize` wrote that answer back over whatever was stored. Every read site
108
+ now asks the island — export already dispatched on the type, the Typst emitter
109
+ already lowered `Island` as `Para`, and the one fact the kind alone still
110
+ answered, "this block renders one line", moves to the new public
111
+ `Content::block_island_at`, which `normalize` reads beside
112
+ `takes_continuations` when it clears a stranded `continues`. Landing a block
113
+ island is two channels rather than three: the delta opens the line, the island
114
+ op fills it, and the `setKind` goes. The TS `ContentLineKind` loses its
115
+ `{ kind: "island" }` arm, so a host branching on it typechecks red; a `setKind`
116
+ still naming `island` lands as the `para` it would have settled to.
117
+ - refactor(core)!: **the storage tag is `quillmark/document@0.115.0`.** The two
118
+ lines above move canonical bytes — the `loss` key for every document holding
119
+ an island, the line kind for every document holding a table — so content
120
+ hashes recompute once on those rows. `DocumentV0_112_0` freezes with a raw
121
+ `body` and the new `DocumentV0_115_0` carries the `CanonicalContent`; the hop
122
+ between them is that body's decode, and `V0_93_0 → V0_112_0` becomes a retag
123
+ now that both trees spell `body` raw. Stored rows need no migration: the
124
+ content decoder reads `island` as `para` wherever it meets it, permanently,
125
+ since the same spelling rests untagged inside every `richtext` field and
126
+ `$seed` overlay, where no hop reaches it. Closes #1647.
127
+
128
+ ### Schema, validation and the resolved view
129
+
130
+ - refactor(core)!: **a schema is loaded, not deserialized.** `QuillConfig`,
131
+ `CardSchema` and `FieldSchema` serialize and no longer implement
132
+ `Deserialize`, and `FieldSchema::from_quill_value` is crate-internal. A field
133
+ arrives in two steps — the parse, which folds the sibling keys (`inline:`,
134
+ `values:`, `members:`, `max:`) into the type payload, and the shape walk,
135
+ which rejects an `object` without `properties`, an `array` without `items`,
136
+ an empty `properties` map, and the placements — and only `parse_fields` ran
137
+ both, so a config assembled through serde carried states no `Quill.yaml` can
138
+ reach, absorbed downstream without a diagnostic. Gating the serde path
139
+ instead would have duplicated the walk and collapsed its diagnostic codes
140
+ into `quill::field_parse_error` at that boundary. The way to a config is
141
+ `QuillConfig::from_yaml_with_warnings`, or a `Quill` read off a file tree;
142
+ serialization is untouched, so `schema()`, `schema_yaml()` and every emission
143
+ read the same loaded form they did. Closes #1836.
144
+ - refactor(core,wasm)!: **a matrix roster is one flat mapping; group headings
145
+ are gone.** A block's `group:` was declared in the schema and absent from the
146
+ value, the address and the vocabulary — the blueprint emits
147
+ `matrix<a | b | c>` off one flat key space, so the partition reached the
148
+ visual editor and nowhere else, buying one shared `properties` map for a
149
+ schema concept, its load rules and a reserved key. `members:` is now
150
+ `{id: Title}` directly, `MatrixGroup`, `MATRIX_GROUP_KEY` and the TypeScript
151
+ `QuillMatrixGroup` are deleted, and `FieldType::Matrix` carries an
152
+ `IndexMap<String, String>` read through the new `FieldType::matrix_roster`,
153
+ which replaces `matrix_members`. The projection stops writing `group` onto
154
+ every member, which returns `group` to the column names a quill may declare,
155
+ and `quill::duplicate_matrix_member` retires: the roster is a mapping, so it
156
+ has one slot per id exactly as the stored value does and a duplicate is
157
+ unspellable on both sides. Closes #1851.
158
+
159
+ ### The blueprint
160
+
161
+ - feat(core)!: **a dormant variant world is its own cells, commented out.** A
162
+ variant-bearing enum's blueprint named every world it could not show, one
163
+ `# when CUI: controlled_by, poc, category` line each — the one place on the
164
+ surface where a cell was named rather than shown, dropping the position,
165
+ obligation, type, description and example a cell carries. Every world now sits
166
+ under a `# when <MEMBER>:` header in declaration order: the selected world's
167
+ cells live, every other world's the same cells with a `# ` in front, at the
168
+ slot they would occupy live. A reader activates a world by setting the
169
+ discriminant and deleting `# `. One builder and one emitter serve both, so a
170
+ dormant line is byte-for-byte the live line, and the block round-trips through
171
+ `Document::parse` as the comments it is, reaching neither the validator nor
172
+ the render floor. Closes #1846.
173
+
174
+
3
175
  ## v0.114.0 - 2026-09-18
4
176
 
5
177
  ### Schema, validation and the resolved view
package/README.md CHANGED
@@ -175,9 +175,9 @@ const doc = Document.storageVersionOf(content)
175
175
  : Document.fromMarkdown(content);
176
176
  ```
177
177
 
178
- The `schema` value (`quillmark/document@0.112.0`) is the **model version**,
178
+ The `schema` value (`quillmark/document@0.115.0`) is the **model version**,
179
179
  not the running crate version. It is a hand-set constant, bumped only when
180
- the `Document` model itself changes, so every `0.112.x` patch release reads
180
+ the `Document` model itself changes, so every `0.115.x` patch release reads
181
181
  and writes that same value.
182
182
 
183
183
  - **Upgrading is safe.** A newer build reads documents an older build's
@@ -187,8 +187,10 @@ and writes that same value.
187
187
  canonical on-disk format keeps loading across crate upgrades: there is no
188
188
  need to pin old wasm to read old data. The exception is a row a host
189
189
  authored a content construct of its own into — a line `kind`, container,
190
- mark `type`, island `type` or `loss` outside the vocabulary. Those are
191
- refused from 0.113 on; see that release's migration guide.
190
+ mark `type` or island `type` outside the vocabulary. Those are refused from
191
+ 0.113 on; see that release's migration guide. An island `loss` is not one of
192
+ them: 0.115 drops the key, so a row spelling one opens whatever the class and
193
+ comes back without it.
192
194
  - **Downgrading is not.** `fromStored` rejects an *unknown* (i.e. newer)
193
195
  `schema` version rather than guessing at a format it predates. Don't feed
194
196
  documents written by a newer build back into an older one.
package/core/wasm.d.ts CHANGED
@@ -73,9 +73,9 @@ export interface Content {
73
73
  }
74
74
 
75
75
  /** One `\n`-separated segment of `Content.text`, in order. `kind` is a closed
76
- * set: a role outside it is refused wherever content is decoded. Every role
77
- * spells its payload in `attrs`, so `kind === "heading"` narrows `attrs` to
78
- * `{ level: number }` with no guard. */
76
+ * set: `island` reads as `para`, and any other role outside it is refused
77
+ * wherever content is decoded. Every role spells its payload in `attrs`, so
78
+ * `kind === "heading"` narrows `attrs` to `{ level: number }` with no guard. */
79
79
  export type ContentLine = {
80
80
  containers: ContentContainer[];
81
81
  /** A within-block hard line break rather than a new block. Omitted (false) in the common case. */
@@ -87,7 +87,6 @@ export type ContentLineKind =
87
87
  | { kind: "para" }
88
88
  | { kind: "heading"; attrs: { level: number } }
89
89
  | { kind: "code"; attrs?: { lang?: string } }
90
- | { kind: "island" }
91
90
  | { kind: "rule" };
92
91
 
93
92
  /** An ancestor block a line nests inside, outermost first. Closed like
@@ -158,14 +157,10 @@ export interface ImageProps {
158
157
  alt: string;
159
158
  }
160
159
 
161
- /** How faithfully the markdown projection can carry an island. */
162
- export type ContentLossClass = "lossless" | "degraded" | "unrepresentable";
163
-
164
160
  /** A structured object occupying one island slot in `Content.text`. `type` is a
165
161
  * closed set, so `type === "table"` narrows `props` to `TableProps`. */
166
162
  export type ContentIsland = {
167
163
  id: string;
168
- loss: ContentLossClass;
169
164
  } & (
170
165
  | { type: "table"; props: TableProps }
171
166
  | { type: "image"; props: ImageProps }
@@ -237,10 +232,12 @@ export type MarkOp =
237
232
  * metadata. `setContinues` sets or clears a line's within-block hard-break flag
238
233
  * (`ContentLine.continues`); `continues: true` lands as `false` on line 0, which
239
234
  * nothing precedes, on a line whose containers differ from the line above, and
240
- * on one following a heading, island or rule, each a block of one line.
241
- * `setKind` lands a kind the line's text contradicts — `island` or `rule` over
235
+ * on one following a heading, a rule or a block island's line, each a block of
236
+ * one line. `setKind` lands a kind the line's text contradicts — `rule` over
242
237
  * prose, `code` over a slot — as `para`, which is what re-importing the line's
243
- * own markdown yields. Read the content back to see where an op settled.
238
+ * own markdown yields, and reads an `island` the union does not offer as the
239
+ * `para` that spelling projects. Read the content back to see where an op
240
+ * settled.
244
241
  */
245
242
  export type LineOp =
246
243
  | { op: "split"; at: number }
@@ -264,22 +261,16 @@ export type LineOp =
264
261
  * one `insert` per slot.
265
262
  *
266
263
  * Deleting an island needs no op: a `delta` that removes its slot drops the
267
- * island whole, and a block island's line demotes to `para`. Re-landing it is an
268
- * `insert` of the full island under its original id; a pasted copy of a live
269
- * island mints a fresh one.
270
- *
271
- * A `set` stores the `loss` it is given; nothing re-derives the class from the
272
- * new `props`.
264
+ * island whole. Re-landing it is an `insert` of the full island under its
265
+ * original id; a pasted copy of a live island mints a fresh one.
273
266
  *
274
267
  * An island is *inline* (a slot inside a paragraph) or a **block** (that slot
275
- * alone on a line under `kind: "island"`), and for a slot alone on a line the
276
- * type settles which: markdown writes a `table` as a block and an image inline,
277
- * so the line's `kind` is read off the type and a `setKind` spelling it
278
- * otherwise does not survive. Landing a block island is one bundle of all three
279
- * channels, in the order they apply: `delta` inserts the `\n` that opens the
280
- * line, `islandOps` inserts the slot, `lineOps` tags the line
281
- * `{ op: "setKind", kind: "island" }`. `{ op: "split" }` cannot open that line,
282
- * since line ops run after island ops.
268
+ * alone on its own line), and the type settles which: markdown writes a `table`
269
+ * as a block and an image inline. Both lines read `kind: "para"` — the block is
270
+ * the slot's markup, not a role the line carries — so landing a block island is
271
+ * two channels: `delta` inserts the `\n` that opens the line, `islandOps`
272
+ * inserts the slot. `{ op: "split" }` cannot open that line, since line ops run
273
+ * after island ops.
283
274
  *
284
275
  * A `table` has no inline placement: markdown writes it as a block, so an
285
276
  * `insert` whose `at` is not an empty line throws, as does a `set` retyping an
@@ -398,9 +389,10 @@ export interface QuillFieldUi {
398
389
  * conventional label of its own. */
399
390
  blank_title?: string;
400
391
  /** The control the field asks for, where the shape admits more than one.
401
- * A request, not a contract: a consumer that cannot draw it falls back to
402
- * its own choice for the type. `"table"` is valid only on an `array`
403
- * whose `items` is an `object`. */
392
+ * `"table"` is valid only on an `array` whose `items` is an `object`.
393
+ * Every column is a leaf, a contract refused at load as
394
+ * `quill::table_column_not_flat`. Drawing the grid is a request: a
395
+ * consumer that cannot falls back to its own choice for the type. */
404
396
  layout?: "table";
405
397
  }
406
398
 
@@ -460,16 +452,17 @@ export interface QuillFieldSchema {
460
452
  /** The closed set of allowed values. Required on `type: "enum"`, and valid
461
453
  * nowhere else. */
462
454
  values?: string[];
463
- /** Per-member field sets on a card-level `type: "enum"` field, keyed by
464
- * member: the fields that exist only where the discriminant holds that
465
- * member. Declaring it makes the field rest as a container,
466
- * `{value: <member>, …that member's fields}`, rather than a bare string. */
455
+ /** Per-member field sets on a `type: "enum"` field that is a card's own
456
+ * field or a typed dictionary's property, keyed by member: the fields that
457
+ * exist only where the discriminant holds that member. Declaring it makes
458
+ * the field rest as a container, `{value: <member>, …that member's
459
+ * fields}`, rather than a bare string. */
467
460
  variants?: Record<string, Record<string, QuillFieldSchema>>;
468
461
  /** The roster of a `type: "matrix"` field, required there and valid
469
- * nowhere else: the closed vocabulary a document ticks, in display order.
462
+ * nowhere else: member id to display title, key order the display order.
470
463
  * Each member is an object of `held` plus the field's `properties`
471
464
  * (the columns), addressed as `<field>.<member id>.held`. */
472
- members?: QuillMatrixGroup[];
465
+ members?: Record<string, string>;
473
466
  ui?: QuillFieldUi;
474
467
  properties?: Record<string, QuillFieldSchema>;
475
468
  items?: QuillFieldSchema;
@@ -482,13 +475,6 @@ export interface QuillFieldSchema {
482
475
  inline?: boolean;
483
476
  }
484
477
 
485
- /** One block of a `type: "matrix"` roster: an optional display heading and the
486
- * members under it, member id to display title. Key order is display order. */
487
- export interface QuillMatrixGroup {
488
- group?: string;
489
- values: Record<string, string>;
490
- }
491
-
492
478
  /** Schema entry for the main card or a named card kind. */
493
479
  export interface QuillCardSchema {
494
480
  description?: string;
@@ -853,6 +839,14 @@ export class Quill {
853
839
  * mutation.
854
840
  */
855
841
  conform(doc: Document): Diagnostic[];
842
+ /**
843
+ * The empty document: a main card carrying this quill's `$quill` and
844
+ * `$kind: main`, no field committed, no composable card, no body. The
845
+ * type-minimal valid input, and the document a plate is bound to render
846
+ * (the quill authoring contract). `new Document(ref)` is the same document
847
+ * reached by spelling the reference.
848
+ */
849
+ emptyDocument(): Document;
856
850
  /**
857
851
  * Build a quill from a file tree. Pure: the declared backend is resolved
858
852
  * later, at render time. Accepts a `Map<string, Uint8Array>` or a plain
@@ -1087,6 +1081,7 @@ export interface InitOutput {
1087
1081
  readonly quill_backendId: (a: number, b: number) => void;
1088
1082
  readonly quill_blueprint: (a: number, b: number) => void;
1089
1083
  readonly quill_conform: (a: number, b: number, c: number) => void;
1084
+ readonly quill_emptyDocument: (a: number) => number;
1090
1085
  readonly quill_fromTree: (a: number, b: number) => void;
1091
1086
  readonly quill_metadata: (a: number, b: number) => void;
1092
1087
  readonly quill_parse: (a: number, b: number, c: number, d: number) => void;
package/core/wasm.js CHANGED
@@ -1297,6 +1297,18 @@ export class Quill {
1297
1297
  wasm.__wbindgen_add_to_stack_pointer(16);
1298
1298
  }
1299
1299
  }
1300
+ /**
1301
+ * The empty document: a main card carrying this quill's `$quill` and
1302
+ * `$kind: main`, no field committed, no composable card, no body. The
1303
+ * type-minimal valid input, and the document a plate is bound to render
1304
+ * (the quill authoring contract). `new Document(ref)` is the same document
1305
+ * reached by spelling the reference.
1306
+ * @returns {Document}
1307
+ */
1308
+ emptyDocument() {
1309
+ const ret = wasm.quill_emptyDocument(this.__wbg_ptr);
1310
+ return Document.__wrap(ret);
1311
+ }
1300
1312
  /**
1301
1313
  * Build a quill from a file tree. Pure: the declared backend is resolved
1302
1314
  * later, at render time. Accepts a `Map<string, Uint8Array>` or a plain
package/core/wasm_bg.wasm CHANGED
Binary file
@@ -62,6 +62,7 @@ export const quill__resolve: (a: number, b: number, c: number) => void;
62
62
  export const quill_backendId: (a: number, b: number) => void;
63
63
  export const quill_blueprint: (a: number, b: number) => void;
64
64
  export const quill_conform: (a: number, b: number, c: number) => void;
65
+ export const quill_emptyDocument: (a: number) => number;
65
66
  export const quill_fromTree: (a: number, b: number) => void;
66
67
  export const quill_metadata: (a: number, b: number) => void;
67
68
  export const quill_parse: (a: number, b: number, c: number, d: number) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/wasm",
3
- "version": "0.114.0",
3
+ "version": "0.115.0",
4
4
  "description": "WebAssembly bindings for Quillmark, a schema-driven document engine",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/render/wasm.d.ts CHANGED
@@ -73,9 +73,9 @@ export interface Content {
73
73
  }
74
74
 
75
75
  /** One `\n`-separated segment of `Content.text`, in order. `kind` is a closed
76
- * set: a role outside it is refused wherever content is decoded. Every role
77
- * spells its payload in `attrs`, so `kind === "heading"` narrows `attrs` to
78
- * `{ level: number }` with no guard. */
76
+ * set: `island` reads as `para`, and any other role outside it is refused
77
+ * wherever content is decoded. Every role spells its payload in `attrs`, so
78
+ * `kind === "heading"` narrows `attrs` to `{ level: number }` with no guard. */
79
79
  export type ContentLine = {
80
80
  containers: ContentContainer[];
81
81
  /** A within-block hard line break rather than a new block. Omitted (false) in the common case. */
@@ -87,7 +87,6 @@ export type ContentLineKind =
87
87
  | { kind: "para" }
88
88
  | { kind: "heading"; attrs: { level: number } }
89
89
  | { kind: "code"; attrs?: { lang?: string } }
90
- | { kind: "island" }
91
90
  | { kind: "rule" };
92
91
 
93
92
  /** An ancestor block a line nests inside, outermost first. Closed like
@@ -158,14 +157,10 @@ export interface ImageProps {
158
157
  alt: string;
159
158
  }
160
159
 
161
- /** How faithfully the markdown projection can carry an island. */
162
- export type ContentLossClass = "lossless" | "degraded" | "unrepresentable";
163
-
164
160
  /** A structured object occupying one island slot in `Content.text`. `type` is a
165
161
  * closed set, so `type === "table"` narrows `props` to `TableProps`. */
166
162
  export type ContentIsland = {
167
163
  id: string;
168
- loss: ContentLossClass;
169
164
  } & (
170
165
  | { type: "table"; props: TableProps }
171
166
  | { type: "image"; props: ImageProps }
@@ -237,10 +232,12 @@ export type MarkOp =
237
232
  * metadata. `setContinues` sets or clears a line's within-block hard-break flag
238
233
  * (`ContentLine.continues`); `continues: true` lands as `false` on line 0, which
239
234
  * nothing precedes, on a line whose containers differ from the line above, and
240
- * on one following a heading, island or rule, each a block of one line.
241
- * `setKind` lands a kind the line's text contradicts — `island` or `rule` over
235
+ * on one following a heading, a rule or a block island's line, each a block of
236
+ * one line. `setKind` lands a kind the line's text contradicts — `rule` over
242
237
  * prose, `code` over a slot — as `para`, which is what re-importing the line's
243
- * own markdown yields. Read the content back to see where an op settled.
238
+ * own markdown yields, and reads an `island` the union does not offer as the
239
+ * `para` that spelling projects. Read the content back to see where an op
240
+ * settled.
244
241
  */
245
242
  export type LineOp =
246
243
  | { op: "split"; at: number }
@@ -264,22 +261,16 @@ export type LineOp =
264
261
  * one `insert` per slot.
265
262
  *
266
263
  * Deleting an island needs no op: a `delta` that removes its slot drops the
267
- * island whole, and a block island's line demotes to `para`. Re-landing it is an
268
- * `insert` of the full island under its original id; a pasted copy of a live
269
- * island mints a fresh one.
270
- *
271
- * A `set` stores the `loss` it is given; nothing re-derives the class from the
272
- * new `props`.
264
+ * island whole. Re-landing it is an `insert` of the full island under its
265
+ * original id; a pasted copy of a live island mints a fresh one.
273
266
  *
274
267
  * An island is *inline* (a slot inside a paragraph) or a **block** (that slot
275
- * alone on a line under `kind: "island"`), and for a slot alone on a line the
276
- * type settles which: markdown writes a `table` as a block and an image inline,
277
- * so the line's `kind` is read off the type and a `setKind` spelling it
278
- * otherwise does not survive. Landing a block island is one bundle of all three
279
- * channels, in the order they apply: `delta` inserts the `\n` that opens the
280
- * line, `islandOps` inserts the slot, `lineOps` tags the line
281
- * `{ op: "setKind", kind: "island" }`. `{ op: "split" }` cannot open that line,
282
- * since line ops run after island ops.
268
+ * alone on its own line), and the type settles which: markdown writes a `table`
269
+ * as a block and an image inline. Both lines read `kind: "para"` — the block is
270
+ * the slot's markup, not a role the line carries — so landing a block island is
271
+ * two channels: `delta` inserts the `\n` that opens the line, `islandOps`
272
+ * inserts the slot. `{ op: "split" }` cannot open that line, since line ops run
273
+ * after island ops.
283
274
  *
284
275
  * A `table` has no inline placement: markdown writes it as a block, so an
285
276
  * `insert` whose `at` is not an empty line throws, as does a `set` retyping an
@@ -453,9 +444,10 @@ export interface QuillFieldUi {
453
444
  * conventional label of its own. */
454
445
  blank_title?: string;
455
446
  /** The control the field asks for, where the shape admits more than one.
456
- * A request, not a contract: a consumer that cannot draw it falls back to
457
- * its own choice for the type. `"table"` is valid only on an `array`
458
- * whose `items` is an `object`. */
447
+ * `"table"` is valid only on an `array` whose `items` is an `object`.
448
+ * Every column is a leaf, a contract refused at load as
449
+ * `quill::table_column_not_flat`. Drawing the grid is a request: a
450
+ * consumer that cannot falls back to its own choice for the type. */
459
451
  layout?: "table";
460
452
  }
461
453
 
@@ -515,16 +507,17 @@ export interface QuillFieldSchema {
515
507
  /** The closed set of allowed values. Required on `type: "enum"`, and valid
516
508
  * nowhere else. */
517
509
  values?: string[];
518
- /** Per-member field sets on a card-level `type: "enum"` field, keyed by
519
- * member: the fields that exist only where the discriminant holds that
520
- * member. Declaring it makes the field rest as a container,
521
- * `{value: <member>, …that member's fields}`, rather than a bare string. */
510
+ /** Per-member field sets on a `type: "enum"` field that is a card's own
511
+ * field or a typed dictionary's property, keyed by member: the fields that
512
+ * exist only where the discriminant holds that member. Declaring it makes
513
+ * the field rest as a container, `{value: <member>, …that member's
514
+ * fields}`, rather than a bare string. */
522
515
  variants?: Record<string, Record<string, QuillFieldSchema>>;
523
516
  /** The roster of a `type: "matrix"` field, required there and valid
524
- * nowhere else: the closed vocabulary a document ticks, in display order.
517
+ * nowhere else: member id to display title, key order the display order.
525
518
  * Each member is an object of `held` plus the field's `properties`
526
519
  * (the columns), addressed as `<field>.<member id>.held`. */
527
- members?: QuillMatrixGroup[];
520
+ members?: Record<string, string>;
528
521
  ui?: QuillFieldUi;
529
522
  properties?: Record<string, QuillFieldSchema>;
530
523
  items?: QuillFieldSchema;
@@ -537,13 +530,6 @@ export interface QuillFieldSchema {
537
530
  inline?: boolean;
538
531
  }
539
532
 
540
- /** One block of a `type: "matrix"` roster: an optional display heading and the
541
- * members under it, member id to display title. Key order is display order. */
542
- export interface QuillMatrixGroup {
543
- group?: string;
544
- values: Record<string, string>;
545
- }
546
-
547
533
  /** Schema entry for the main card or a named card kind. */
548
534
  export interface QuillCardSchema {
549
535
  description?: string;
@@ -1129,6 +1115,14 @@ export class Quill {
1129
1115
  * mutation.
1130
1116
  */
1131
1117
  conform(doc: Document): Diagnostic[];
1118
+ /**
1119
+ * The empty document: a main card carrying this quill's `$quill` and
1120
+ * `$kind: main`, no field committed, no composable card, no body. The
1121
+ * type-minimal valid input, and the document a plate is bound to render
1122
+ * (the quill authoring contract). `new Document(ref)` is the same document
1123
+ * reached by spelling the reference.
1124
+ */
1125
+ emptyDocument(): Document;
1132
1126
  /**
1133
1127
  * Build a quill from a file tree. Pure: the declared backend is resolved
1134
1128
  * later, at render time. Accepts a `Map<string, Uint8Array>` or a plain
@@ -1403,6 +1397,7 @@ export interface InitOutput {
1403
1397
  readonly quill_backendId: (a: number, b: number) => void;
1404
1398
  readonly quill_blueprint: (a: number, b: number) => void;
1405
1399
  readonly quill_conform: (a: number, b: number, c: number) => void;
1400
+ readonly quill_emptyDocument: (a: number) => number;
1406
1401
  readonly quill_fromTree: (a: number, b: number) => void;
1407
1402
  readonly quill_metadata: (a: number, b: number) => void;
1408
1403
  readonly quill_parse: (a: number, b: number, c: number, d: number) => void;
package/render/wasm.js CHANGED
@@ -1620,6 +1620,18 @@ export class Quill {
1620
1620
  wasm.__wbindgen_add_to_stack_pointer(16);
1621
1621
  }
1622
1622
  }
1623
+ /**
1624
+ * The empty document: a main card carrying this quill's `$quill` and
1625
+ * `$kind: main`, no field committed, no composable card, no body. The
1626
+ * type-minimal valid input, and the document a plate is bound to render
1627
+ * (the quill authoring contract). `new Document(ref)` is the same document
1628
+ * reached by spelling the reference.
1629
+ * @returns {Document}
1630
+ */
1631
+ emptyDocument() {
1632
+ const ret = wasm.quill_emptyDocument(this.__wbg_ptr);
1633
+ return Document.__wrap(ret);
1634
+ }
1623
1635
  /**
1624
1636
  * Build a quill from a file tree. Pure: the declared backend is resolved
1625
1637
  * later, at render time. Accepts a `Map<string, Uint8Array>` or a plain
Binary file
@@ -76,6 +76,7 @@ export const quill__resolve: (a: number, b: number, c: number) => void;
76
76
  export const quill_backendId: (a: number, b: number) => void;
77
77
  export const quill_blueprint: (a: number, b: number) => void;
78
78
  export const quill_conform: (a: number, b: number, c: number) => void;
79
+ export const quill_emptyDocument: (a: number) => number;
79
80
  export const quill_fromTree: (a: number, b: number) => void;
80
81
  export const quill_metadata: (a: number, b: number) => void;
81
82
  export const quill_parse: (a: number, b: number, c: number, d: number) => void;
@@ -108,7 +108,6 @@ export type {
108
108
  QuillFieldUi,
109
109
  QuillCardUi,
110
110
  QuillGroupUi,
111
- QuillMatrixGroup,
112
111
  QuillMetadata
113
112
  } from '../core/wasm.js';
114
113
 
@@ -182,9 +181,9 @@ export declare function isQuillmarkError(e: unknown): e is QuillmarkError;
182
181
 
183
182
  import type { ContentContainer } from '../core/wasm.js';
184
183
 
185
- // `ContentIsland.type`, `ContentMark.type`, `ContentLine.kind`,
186
- // `ContentContainer.container` and an island's `loss` are closed sets, so a
187
- // bare discriminant check narrows the payload on its own:
184
+ // `ContentIsland.type`, `ContentMark.type`, `ContentLine.kind` and
185
+ // `ContentContainer.container` are closed sets, so a bare discriminant check
186
+ // narrows the payload on its own:
188
187
  // `line.kind === 'heading'` reaches `line.attrs.level`, with no guard to call.
189
188
 
190
189
  /**
@@ -174,7 +174,7 @@ const HANDLE_KINDS = {
174
174
  },
175
175
  Document: {
176
176
  code: 'runtime::not_a_document',
177
- hint: 'Pass a Document built by Document.fromMarkdown / fromStored or quill.seedDocument. A Document from another copy of @quillmark/wasm is refused too — each copy is its own WASM linear memory and its own class — so run `npm ls @quillmark/wasm` and dedupe to one.'
177
+ hint: 'Pass a Document built by Document.fromMarkdown / fromStored or quill.seedDocument / emptyDocument. A Document from another copy of @quillmark/wasm is refused too — each copy is its own WASM linear memory and its own class — so run `npm ls @quillmark/wasm` and dedupe to one.'
178
178
  }
179
179
  };
180
180
 
@@ -219,9 +219,15 @@ function requireLocalQuill(quill, method) {
219
219
  // WELD_KEYS is the rule `Container::same_weld` owns upstream: which `attrs`
220
220
  // entries two adjacent runs must share for the markdown projection to read them
221
221
  // as one. `start` is not among them, since CommonMark reads only a list's first
222
- // number — a subset, which is why every container needs its own entry. A table
223
- // rather than a switch, so `tests/known_names_drift.rs` can pin it against the
224
- // Rust predicate.
222
+ // number — a subset, which is why every container needs its own entry.
223
+ //
224
+ // Re-spelled here rather than called, because `assignInstances` answers before
225
+ // wasm initializes (`prose/canon/BINDINGS.md` § "WebAssembly"). What holds the
226
+ // two spellings equal is running the rule, not comparing their sources:
227
+ // `runtime.test.js` § "container run boundaries" stamps a pair and re-imports
228
+ // it, where `Content::normalize` re-mints against the Rust predicate. A tag the
229
+ // table omits welds with nothing, so `tests/known_names_drift.rs` pins that
230
+ // every container has an entry.
225
231
 
226
232
  const WELD_KEYS = { list_item: ['ordered'], quote: [] };
227
233