@quillmark/wasm 0.132.0 → 0.134.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 +418 -0
- package/README.md +30 -79
- package/THIRD_PARTY_LICENSES.md +446 -106
- package/core/wasm.d.ts +537 -378
- package/core/wasm.js +698 -212
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +30 -13
- package/package.json +1 -1
- package/render/wasm.d.ts +550 -394
- package/render/wasm.js +716 -248
- package/render/wasm_bg.wasm +0 -0
- package/render/wasm_bg.wasm.d.ts +30 -14
- package/runtime/runtime.d.ts +85 -121
- package/runtime/runtime.js +54 -137
- package/sbom.cdx.json +408 -573
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,423 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.134.0 - 2026-11-02
|
|
4
|
+
|
|
5
|
+
Upgrade path: [0.133 → 0.134](docs/migrations/0.133-to-0.134.md). Every
|
|
6
|
+
content value is the content model, and offsets in content values and changes
|
|
7
|
+
cross WASM in UTF-16 code units, where they counted scalar values. Python's
|
|
8
|
+
`revise_body`, `revise_field` and `revise_document` return a dict receipt,
|
|
9
|
+
where they returned a list of `Diagnostic`, so a loop over the result runs
|
|
10
|
+
over its keys: read `receipt["warnings"]`.
|
|
11
|
+
|
|
12
|
+
### Content v2
|
|
13
|
+
|
|
14
|
+
- feat(content)!: **the content model.** One value per body or `richtext`
|
|
15
|
+
field: flat text, blocks and containers with ids, marks, anchors apart from
|
|
16
|
+
marks, and islands whose cells and notes are slots of nested content.
|
|
17
|
+
Nothing derived is stored: an item's number is computed, and runs are told
|
|
18
|
+
apart by container id. The line form moves under `quillmark_content::lines`
|
|
19
|
+
and stays what the renderers and markdown codecs read; `migrate` moves
|
|
20
|
+
values between the two, minting ids deterministically.
|
|
21
|
+
- feat(content)!: **the line form's op lane is removed.** `lines::ops`
|
|
22
|
+
(`ChangeBundle`, `LineOp`, `MarkOp`, `IslandOp` and their decoders),
|
|
23
|
+
`Card::apply_body_change`, `Card::apply_field_change`, the `Normalized`
|
|
24
|
+
`apply_*` methods and `EditError::ContentApply` go: a `Change` through
|
|
25
|
+
`Card::apply` is the one edit. `quillmark::session` no longer re-exports
|
|
26
|
+
them. The parity corpus's op-wire cell reads the `Change` `diff` takes
|
|
27
|
+
from the empty value.
|
|
28
|
+
- feat(content): **`Change`, the one way a value is edited.** Splice,
|
|
29
|
+
`setBlock`, `container`, `mark`, `anchor`, `island` and `nested` steps,
|
|
30
|
+
with `invert`, `compose`, `map_pos` and a word-aligned `diff`, each a
|
|
31
|
+
property-tested law. A change yields a valid value or fails and changes
|
|
32
|
+
nothing.
|
|
33
|
+
- feat(content)!: **a block's record rides the `\n` that opens it.** A
|
|
34
|
+
split names the right half and the left keeps its id, so Enter at the end
|
|
35
|
+
of any block, the last included, leaves the text on its id; a join keeps
|
|
36
|
+
the left block's id and kind. `blocks[0]` is the record no splice reaches,
|
|
37
|
+
so `diff` and `replace` keep the stored first id, and `repair` gives the
|
|
38
|
+
first piece of a block it splits beside an island the block's record.
|
|
39
|
+
- feat(content): **`gestures::split_block` and `join_blocks`** build Enter
|
|
40
|
+
and Backspace as one splice each, inserting ahead of a block's opening
|
|
41
|
+
`\n` at its start so the text keeps its id.
|
|
42
|
+
- feat(content): **`gestures::inherited_marks(content, at)`** answers the
|
|
43
|
+
marks typed text carries: `strong`, `emph`, `underline` and `strike` grow
|
|
44
|
+
at their end, `code`, `link` and an `x-` mark at neither edge,
|
|
45
|
+
`font_family` at both.
|
|
46
|
+
- feat(bindings)!: **every WASM offset is UTF-16.** A region's `span`,
|
|
47
|
+
`positionAt`'s and `locate`'s `pos`, a typed revise's `Revised.delta` and
|
|
48
|
+
the deltas `reattach` takes count in UTF-16 code units, as content values
|
|
49
|
+
and changes do; core takes a host's units through `LiveSession::*_in`,
|
|
50
|
+
`Engine::render_in`, `Revised::delta_in` and `Document::reattach_in`.
|
|
51
|
+
- feat(content): **`repair` makes a merged state valid.** A missing
|
|
52
|
+
container leaves a path, a container under two paths takes its first
|
|
53
|
+
block's, a later run gets a derived container, an orphan slot or record
|
|
54
|
+
goes, text beside a table splits off, and a collapsed range anchor is
|
|
55
|
+
dropped. Total and deterministic.
|
|
56
|
+
- feat(content): **repair is how a merged state reads, and a write-back is
|
|
57
|
+
optional.** Peers holding one merged state compute one value without
|
|
58
|
+
writing. Any peer may write back a repair that only deletes or sets; one
|
|
59
|
+
that inserts (a block split beside an island, a heading's hard break, a
|
|
60
|
+
missing cell) is written back by a single writer, since two peers would
|
|
61
|
+
each insert their own. `Repair::inserts` says which, `inserts: true` on
|
|
62
|
+
the wire, and a missing cell reports `Repair::EmptyCell`.
|
|
63
|
+
- feat(core)!: **storage moves to `quillmark/document@0.134.0`**, each body
|
|
64
|
+
and richtext field the content model. An older row loads migrated, with the
|
|
65
|
+
same ids on every load. A value holds each anchor id once over its text
|
|
66
|
+
and every slot, so a stored value holding one twice loads keeping its
|
|
67
|
+
first holder.
|
|
68
|
+
- feat(core)!: **`Card::apply`, `revise` and `replace`** write a body or
|
|
69
|
+
richtext field and answer a `Receipt`: the applied change, the new hash,
|
|
70
|
+
and the dropped and moved anchors. Each takes the hash its writer read and
|
|
71
|
+
refuses a stale one with `edit::stale_base`. `Card::write` takes the
|
|
72
|
+
`Edit` each builds its change from, and `write_in` takes a host's units.
|
|
73
|
+
- feat(wasm)!: **`doc.read`, `apply`, `revise`, `replace`, `annotated` and
|
|
74
|
+
`hash(addr)`, and `invert` and `diff`**, offsets in UTF-16 code units.
|
|
75
|
+
Every content payload crossing the binding is the model. `overwrite`,
|
|
76
|
+
`reviseContent`, `applyChange` and `mapMarks` are removed: call `replace`
|
|
77
|
+
and `apply`. `compose`, `rebase` and `repair` stay unbound until the
|
|
78
|
+
editor or collaboration calls them.
|
|
79
|
+
- feat(wasm)!: **the editing gestures and `mapPos(change, pos, assoc?)`.**
|
|
80
|
+
`splitBlock`, `joinBlocks`, `deleteRange`, `indent`, `outdent`, `wrap` and
|
|
81
|
+
`inheritedMarks` take a `Content` and answer the `Change` to apply, offsets
|
|
82
|
+
in UTF-16. `mapPos` carries a position across a `Change`, where it took a
|
|
83
|
+
line delta, and answers a `MapResult`.
|
|
84
|
+
- feat(python)!: **the same surface in snake case**, offsets in code points.
|
|
85
|
+
- feat(core)!: **a content write names its card by `$id`.** An id no card
|
|
86
|
+
holds refuses with `edit::unknown_card_id`. `Document::card_index` and
|
|
87
|
+
`card_by_id` find a card by id. WASM `Addr.card` and Python `card=`
|
|
88
|
+
take a card's `$id` on every verb, and `apply`, `revise` and
|
|
89
|
+
`replace` take nothing else, so a card moved or inserted between a read and
|
|
90
|
+
its write, its body equal to the one read, no longer takes the write.
|
|
91
|
+
- feat(content)!: **the receipt names every anchor a change loses.** An
|
|
92
|
+
island a splice deletes, or a slot a props step no longer names, names the
|
|
93
|
+
anchors of its slots, each with the `island` and `slot` that held it.
|
|
94
|
+
`revise` and `replace` name every anchor they lose; `apply` every one but
|
|
95
|
+
those its own `anchor` steps removed. `Applied.dropped` and
|
|
96
|
+
`Receipt.dropped_anchors` are `change::Dropped`, an `AnchorContext` and a
|
|
97
|
+
`SlotRef`; WASM spells it `DroppedAnchor`.
|
|
98
|
+
- feat(content)!: **`diff` refuses an invalid target.** `diff` and `replace`
|
|
99
|
+
return the `Invariant` an invalid `after` breaks, where they returned zero
|
|
100
|
+
steps, and reach every valid one step-wise. WASM and Python `diff` answer
|
|
101
|
+
the `Change`.
|
|
102
|
+
- feat(content)!: **every offset-addressed step can hold to what its writer
|
|
103
|
+
read.** `mark` steps and an `anchor` placement take an optional `quote`,
|
|
104
|
+
the text their range covered, and an `island` insert an optional `block`,
|
|
105
|
+
the id of the block it lands in; a mismatch is `ApplyError::Stale`, as a
|
|
106
|
+
splice's `delete` is. `Applied.applied` and `invert` fill both in.
|
|
107
|
+
`MarkStep::Add`, `MarkStep::Remove`, `AnchorStep::Set` and
|
|
108
|
+
`IslandStep::Insert` carry the new fields.
|
|
109
|
+
- feat(content)!: **`map_pos` answers a `MapResult`.** A position always
|
|
110
|
+
lands, inside deleted text on the deletion's `assoc` edge and inside an
|
|
111
|
+
island a splice deleted on the island's slot, with `deleted`,
|
|
112
|
+
`deleted_before`, `deleted_after` and `deleted_across` saying what went
|
|
113
|
+
around it. A splice that deletes an island and names none in `removed` is
|
|
114
|
+
refused, where it mapped a position inside that island as kept. New
|
|
115
|
+
`mapping::StepMap` and `Mapping`, a list of step maps with mirror links, so
|
|
116
|
+
a position a step deleted and its rebased mirror put back maps home.
|
|
117
|
+
- feat(content)!: **reserved `x-` names on three vocabularies.** A block
|
|
118
|
+
kind, mark type or island type spelled `x-` and more decodes into
|
|
119
|
+
`BlockKind::Ext`, `MarkKind::Ext` or `IslandType::Ext`, its attrs or props
|
|
120
|
+
held as written, and re-encodes to the same bytes; any other unknown name
|
|
121
|
+
still refuses. Validation is structural only. An `x-` block reads as a
|
|
122
|
+
paragraph, an `x-` mark formats nothing, and an `x-` island renders and
|
|
123
|
+
exports to markdown as nothing, which the annotated read spells as its
|
|
124
|
+
token so a revise keeps it; a content write's revise or replace that
|
|
125
|
+
removes one refuses with `edit::drops_ext_island`, and `apply` removes it.
|
|
126
|
+
Steps set and remove them. `MarkStep::Remove`'s `tag` is a `String`,
|
|
127
|
+
`BlockKind::tag` and `MarkKind::tag` return `&str`, and `IslandType` is no
|
|
128
|
+
longer `Copy`.
|
|
129
|
+
- feat(content): **`rebase(base, change, over)` moves a change past another
|
|
130
|
+
made from the same value**, after ProseMirror's `rebaseSteps`. Each step
|
|
131
|
+
maps through the inverse of its change's earlier steps, then `over`, then
|
|
132
|
+
those steps rebased, and holds to its `delete`, `quote` or `block` on the
|
|
133
|
+
value it now reads; a step `over` deleted the text of, or whose id is gone,
|
|
134
|
+
is a `rebase::Conflict` naming it. A step by id that would overwrite what
|
|
135
|
+
`over` changed (a block's kind or path, a container, an island's props)
|
|
136
|
+
conflicts rather than undoing it, as does a splice that deletes an island
|
|
137
|
+
or block `over` changed, so a diff's `setBlock` names only the fields it
|
|
138
|
+
changes.
|
|
139
|
+
`Mapping::map_point` and `project` map
|
|
140
|
+
as `apply` moves a point anchor and inside one slot.
|
|
141
|
+
- feat(core)!: **a stale `base` rebases the write instead of refusing it.**
|
|
142
|
+
Each card keeps, in memory only, a log of its recent writes per value: one
|
|
143
|
+
snapshot of the value, and each write's normalized change and the hash it
|
|
144
|
+
left. A base rebases while the writes since it hold at most `LOG_STEPS`
|
|
145
|
+
(4096) steps, thousands of keystrokes, and the base the newest write read
|
|
146
|
+
whatever its size; past twice that, the oldest writes fold into the
|
|
147
|
+
snapshot, so the log holds one copy of the value however large.
|
|
148
|
+
`apply`, `revise` and `replace` with a base the log holds rebase over the
|
|
149
|
+
changes since, and a revise or replace diffs from the value the base
|
|
150
|
+
names. The receipt's new `rebased` says so. A conflict refuses with
|
|
151
|
+
`edit::rebase_conflict`, carrying `step` and `reason`; a base the log does
|
|
152
|
+
not reach is `edit::stale_base` as before. `Card::read_at` answers the
|
|
153
|
+
value a base names, and `BaseError::Conflict` carries `step` and
|
|
154
|
+
`reason`.
|
|
155
|
+
- fix(content): **`replace` moves an anchor between the prose and a cell**,
|
|
156
|
+
where it refused with `DuplicateId`: it carries only an anchor the new
|
|
157
|
+
value names nowhere. A props step naming a slot in `slots` that the props
|
|
158
|
+
do not name says so, apart from one naming a slot the island holds.
|
|
159
|
+
- feat(content)!: **a block or island keeps its id when a change moves
|
|
160
|
+
it.** Block and island ids are unique at the end of a change, as anchor
|
|
161
|
+
ids are, so a step may insert a copy before another deletes the original;
|
|
162
|
+
a step naming an id held twice between steps is `ApplyError::BadStep`.
|
|
163
|
+
`diff` keeps the id of a block or island moved either way, where it
|
|
164
|
+
re-minted one moved toward the front.
|
|
165
|
+
- perf(content): **`apply` runs a change in one pass.** Each step works on
|
|
166
|
+
the blocks it touches, checks only what it touched, and answers its
|
|
167
|
+
inverse as it applies; the whole value is validated once, at the end.
|
|
168
|
+
`invert` reads those inverses back, and a receipt lowers the value or
|
|
169
|
+
compares anchors only where something was dropped or rewritten. A
|
|
170
|
+
4000-step change to 288k characters applies in 26 ms, where it took 96 s,
|
|
171
|
+
and inverts in 30 ms, where it took 496 s; a revise of it takes 0.1 s and
|
|
172
|
+
a rebase 0.2 s, where they took 294 s and 396 s. An inverse's mark steps
|
|
173
|
+
restore each run of marks that differs, not one range over all of them.
|
|
174
|
+
`tests/speed.rs` is the gate, run in CI in a release build.
|
|
175
|
+
- perf(content): **converting a change or value to UTF-16 finds each
|
|
176
|
+
offset by binary search**, where it walked the text from its start: the
|
|
177
|
+
4000-step change crosses each way in about 20 ms, where it took 12 s out
|
|
178
|
+
and 19 s in.
|
|
179
|
+
- perf(core): **`content_hash` digests the content value as written**,
|
|
180
|
+
whose keys are already in order, rather than sorting a copy; the bytes are
|
|
181
|
+
the same.
|
|
182
|
+
|
|
183
|
+
### Card identity
|
|
184
|
+
|
|
185
|
+
- feat(core)!: **every composable card carries a `$id`.** It emits after
|
|
186
|
+
`$kind`, is minted where none is written (`c` and five base32 letters of a
|
|
187
|
+
fixed SHA-256 sequence, so equal markdown parses to equal ids), and a parse
|
|
188
|
+
re-mints a malformed or repeated one with `parse::card_id_reminted`. A
|
|
189
|
+
null `$id` (`$id:`, `~`) is no `$id`.
|
|
190
|
+
`insert_card` refuses a bad or held id (`edit::invalid_card_id`,
|
|
191
|
+
`edit::duplicate_card_id`). An older row mints its ids on load. `$id`
|
|
192
|
+
never reaches the plate JSON.
|
|
193
|
+
- feat(core)!: **a whole-document revise pairs cards by `$id` first.** An
|
|
194
|
+
incoming card spelling a stored card's id revises it, the most similar of
|
|
195
|
+
several spelling one keeps it, and the rest align by text as before. A
|
|
196
|
+
revised card keeps its stored id; an inserted one keeps a fresh id or mints
|
|
197
|
+
past every stored one. A list field's items align by text, so an item
|
|
198
|
+
inserted ahead of an anchored one leaves the anchor on its own item.
|
|
199
|
+
- feat(core)!: **a comment selector names its card.** `AnchorSelector` and
|
|
200
|
+
`DocumentAnchor` gain `card_id`, and `reattach` resolves a selector's card by
|
|
201
|
+
it wherever the card moved. WASM spells it `cardId` on `AnchorContext`,
|
|
202
|
+
`DocumentAnchor` and the `Card` / `CardInput` `id`; Python `card_id` and the
|
|
203
|
+
card dict's `id`.
|
|
204
|
+
|
|
205
|
+
### Two-way annotated read
|
|
206
|
+
|
|
207
|
+
- feat(content)!: **a revise reads the annotated read back.** A
|
|
208
|
+
`qm-anchor` tag the writer kept pins its anchor's start where it stands,
|
|
209
|
+
one it dropped leaves the anchor to the diff, and one naming nothing stored
|
|
210
|
+
reports `qm-anchor[ref]`. The read marks a tag it moved off its anchor's
|
|
211
|
+
start `at="span"` or `at="line"`, which pins nothing.
|
|
212
|
+
- feat(content)!: **an island markdown cannot spell reads as a
|
|
213
|
+
`qm-island` token**, `<qm-island ref="ID"></qm-island>`, which a revise
|
|
214
|
+
keeps as stored: a table's unnamed keys, an equation's unwritable TeX, an
|
|
215
|
+
image's extra props. A token naming no stored island reports
|
|
216
|
+
`qm-island[ref]`; a cold import drops every token, reported as
|
|
217
|
+
`qm-island`. A `Base::Read` merge compares the sides as annotated reads,
|
|
218
|
+
so a token survives it and a moved tag pins through it.
|
|
219
|
+
|
|
220
|
+
### Anchors on every text surface
|
|
221
|
+
|
|
222
|
+
- feat(content)!: **a table cell's and a footnote note's anchors join the
|
|
223
|
+
comment contract.** `Content::anchor_sites` reads every anchor with its
|
|
224
|
+
`Surface`, prose or a cell, and `anchor_contexts`, the annotated read's
|
|
225
|
+
`anchors` and every `dropped_anchors` receipt list cell anchors, a cell's
|
|
226
|
+
text as its `line`. `RemoveAnchor` removes an id from every cell, an anchor
|
|
227
|
+
`Add` refuses an id a cell holds, and an island op refuses a cell anchor
|
|
228
|
+
whose id the value already holds (`ApplyError::AnchorIdCollision`). A revise
|
|
229
|
+
keeps a cell's anchor where its island continues and the cell's text is
|
|
230
|
+
unchanged. `locate_site` and `reattach` search the cells with the prose.
|
|
231
|
+
|
|
232
|
+
### Scoped reads
|
|
233
|
+
|
|
234
|
+
- feat(core): **the annotated read of one address.** `Card::body_annotated`
|
|
235
|
+
and `field_annotated` (WASM `doc.annotatedAt(addr)`) read one body or
|
|
236
|
+
richtext field as the revise at that address reads it back, and as its
|
|
237
|
+
`baseMarkdown`.
|
|
238
|
+
|
|
239
|
+
### Guarded revise
|
|
240
|
+
|
|
241
|
+
- feat(core)!: **a revise states what its writer read.** `Base::Hash` is the
|
|
242
|
+
hash of the value read (`Card::body_hash`, `Card::field_hash`,
|
|
243
|
+
`Document::hash`, `content_hash`, and the typed writer's `hash` /
|
|
244
|
+
`field_hash` through the schema: SHA-256 over `quillmark/hash@1` and the
|
|
245
|
+
key-sorted canonical JSON), and a value that now hashes otherwise refuses
|
|
246
|
+
the write with `edit::stale_base`. `Base::Read` is the markdown read, annotated or not:
|
|
247
|
+
when the value has changed since, the writer's edit and the change merge by
|
|
248
|
+
line (`quillmark_content::delta::merge_lines`), so a person's edit made
|
|
249
|
+
between an LLM's read and its write lands beside the LLM's, and runs of
|
|
250
|
+
lines both changed refuse with `edit::rebase_conflict`. Both carry `current`,
|
|
251
|
+
the hash now. The verbs are `revise_body_against`, `revise_field_against`,
|
|
252
|
+
the writers' `revise_document_against`, and the free
|
|
253
|
+
`revise_import_against`. `EditError` gains `Base(BaseError)` and
|
|
254
|
+
`BoundParseError` gains `Base`.
|
|
255
|
+
- feat(wasm): **`BaseOptions` on every revise.** The writer's `reviseBody` /
|
|
256
|
+
`reviseField` / `reviseDocument` take an optional
|
|
257
|
+
`{ baseHash?, baseMarkdown? }`, read through the writer's `hash()` /
|
|
258
|
+
`hashAt(addr)` or the quill-free `doc.hashAt(addr)` / `doc.hash()`.
|
|
259
|
+
- feat(python): **`base_hash` and `base_markdown` on the writer's revise
|
|
260
|
+
verbs**, read through the writer's `hash()` / `hash_at(field=None,
|
|
261
|
+
card=None)` and the quill-free `doc.hash_at` / `doc.hash()`.
|
|
262
|
+
|
|
263
|
+
### Markdown export
|
|
264
|
+
|
|
265
|
+
- feat(content): **an export can name what it does not spell.**
|
|
266
|
+
`export::to_markdown_checked` returns `Exported { markdown, losses }`, the
|
|
267
|
+
markdown read back and compared with the content: each `ExportLoss` is a
|
|
268
|
+
mark written over other text or dropped, an island that reads back
|
|
269
|
+
otherwise, or `Text` where the text or its blocks do. Core mints one
|
|
270
|
+
`export::lost_construct` warning per loss (`lost_construct`), its
|
|
271
|
+
`construct` the mark's wire type, the island's type or `text`, a mark's
|
|
272
|
+
`start` and `end` beside it.
|
|
273
|
+
- fix(content): **a `*`, `_` or `~~` mark over edge whitespace keeps its
|
|
274
|
+
text.** `bar ` in bold wrote no bold at all, its delimiter never closing
|
|
275
|
+
against a space; it now writes `**bar** `, the mark over `bar`.
|
|
276
|
+
- perf(content): **`to_markdown` reads each line's marks alone**, so export is
|
|
277
|
+
linear in the marks where it scanned every mark per line.
|
|
278
|
+
- feat(content)!: **a markdown import reports the bidi controls it drops**,
|
|
279
|
+
under `parse::dropped_construct` with `construct: "bidi"`. `Dropped` gains
|
|
280
|
+
`Bidi`.
|
|
281
|
+
|
|
282
|
+
### Comments
|
|
283
|
+
|
|
284
|
+
- feat(content): **anchor contexts and exact re-attachment.** The `anchor`
|
|
285
|
+
module reads each prose anchor's quote, prefix and suffix off the stored
|
|
286
|
+
text as it is, an inline island's `U+FFFC` slot included
|
|
287
|
+
(`anchor_contexts`), finds the one range a context names (`locate`), and
|
|
288
|
+
re-adds a dropped anchor through the `Add` arm (`Content::reattach`), a range
|
|
289
|
+
over exactly its quote and a point at the one place its context meets. The
|
|
290
|
+
rule never guesses: a quote of `MIN_ANCHOR_QUOTE` (four) scalar values or
|
|
291
|
+
more, or island slots alone, its context agreeing on `ANCHOR_AGREE_USV`
|
|
292
|
+
(eight) each side or as far as it reaches where the field's edge cut it, a match overlapping inserted text where a
|
|
293
|
+
delta is given, and exactly one such place. `Delta::inserted_spans` is
|
|
294
|
+
public. (#2349)
|
|
295
|
+
- feat(core): **`Document::reattach` and `Revised.dropped_anchors`.**
|
|
296
|
+
`Document::anchor_contexts` / `anchor_context(id)` read each anchor's
|
|
297
|
+
`AnchorSelector { id, path, quote, prefix, suffix }`, and
|
|
298
|
+
`Document::reattach(selectors, deltas)` places dropped anchors at their
|
|
299
|
+
paths, path-strict, an id live anywhere in the document, a cell's included,
|
|
300
|
+
answered without a write. `Revised` gains `dropped_anchors`, each
|
|
301
|
+
`AnchorRead { id, line }`, at every rung that revises one body or field, and
|
|
302
|
+
`revise_import` takes the `&Normalized` it names them from. The facade
|
|
303
|
+
re-exports `AnchorSelector`, `Reattached`, `AnchorRead` and `DocPath`.
|
|
304
|
+
(#2349)
|
|
305
|
+
- feat(wasm): **`anchorContexts`, `anchorContext`, `reattach` and
|
|
306
|
+
`droppedAnchors`.** The selector crosses flat, its `path` a `DocPath`
|
|
307
|
+
string; `reattach(selectors, deltas?)` takes a `Record<path, Delta>` and
|
|
308
|
+
returns `{ reattached, orphaned }`. The `Revised` receipt of
|
|
309
|
+
`writer.reviseBody` and `writer.reviseField` gains
|
|
310
|
+
`droppedAnchors: { id, line }[]`. (#2349)
|
|
311
|
+
- feat(python)!: **the revise verbs return the whole receipt.**
|
|
312
|
+
`writer.revise_body` / `revise_field` return `{ delta, warnings,
|
|
313
|
+
dropped_anchors }` and `revise_document` `{ dropped_anchors, warnings, from }`,
|
|
314
|
+
its warnings the same `Diagnostic` objects, where each returned a list of
|
|
315
|
+
them. `doc.anchor_contexts()`, `doc.anchor_context(id)` and
|
|
316
|
+
`doc.reattach(selectors, deltas=None)` expose the handle. (#2349)
|
|
317
|
+
- docs(canon): **`COMMENTS.md` states the comment-anchor contract**: the
|
|
318
|
+
thread outside the document, the id it shares with its anchor, the matching
|
|
319
|
+
rule, drift, table cells, and the hosting rule. (#2349)
|
|
320
|
+
|
|
321
|
+
### Removed
|
|
322
|
+
|
|
323
|
+
- refactor(wasm)!: **WASM API no consumer uses.** `Document.revise` (the
|
|
324
|
+
0.133 markdown revise; `revise` is now the content-lane write),
|
|
325
|
+
`rebase`, `loadStored`, `storeFields`, `Quill.seedMain`,
|
|
326
|
+
`Document.markdownRules`, `assignInstances`, `CardReader` and
|
|
327
|
+
`reader.card(i)`, `CardWriter.index` / `kind`, the writer's and reader's
|
|
328
|
+
`document` getters and `LiveSession.backendId` are removed. The migration
|
|
329
|
+
guide's § Removed names each replacement.
|
|
330
|
+
- refactor(core,pdf)!: **public items nothing outside their crate uses.**
|
|
331
|
+
`BUNDLE_MEDIA_TYPE` is removed; `QuillConfig::is_snake_case_identifier`,
|
|
332
|
+
`quillmark_pdf::CHECK_GLYPH` and `FieldSpec::assert_finite_rect` are
|
|
333
|
+
crate-private; `SessionHandle::update` has no default, and the
|
|
334
|
+
`backend::update_unsupported` code no built-in backend raised is gone.
|
|
335
|
+
- refactor(content)!: **`serial::line_kind_to_value` is removed**, and
|
|
336
|
+
`serial::line_kind_from_value`, `container_from_value` and
|
|
337
|
+
`mark_from_value` are crate-private: decode a whole value through
|
|
338
|
+
`lines::serial::from_canonical_value`.
|
|
339
|
+
|
|
340
|
+
### Dependencies
|
|
341
|
+
|
|
342
|
+
- build(deps): **`serde-saphyr` 2.0**, with the YAML error sanitizer
|
|
343
|
+
stripping its new API names from messages; **Typst's `hayro` 0.7** for the
|
|
344
|
+
PDF form backend's raster; `pyo3` 0.30.1 and `lopdf` 0.46.
|
|
345
|
+
- ci(release): PyPI publishes only where `PUBLISH_PYPI` is set.
|
|
346
|
+
|
|
347
|
+
## v0.133.0 - 2026-10-29
|
|
348
|
+
|
|
349
|
+
Upgrade path: [0.132 → 0.133](docs/migrations/0.132-to-0.133.md). The
|
|
350
|
+
blueprint is retired: a model writes the JSON authoring document, so a prompt
|
|
351
|
+
built from `quill.blueprint` moves to `quill.authoringSchema`. A root
|
|
352
|
+
`example.md` is an ordinary file: a quill's example is what its `Quill.yaml`
|
|
353
|
+
declares, so move each value onto its field before upgrading.
|
|
354
|
+
|
|
355
|
+
### LLM authoring
|
|
356
|
+
|
|
357
|
+
- feat(core,cli,wasm,python)!: **LLMs author documents as JSON; the blueprint
|
|
358
|
+
is retired.** The authoring document is the values form lifted to the whole
|
|
359
|
+
document, `{fields, body, cards: [{kind, fields, body}]}`, every rich-text
|
|
360
|
+
field and body a Markdown string and `null` absent.
|
|
361
|
+
`QuillConfig::authoring_schema()` is its JSON Schema, built by
|
|
362
|
+
`build_transform_schema` under the new `SchemaCodec::Text`: every cell admits
|
|
363
|
+
`null`, every declared object refuses an undeclared key, a variant-bearing enum is a `oneOf` per world, and each slot carries
|
|
364
|
+
its `title`, `description`, `default` and `examples`.
|
|
365
|
+
`Quill::to_authoring` exports a document, a field stored as `null` absent, and `Quill::from_authoring` imports
|
|
366
|
+
and conforms one, refusing a malformed shape with `authoring::invalid_shape`,
|
|
367
|
+
its RFC 6901 pointer in `args.pointer`. Export never fails: an undecodable
|
|
368
|
+
or string-held content leaf passes as stored, so every export validates
|
|
369
|
+
against the schema and imports back unchanged.
|
|
370
|
+
WASM `authoringSchema` / `toAuthoring` / `fromAuthoring`, Python
|
|
371
|
+
`authoring_schema` / `to_authoring` / `from_authoring`, and
|
|
372
|
+
`quillmark schema --authoring` expose them. `QuillConfig::blueprint()`,
|
|
373
|
+
`quill.blueprint`, `quillmark blueprint` and `print_blueprint` are deleted;
|
|
374
|
+
`quillmark validate` renders the empty and seeded documents.
|
|
375
|
+
`build_transform_schema` takes a `SchemaCodec`. The blueprint's load rules go
|
|
376
|
+
with it: a multi-line `description:` and an enum member holding `>`, `;` or
|
|
377
|
+
`|` load, and `quill::description_multiline` and
|
|
378
|
+
`quill::format_literal_reserved_char` are deleted. (#2331)
|
|
379
|
+
|
|
380
|
+
### Examples
|
|
381
|
+
|
|
382
|
+
- feat(core)!: **`example:` and a body's `description` and `example` join the
|
|
383
|
+
quill schema.** A slot at any depth declares at most one `example:`, a value
|
|
384
|
+
of its type checked at load as `default:` is, under
|
|
385
|
+
`quill::example_{type_mismatch,not_in_enum,format_violation,held_stored,on_namespace,over_max,over_length}`
|
|
386
|
+
and `quill::richtext_example_import`. It answers no cell. A body's `description` is text for the writer and its
|
|
387
|
+
`example` markdown that imports without a drop; either beside
|
|
388
|
+
`enabled: false` is `quill::invalid_body`. `schema()` carries them,
|
|
389
|
+
`FieldSchema` and `BodyCardSchema` gain the fields, and WASM types them as
|
|
390
|
+
`QuillFieldSchema.example` and `QuillCardBody.{description,example}`. (#2329)
|
|
391
|
+
- feat(core,cli,wasm,python)!: **the example document is derived.**
|
|
392
|
+
`Quill::example_document()` returns `Option<Document>`: the seeded document
|
|
393
|
+
with every declared `example:` committed and each kind's `body.example` as
|
|
394
|
+
its body, `None` where the quill declares none. A root `example.md` is an
|
|
395
|
+
ordinary file: `EXAMPLE_FILE` and `quill::example_reference` are deleted,
|
|
396
|
+
WASM `exampleDocument()` and Python `example_document()` no longer throw, and
|
|
397
|
+
`quillmark validate` validates and renders the derived document, warning
|
|
398
|
+
`cli::example_file_ignored` while a root `example.md` remains. The
|
|
399
|
+
fixture quills declare the values their `example.md` held. (#2329)
|
|
400
|
+
|
|
401
|
+
### Content
|
|
402
|
+
|
|
403
|
+
- feat(content,core,wasm): **`reviseContent` revises a body or field to a
|
|
404
|
+
content and keeps its prose anchors.** WASM `doc.reviseContent(addr, rt)`,
|
|
405
|
+
over core's `Card::revise_body_content` / `revise_field_content` and the
|
|
406
|
+
content crate's `delta::carry_anchors`, diffs a content built elsewhere
|
|
407
|
+
against the stored value, carries each prose anchor whose id `rt` does not
|
|
408
|
+
hold, and returns the `Revised` receipt with its text `delta`. `rt`'s own
|
|
409
|
+
anchors and island ids land as they are.
|
|
410
|
+
- feat(content)!: **normalizing content moves no text.** A table's slot
|
|
411
|
+
sharing its line with prose is canonical: `Content::normalize` leaves the
|
|
412
|
+
line as given, where it inserted a `\n` on each side, and `to_markdown`
|
|
413
|
+
breaks it instead. `overwrite`, `reviseContent` and
|
|
414
|
+
`serial::from_authored_value` take the shape where they threw `block island
|
|
415
|
+
in a line's prose`, an `IslandOp::Set` on such a table lands, and a stored
|
|
416
|
+
row holding it loads as stored under the same tag. Typst sets the table as a
|
|
417
|
+
block where its slot sits, closing a heading around it, and a hard break
|
|
418
|
+
after a line the slot ends clears. WASM gains `normalize(content)`, the form
|
|
419
|
+
`overwrite` stores. (#2330)
|
|
420
|
+
|
|
3
421
|
## v0.132.0 - 2026-10-27
|
|
4
422
|
|
|
5
423
|
Upgrade path: [0.131 → 0.132](docs/migrations/0.131-to-0.132.md). A row
|
package/README.md
CHANGED
|
@@ -182,38 +182,9 @@ const doc = Document.storageVersionOf(content)
|
|
|
182
182
|
: Document.fromMarkdown(content);
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
The
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
itself changes, so every `0.131.x` patch release reads and writes those same
|
|
189
|
-
values.
|
|
190
|
-
|
|
191
|
-
- **Upgrading is safe.** A newer build reads documents an older build's
|
|
192
|
-
*writer* produced. Each schema version's wire format is frozen and never
|
|
193
|
-
changes; when the model does change, the new build ships a migration that
|
|
194
|
-
converts old payloads on `fromStored`. A document you commit as your
|
|
195
|
-
canonical on-disk format keeps loading across crate upgrades until its tag
|
|
196
|
-
is retired: 0.130 reads `@0.124.0` and `@0.129.0` only, so resave an older
|
|
197
|
-
row with a 0.124 to 0.129 build before taking 0.130, as that release's
|
|
198
|
-
migration guide shows. The other exception is a row a host authored a
|
|
199
|
-
content construct of its own into — a line `kind`, container, mark `type`
|
|
200
|
-
or island `type` outside the vocabulary. Those are refused from 0.113 on;
|
|
201
|
-
see that release's migration guide. An island `loss` is not one of them:
|
|
202
|
-
0.115 drops the key, so a row spelling one opens whatever the class and
|
|
203
|
-
comes back without it. From 0.126 on, a row whose `$quill` is a lone major,
|
|
204
|
-
`memo@1`, or whose body nests past 12 levels is refused too; rewrite it
|
|
205
|
-
before upgrading, as that release's migration guide shows.
|
|
206
|
-
- **Downgrading is not.** `fromStored` rejects a `schema` version it does not
|
|
207
|
-
read, newer than the build or retired, rather than guessing at its format.
|
|
208
|
-
Don't feed documents written by a newer build back into an older one.
|
|
209
|
-
|
|
210
|
-
`Document.currentStorageVersion()` is the newest tag this build writes. A
|
|
211
|
-
save writes the oldest tag that holds the row, so a fresh row can carry an
|
|
212
|
-
older tag still read: a row holding no font mark, `table` element, merged
|
|
213
|
-
table cell or table `head` 0.130 cannot draw saves as
|
|
214
|
-
`quillmark/document@0.129.0`. A tag other than `currentStorageVersion()` is
|
|
215
|
-
therefore not a mismatch. Use `storageVersionOf` after `fromStored` throws,
|
|
216
|
-
to tell "a version this build does not read" from "corrupt":
|
|
185
|
+
The tag model, what upgrading reads, and the one-time rewrites a release asks of stored rows are in [Persistence](https://quillmark.dev/integration/persistence/).
|
|
186
|
+
|
|
187
|
+
`Document.currentStorageVersion()` is the newest tag this build writes, and a save writes the oldest tag that holds the row, so a tag other than it is not a mismatch. Use `storageVersionOf` after `fromStored` throws, to tell "a version this build does not read" from "corrupt":
|
|
217
188
|
|
|
218
189
|
```ts
|
|
219
190
|
try {
|
|
@@ -224,13 +195,7 @@ try {
|
|
|
224
195
|
}
|
|
225
196
|
```
|
|
226
197
|
|
|
227
|
-
`storageVersionOf` does not validate the payload: it only reads the
|
|
228
|
-
`schema` field, returning `undefined` for non-JSON, non-objects, or
|
|
229
|
-
payloads that don't carry one.
|
|
230
|
-
|
|
231
|
-
In short: persist the `toStored` string, resave rows under a retired tag
|
|
232
|
-
before upgrading past it, never downgrade. The full design (including how
|
|
233
|
-
migrations are added) is in `prose/canon/DOCUMENT_STORAGE.md`.
|
|
198
|
+
`storageVersionOf` does not validate the payload: it only reads the `schema` field, returning `undefined` for non-JSON, non-objects, or payloads that don't carry one.
|
|
234
199
|
|
|
235
200
|
### Cards, seeds, and addresses
|
|
236
201
|
|
|
@@ -240,12 +205,21 @@ values from the `Document` payload: there is no separate form-view projection.
|
|
|
240
205
|
`quill.validate(doc)` scores it without invoking the backend.
|
|
241
206
|
|
|
242
207
|
`quill.exampleDocument()` returns the quill's example, a filled-in page whose
|
|
243
|
-
values are made up
|
|
244
|
-
|
|
208
|
+
values are made up: the seeded document with each `example` the schema
|
|
209
|
+
declares committed, a field's as its value and a body's as the body;
|
|
210
|
+
`undefined` when the quill declares none.
|
|
211
|
+
|
|
212
|
+
An LLM writes a document as JSON: `quill.authoringSchema` is its JSON Schema,
|
|
213
|
+
`quill.fromAuthoring(reply)` builds and conforms the `Document` (the JSON twin of
|
|
214
|
+
`quill.parse`), and `quill.toAuthoring(doc)` exports any document back, every
|
|
215
|
+
rich-text field and body a Markdown string. A shape error throws
|
|
216
|
+
`authoring::invalid_shape` at its JSON path; a value of the wrong type lands
|
|
217
|
+
with a `conform::*` warning and `quill.validate(doc)` reports it at its
|
|
218
|
+
document path.
|
|
245
219
|
|
|
246
220
|
`quill.seedDocument()` returns a starter document: one card per kind, each
|
|
247
221
|
carrying the kind's `seed` from `Quill.yaml` and no other field;
|
|
248
|
-
`quill.
|
|
222
|
+
`quill.seedCard(kind)` seeds one composable card. Both
|
|
249
223
|
return the read `Card` shape of `doc.main` / `doc.cards`, which `doc.insertCard`
|
|
250
224
|
accepts directly:
|
|
251
225
|
|
|
@@ -282,16 +256,10 @@ the content object, a `text` field as its literal string), and as authored
|
|
|
282
256
|
through the transport door. For the `Content` either way read
|
|
283
257
|
`quill.reader(doc).getContent(addr)`, which decodes through the codec the
|
|
284
258
|
field's declared type names. Card-scoped verbs take a
|
|
285
|
-
`CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
the thrown error carries one diagnostic per offending field (`path` = field
|
|
290
|
-
name). The address is first (never shape-overloaded, since `card` is a legal
|
|
291
|
-
field name), and parses strictly: a stray key throws rather than silently
|
|
292
|
-
reading as `{}`. The main card is `{}`, or **`MAIN_CARD_ADDR`** (from
|
|
293
|
-
`@quillmark/wasm`), a frozen alias that spells the intent:
|
|
294
|
-
`doc.storeFields(MAIN_CARD_ADDR, {...})`.
|
|
259
|
+
`CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`. An address parses
|
|
260
|
+
strictly: a stray key throws rather than silently reading as `{}`. The main
|
|
261
|
+
card is `{}`, or **`MAIN_CARD_ADDR`** (from `@quillmark/wasm`), a frozen alias
|
|
262
|
+
that spells the intent: `doc.getExt(MAIN_CARD_ADDR)`.
|
|
295
263
|
|
|
296
264
|
### Typed writes: `commit*` is the default, `store*` is the quill-free primitive
|
|
297
265
|
|
|
@@ -308,13 +276,12 @@ on `Document` itself (**store** = verbatim, **set** = typed):
|
|
|
308
276
|
on the typed path an undeclared name is a typo, not a fallback. The batch form
|
|
309
277
|
(`setAll`) is all-or-nothing: an undeclared name aborts the whole write and its
|
|
310
278
|
per-field diagnostics name every offending field, so a whole-form submit
|
|
311
|
-
surfaces every typo
|
|
312
|
-
carries the
|
|
279
|
+
surfaces every typo the opaque store would silently absorb. (The raw wasm
|
|
280
|
+
class carries the `_commitField` / `_commitFields` / `_addCard` / `_revise` /
|
|
313
281
|
`_reviseField` ABI the writer delegates to, hidden from the `.d.ts`.)
|
|
314
282
|
|
|
315
283
|
- **`store*`: the deliberate quill-free primitive.** `doc.storeField(addr, value)`
|
|
316
|
-
/
|
|
317
|
-
name/depth/kind and store the value verbatim, no quill required. Reach for it
|
|
284
|
+
validates only the field name/depth/kind and stores the value verbatim, no quill required. Reach for it
|
|
318
285
|
on purpose when you *want* the opaque store: quill-agnostic storage/migration
|
|
319
286
|
infra that has no bundle and must write regardless of a drifted schema;
|
|
320
287
|
store-now-validate-later editors holding in-progress input that `commit`
|
|
@@ -346,7 +313,7 @@ ed.card(2).reviseBody("**note**"); // a composable card's body;
|
|
|
346
313
|
is lazy: it never throws; an out-of-range index throws `IndexOutOfRange` at the
|
|
347
314
|
write.
|
|
348
315
|
|
|
349
|
-
#### `DocumentReader
|
|
316
|
+
#### `DocumentReader`: the read twin
|
|
350
317
|
|
|
351
318
|
`quill.reader(doc)` carries the writer's ephemerality and schema authority:
|
|
352
319
|
|
|
@@ -355,7 +322,7 @@ const v = quill.reader(doc);
|
|
|
355
322
|
v.get("subject"); // the values form: every content leaf as its codec's text, else as stored
|
|
356
323
|
v.getContent("subject"); // the same read as a `Content`, whichever lane stored it
|
|
357
324
|
v.bodyMarkdown(); // the main body markdown (quill-free)
|
|
358
|
-
v.card
|
|
325
|
+
v.get({ card: 0 }); // a card's body markdown; its fields read through get
|
|
359
326
|
v.resolve(); // the render view: blank-filled, coerced, each field tagged with its rung
|
|
360
327
|
```
|
|
361
328
|
|
|
@@ -450,15 +417,7 @@ if (cssPxPerPt > 0) { // 0 while the canvas has no layout
|
|
|
450
417
|
|
|
451
418
|
### Schema model
|
|
452
419
|
|
|
453
|
-
A field declares no `required` key
|
|
454
|
-
|
|
455
|
-
**`default:`** — what an unanswered field renders. With one, `quill.blueprint`
|
|
456
|
-
renders that value under a type-only `# <type>` annotation and the render path
|
|
457
|
-
uses it when the document omits the field. Without one, the blueprint leaves the
|
|
458
|
-
cell empty and an absent field blank-fills.
|
|
459
|
-
|
|
460
|
-
An unanswered field draws no diagnostic. Partial documents are accepted, and
|
|
461
|
-
`engine.render(quill, doc)` throws only for malformed input.
|
|
420
|
+
A field declares no `required` key and an unanswered field draws no diagnostic; [`default`](https://quillmark.dev/quills/quill-yaml-reference/#default) is what an unanswered field renders.
|
|
462
421
|
|
|
463
422
|
### Errors
|
|
464
423
|
|
|
@@ -492,19 +451,11 @@ throws a real `Error` and attaches the property, so there is no constructor to
|
|
|
492
451
|
`instanceof` against. Narrow with `isQuillmarkError`, which also works on errors
|
|
493
452
|
from any build or WASM instance in the page.
|
|
494
453
|
|
|
495
|
-
`diagnostics` is always non-empty
|
|
496
|
-
backend compilation errors, and `message` is derived from it. `diagnostics[0]`
|
|
497
|
-
is an error. A failed Typst `engine.render` or `engine.open` also carries the
|
|
498
|
-
quill's load warnings after its errors, so read each entry's `severity`. The
|
|
499
|
-
same shape applies to every throw site:
|
|
454
|
+
`diagnostics` is always non-empty and its first entry is an error; [Error Handling](https://quillmark.dev/integration/error-handling/) has the shape, the code namespaces and the warnings that follow. The same shape applies to every throw site:
|
|
500
455
|
|
|
501
456
|
- `Document.fromMarkdown`: parse errors (missing root `$quill` metadata, YAML
|
|
502
457
|
errors, `parse::input_too_large` for inputs > 10 MiB).
|
|
503
|
-
- `Document` mutators (`storeField`, `insertCard`, the writer's
|
|
504
|
-
`set`, etc.): mutator failures carry a namespaced `edit::*` `code` on
|
|
505
|
-
`diagnostics[0]` (`edit::invalid_field_name`, `edit::unknown_field`,
|
|
506
|
-
`edit::index_out_of_range`, `edit::field_coercion_failed`, …). Route on
|
|
507
|
-
`diagnostics[0].code`, never on message text.
|
|
458
|
+
- `Document` mutators (`storeField`, `insertCard`, the writer's `set`, etc.): a namespaced `edit::*` `code` on `diagnostics[0]`.
|
|
508
459
|
- `engine.render` / `session.render`: backend compilation failures and
|
|
509
460
|
validation errors.
|
|
510
461
|
- An object argument carrying a key its verb does not read: the render
|
|
@@ -512,8 +463,8 @@ same shape applies to every throw site:
|
|
|
512
463
|
key counts, a non-enumerable one and one holding `undefined` included. The
|
|
513
464
|
argument must be a plain object, its prototype `null`, `Object.prototype`
|
|
514
465
|
of any realm, or a null-prototype object whose keys count too, so a `Map`, a
|
|
515
|
-
class instance or `Object.create({ … })` throws. A payload item
|
|
516
|
-
|
|
466
|
+
class instance or `Object.create({ … })` throws. A payload item and a
|
|
467
|
+
`$ext` or `$seed` value read their own
|
|
517
468
|
enumerable keys alone: a non-enumerable or inherited key there is neither
|
|
518
469
|
read nor refused.
|
|
519
470
|
The diagnostic names the key or what was passed, and carries no `code`: it
|