@quillmark/wasm 0.99.0 → 0.101.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 +108 -104
- package/README.md +54 -5
- package/backends/pdfform/wasm.d.ts +99 -17
- package/backends/pdfform/wasm_bg.js +138 -23
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/pdfform/wasm_bg.wasm.d.ts +3 -1
- package/backends/typst/wasm.d.ts +99 -17
- package/backends/typst/wasm_bg.js +138 -23
- package/backends/typst/wasm_bg.wasm +0 -0
- package/backends/typst/wasm_bg.wasm.d.ts +3 -1
- package/core/wasm.d.ts +99 -17
- package/core/wasm_bg.js +138 -23
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +3 -1
- package/package.json +1 -1
- package/runtime/runtime.d.ts +36 -9
- package/runtime/runtime.js +43 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,112 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.101.0 - 2026-08-03
|
|
4
|
+
|
|
5
|
+
- refactor(core): gate the raw-plate seam behind a feature, and fixes from review
|
|
6
|
+
- docs(core): state the conform gate's codes instead of linking a private item
|
|
7
|
+
- refactor(core): bind a live session to its quill so apply takes a Document
|
|
8
|
+
- feat(quillmark): the facade names what the read and preview flows return
|
|
9
|
+
- docs(content,wasm): density pass over the island channel's prose
|
|
10
|
+
- feat(content,core,wasm)!: reach islands through the op vocabulary
|
|
11
|
+
- refactor(core)!: `Payload` becomes a read view
|
|
12
|
+
- refactor(core)!: collapse the schema-free field projection
|
|
13
|
+
- refactor(core)!: fold `Quill`'s file queries into `FileTreeNode`
|
|
14
|
+
- fix(core): repoint the doc references the `from_yaml` removal orphaned
|
|
15
|
+
- refactor(quillmark): move the facade gate off the front page, and stop tests reaching past it
|
|
16
|
+
- docs: migration guide for the 0.101 surface removals
|
|
17
|
+
- refactor(core)!: drop the lossy `QuillConfig::from_yaml`
|
|
18
|
+
- refactor(core)!: `Document::from_main_and_cards` becomes crate-internal
|
|
19
|
+
- feat(quillmark): the facade covers authoring, and the examples enter through the bound door
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## v0.100.0 - 2026-08-03
|
|
23
|
+
|
|
24
|
+
A content field gets one resting form, and the last reserved `$` key with no
|
|
25
|
+
reader is removed. All breaking changes are covered by
|
|
26
|
+
`docs/migrations/0.99-to-0.100.md`. Stored documents load unchanged, but a row
|
|
27
|
+
read through the bound door converges once: read-repair, not a schema-version
|
|
28
|
+
event. One ordering matters, and the guide's "Legacy data" section states it —
|
|
29
|
+
conform a stored population before exporting markdown from it.
|
|
30
|
+
|
|
31
|
+
- refactor(core,wasm,python)!: content fields have one resting form, enforced at
|
|
32
|
+
load. `Quill::conform(&mut doc)` is the primitive and `Quill::parse(md)`
|
|
33
|
+
(parse, then conform) the convenience — the documented primary ingestion path,
|
|
34
|
+
`quill.parse` / `quill.conform` on both bindings. A `richtext` field rests as
|
|
35
|
+
the canonical content object, a `plaintext` field as its **literal string**, so
|
|
36
|
+
the stored shape is a property of the codec instead of the construction lane:
|
|
37
|
+
`equals` and content hashes stop separating semantically identical documents.
|
|
38
|
+
The typed writer commits `plaintext` as a string, and `revise_field` diffs it
|
|
39
|
+
through the literal codec (a byte-identical revise of `a \*b\*` used to commit
|
|
40
|
+
`a *b*`). `Document::parse` / `Document.fromMarkdown` stay exactly as they
|
|
41
|
+
were, demoted to the transport/repair door. Conform is idempotent, a byte no-op
|
|
42
|
+
on an already-canonical document, and reports a `conform::*` warning where the
|
|
43
|
+
strict write refuses rather than retyping or rejecting; a `$quill` naming
|
|
44
|
+
another quill errors before any mutation (#1160, #1162). See
|
|
45
|
+
`docs/migrations/0.99-to-0.100.md`
|
|
46
|
+
- fix(core)!: markdown exported from a `plaintext` field resting as a content
|
|
47
|
+
object is markdown-escaped. Emit is schema-free and cannot tell a `plaintext`
|
|
48
|
+
content from a `richtext` one, so `a *literal* line` leaves as
|
|
49
|
+
`a \*literal\* line` and re-parses with the backslashes as characters — one
|
|
50
|
+
more layer per save cycle. Only the typed writer produced that rest, and the
|
|
51
|
+
string rest above deletes it rather than managing it: load, conform, and
|
|
52
|
+
re-store a population before exporting markdown from it. Markdown already
|
|
53
|
+
exported under ≤0.99 is corrupt at rest, its escapes indistinguishable from
|
|
54
|
+
authored ones, so re-export it from the conformed rows (#1159). See
|
|
55
|
+
`docs/migrations/0.99-to-0.100.md`
|
|
56
|
+
- fix(core,wasm,python)!: a `plaintext` field resting as a string reads through
|
|
57
|
+
the **literal** codec, not markdown — `note: 'a *literal* line'` read back as
|
|
58
|
+
`a literal line` while render and validation kept the asterisks. Only the
|
|
59
|
+
string lane was wrong; the committed-object lane always decoded correctly, so
|
|
60
|
+
a consumer that pre-escaped a `plaintext` field to survive the read drops the
|
|
61
|
+
escaping. Alongside it, `reader.get_content` / `reader.getContent` returns a
|
|
62
|
+
content field's `Content` corpus whichever lane stored it, so a consumer
|
|
63
|
+
holding a corpus editor stops branching on the wire shape. `EditError` gains
|
|
64
|
+
`FieldNotContent` (`edit::field_not_content`) for a declared type that is not a
|
|
65
|
+
content leaf; core adds `Card::field_plaintext_content` (#1154). See
|
|
66
|
+
`docs/migrations/0.99-to-0.100.md`
|
|
67
|
+
- refactor(core,wasm,python)!: card `$id` is removed — the reserved key, its
|
|
68
|
+
resolver (`Document::find_card` / `doc.cardIndexById` / `doc.card_index_by_id`),
|
|
69
|
+
the uniqueness contract (`EditError::CardIdCollision` / `EmptyCardId`, the
|
|
70
|
+
`parse::card_id_*` warnings, the storage rejection), `Card::id` /
|
|
71
|
+
`Payload::{id, set_id, take_id}` / `Document::{set_card_id, remove_card_id}`,
|
|
72
|
+
the `PayloadItem::Id` and `CardWire.id` wire members, and the projected `id` on
|
|
73
|
+
both bindings' card shape. Nothing in the engine read it and it never reached a
|
|
74
|
+
backend, so what is left after removing the machinery that served the resolver
|
|
75
|
+
is `$ext` with a reserved name. A block declaring `$id` no longer parses and a
|
|
76
|
+
blob carrying an `id` payload item no longer loads: a hard cutover, no
|
|
77
|
+
tolerate-and-ignore window. Per-card consumer keys move to `$ext` under a
|
|
78
|
+
namespace you own, with no uniqueness, no collision check, and no repair
|
|
79
|
+
(#1151). See `docs/migrations/0.99-to-0.100.md`
|
|
80
|
+
- refactor(content)!: `Content`, `Line`, `Mark`, and `Island` take
|
|
81
|
+
`#[non_exhaustive]` — the four public structs the 0.99 sweep missed, that pass
|
|
82
|
+
having run as two issues split by crate. Their literals give way to `new` plus
|
|
83
|
+
the `with_*` setters on the same terms as the rest of the API; every field stays
|
|
84
|
+
`pub`, so reading and assigning are unchanged. `Delta`, `Segment`, and
|
|
85
|
+
`BaseLengthMismatch` stay open deliberately and now say so in their rustdoc.
|
|
86
|
+
A Rust source break only: nothing about the wire, the canonical bytes, or the
|
|
87
|
+
bindings moves (#1146). See `docs/migrations/0.99-to-0.100.md`
|
|
88
|
+
- feat(core,wasm,python): `Diagnostic.args` — the facts `message` interpolates,
|
|
89
|
+
keyed by name, so a consumer with its own string table selects a sentence by
|
|
90
|
+
`code` and fills it itself. Values keep their JSON shape (a list arrives as a
|
|
91
|
+
list, a count as a number), engine prose never rides under a key, and a
|
|
92
|
+
formatter missing a key falls back to `message` wholesale. `prose/canon/ERROR.md`
|
|
93
|
+
§ "Diagnostic args" tabulates the keys per code and a test fails when code and
|
|
94
|
+
canon disagree (#1130)
|
|
95
|
+
- fix(core): the `$quill` mismatch message and hint name the pairing rather than
|
|
96
|
+
the verb. `check_quill_reference` gates every schema-bound door now, not the
|
|
97
|
+
render path alone, so a `quill.parse` failure no longer reads "was rendered
|
|
98
|
+
with". The codes (`quill::name_mismatch` / `quill::version_mismatch`) are
|
|
99
|
+
unchanged
|
|
100
|
+
- test(fuzz): the resting-form invariant gains a target, stated as three
|
|
101
|
+
properties — conform is a fixed point, parse-then-conform equals typed-write
|
|
102
|
+
per content field, and a document through the markdown surface and back settles
|
|
103
|
+
after one pass (exactly, for `plaintext`, whose codec is lossless both ways)
|
|
104
|
+
- docs: the cycle's stale pages are repaired. Both binding READMEs gain the bound
|
|
105
|
+
door and the corpus read, `revise_field` is documented per declared type on all
|
|
106
|
+
four surfaces instead of as a markdown-only richtext verb, and four canon claims
|
|
107
|
+
that outran the tree are corrected
|
|
108
|
+
|
|
109
|
+
|
|
3
110
|
## v0.99.0 - 2026-08-01
|
|
4
111
|
|
|
5
112
|
The 1.0.0 API freeze lands ahead of the tag, and the content codec closes its
|
|
@@ -119,109 +226,6 @@ loads byte-identically and `0.99` writes the same bytes for the same content.
|
|
|
119
226
|
`serde_saphyr` types stay out of `quillmark-core`'s public API (see the YAML
|
|
120
227
|
engine entry above), so nothing downstream moves
|
|
121
228
|
|
|
122
|
-
<!-- seed: commits since v0.98.0, confirm the entries above cover them, then delete this comment
|
|
123
|
-
- chore(core): move the time format descriptions to parse_borrowed::<3>
|
|
124
|
-
- build(deps): bump lopdf from 0.42.0 to 0.44.0
|
|
125
|
-
- build(deps): bump thiserror in the cargo group across 1 directory
|
|
126
|
-
- build(deps): bump taiki-e/install-action
|
|
127
|
-
- chore(core): bump serde-saphyr to 1.0
|
|
128
|
-
- Split lint back out of test
|
|
129
|
-
- Link every shipped artifact in CI, and carry v0.99.0's notes forward
|
|
130
|
-
- dense-prose pass over the CI scope
|
|
131
|
-
- Cut CI to one job per shipped surface
|
|
132
|
-
- Stop the release from pushing to a branch that takes only PRs
|
|
133
|
-
- Fix three claims the independent review falsified
|
|
134
|
-
- Correct the release notes' exhaustive-type count
|
|
135
|
-
- dense-prose pass over the review's scope
|
|
136
|
-
- Close the two payload doors the depth guard missed
|
|
137
|
-
- Ship the license the workspace actually grants
|
|
138
|
-
- Correct the release docs against the tree they describe
|
|
139
|
-
- CLAUDE.md: tighten the Tests section to house voice
|
|
140
|
-
- CLAUDE.md: drop `uv run` from the Python binding flow
|
|
141
|
-
- CLAUDE.md: gate local binding builds, name the cheap profiles
|
|
142
|
-
- chore(release): v0.99.0
|
|
143
|
-
- Split serial.rs's fixed-point claim into the two it conflated
|
|
144
|
-
- Make Fidelity the one place a loss class is spelled
|
|
145
|
-
- Make an island's loss class injective instead of guarded
|
|
146
|
-
- dense-prose and simplify passes over the handle-check work
|
|
147
|
-
- Keep the colon out of embedded YAML values
|
|
148
|
-
- Sweep the em-dash out of comments and prose
|
|
149
|
-
- Refuse a core handle from a second copy, everywhere
|
|
150
|
-
- Replace the em-dash ban with the rule the exemplars follow
|
|
151
|
-
- dense-prose pass over the foreign-handle prose
|
|
152
|
-
- Extend the policy to the writer/reader lane, and draw the read/write line
|
|
153
|
-
- Keep the patched method names, and stop over-claiming in the warning
|
|
154
|
-
- Tolerate foreign core handles on the by-reference methods
|
|
155
|
-
- Correct the quill file-size cap doc and MiB units
|
|
156
|
-
- Collapse the rustdoc gate onto --workspace
|
|
157
|
-
- dense-prose pass over the release-readiness fixes
|
|
158
|
-
- Close the release-readiness gaps found against v0.98.0
|
|
159
|
-
- Drop the semver job
|
|
160
|
-
- Trim the Python typing work to its load-bearing parts
|
|
161
|
-
- Revert "Record KnownIslandType::ALL's slice shape in the 0.99 guide"
|
|
162
|
-
- Record KnownIslandType::ALL's slice shape in the 0.99 guide
|
|
163
|
-
- Close the Python Tier-1 gaps: single-card reads and a typed surface
|
|
164
|
-
- Drop the top-level-await plugin the vite 8 test config cannot load
|
|
165
|
-
- Shape the reserved-name lists as slices
|
|
166
|
-
- Shape the island ALL as a slice; list the 0.99 guide as published
|
|
167
|
-
- Prune rottable duplication from CLAUDE.md
|
|
168
|
-
- Adopt the editor's dense-prose rewrite; compress CLAUDE.md
|
|
169
|
-
- build(deps): bump the cargo group across 1 directory with 12 updates
|
|
170
|
-
- build(deps): bump the actions group across 1 directory with 11 updates
|
|
171
|
-
- build(deps): bump tsify from 0.4.5 to 0.5.6
|
|
172
|
-
- build(deps): bump toml from 0.8.23 to 1.1.3+spec-1.1.0
|
|
173
|
-
- build(deps): bump similar from 2.7.0 to 3.1.1
|
|
174
|
-
- build(deps): bump serde-saphyr from 0.0.23 to 0.0.29
|
|
175
|
-
- Run the semver job on release PRs, not on every one
|
|
176
|
-
- Drop the dead check-cfg rustflag that crashed the semver job
|
|
177
|
-
- docs: dense-prose pass over the new test comments
|
|
178
|
-
- dense-prose pass over the branch
|
|
179
|
-
- Gate the compatibility promise mechanically, and write it down
|
|
180
|
-
- test(bindings): cover the WASM and Python exports nothing exercised
|
|
181
|
-
- Close the struct half of the 1.0.0 non_exhaustive sweep, and seal Backend
|
|
182
|
-
- test(pdfform): drive unbound text/checkbox/choice through the fixture
|
|
183
|
-
- Move the workspace to edition 2024
|
|
184
|
-
- build(deps-dev): bump vitest in /crates/bindings/wasm
|
|
185
|
-
- build(deps-dev): bump typescript in /crates/bindings/wasm
|
|
186
|
-
- prose: drop the readiness proposal and SECURITY.md
|
|
187
|
-
- ci: drop the Windows matrix from the test job
|
|
188
|
-
- Finish the coerce_value_strict cleanup in the fuzz crate
|
|
189
|
-
- dense-prose pass across the code and docs
|
|
190
|
-
- Revert the wasm job to `npm install`
|
|
191
|
-
- Trim CI to the checks v1.0.0 actually needs
|
|
192
|
-
- Cut the two CI costs that buy nothing
|
|
193
|
-
- Clear the RUSTSEC vulnerabilities the audit job actually found
|
|
194
|
-
- Pin the tree to LF so the Windows runner reads what the test wrote
|
|
195
|
-
- Cleanup pass: cut the machinery the fix did not need
|
|
196
|
-
- prose: record what landed against the v1.0.0 readiness findings
|
|
197
|
-
- fuzz: cover the four JSON decode lanes the bindings expose
|
|
198
|
-
- Declare and hold an MSRV
|
|
199
|
-
- Ship the license text, verify the packages, scan the dependencies
|
|
200
|
-
- Open the engine crates' public enums ahead of the 1.0.0 freeze
|
|
201
|
-
- Hide the workspace-internal seams from the published surface
|
|
202
|
-
- typst: quill-load defects become session warnings, not stderr
|
|
203
|
-
- core: own the YAML boundary error, and budget the third entry point
|
|
204
|
-
- prose: index the v1.0.0 readiness findings against their issues
|
|
205
|
-
- prose: v1.0.0 production-readiness review
|
|
206
|
-
- Drop issue markers; dense-prose the unreleased 0.99 guide
|
|
207
|
-
- dense-prose pass over the PR 1097 scope
|
|
208
|
-
- docs: record the op-wire key-order change in the unreleased guide
|
|
209
|
-
- content: carry the `<u>` bit on the event, not in a shared set
|
|
210
|
-
- content: one key sorter for the canonical form
|
|
211
|
-
- content: bound opaque JSON payload depth on the Value lane
|
|
212
|
-
- content: make a vocabulary promotion carry its own legacy form
|
|
213
|
-
- content: simplify pass over the codec-gap changes
|
|
214
|
-
- content: house voice on the codec-gap comments
|
|
215
|
-
- content: close the three v1.0.0 codec gaps before the freeze
|
|
216
|
-
- content: cut the enumerated halves of the two open-set tests
|
|
217
|
-
- content: prune tests that assert what a neighbour already pins
|
|
218
|
-
- content: fold the authored-lane scan into the shared cell walk
|
|
219
|
-
- content: split the wire lanes on reserved-name reuse (#1084, #1085, #1086)
|
|
220
|
-
- docs: trim the migration index to a routing table; drop duplicate links
|
|
221
|
-
- refactor(typst): re-export span_scan/extract instead of forwarding
|
|
222
|
-
-->
|
|
223
|
-
|
|
224
|
-
|
|
225
229
|
## v0.98.0 - 2026-07-28
|
|
226
230
|
|
|
227
231
|
Five breaking changes, all covered by `docs/migrations/0.97-to-0.98.md`.
|
|
@@ -370,7 +374,7 @@ Stored documents are unaffected: a `0.97` blob loads byte-identically and
|
|
|
370
374
|
- **breaking** core,wasm,python: a schema-bound read view — `Quill::view(&doc)` / `quill.view(doc)`, the read twin of `quill.writer(doc)`. `view.get(addr)` interprets each field by its declared type (a `richtext` field → markdown, a `plaintext` field → its literal text via the plaintext codec, every other type → its canonical value verbatim), returns absent as `undefined` / `None`, and — the authority the quill-free `getMarkdown` lacks — throws `UnknownField` for a name the schema does not declare and `FieldRichtextDecode` for a content field holding an undecodable value. Core `TypedReader::get` returns a `ReadValue` (`Markdown`/`Plaintext`/`Value`); `view.card(i)` is the card cursor; core adds `Card::field_plaintext` (the `to_plaintext` twin of `field_markdown`). **`getMarkdown`'s field half retires**: `getMarkdown` / `get_markdown` / `get_card_markdown` are now body-only (WASM `getMarkdown` takes a `CardAddr`, a present `field` throws; Python drops the `name` parameter) — a field's markdown is read through `view.get`. The quill-free body projection stays on `Document` (#978)
|
|
371
375
|
- **breaking** content: one delta-application contract — implicit trailing retain is `try_apply`'s semantics (a short delta retains the untouched remainder; the error is over-consumption only), `apply` panics on an over-long delta instead of clamping (clamping is silent corruption), and `extend_to_base` is removed. `split_line` / `join_line` rebase marks through their one-char `\n` splice with `map_pos` — the same mapping the text-delta channel uses — so marks no longer drift across line ops and `apply_field_change` canonicalizes once (a single terminal normalize instead of one per stage); line sync rebuilds in one forward pass instead of per-`\n` `Vec` splices. Mark ops are specified in final-text coordinates (post-delta, post-line-op — the frame they validate against) (#926, #987)
|
|
372
376
|
- **breaking** core: storage blobs tagged `@0.81.0` / `@0.82.0` fail as an unknown schema version — the read-only `V0_81_0` / `V0_82_0` DTO trees and their forward migrations are retired (nothing persisted on this lineage predates `@0.92.0`; `0.82.0` was yanked). `V0_92_0` stays the oldest shape read, and its payload types back the current write path. DOCUMENT_STORAGE.md records variant retirement as the policy when no stored population remains (#929)
|
|
373
|
-
- **breaking** core,wasm,python: the markdown projection stops appending a trailing newline — `to_markdown` projects a *value*, not a file, so `field_markdown` / `body_markdown` (WASM `getMarkdown` / `exportMarkdown`, Python `export_markdown` / `get_markdown`) no longer grow a `\n`; `writer.set("subject", "Hello")` reads back as `"Hello"`, not `"Hello\n"`.
|
|
377
|
+
- **breaking** core,wasm,python: the markdown projection stops appending a trailing newline — `to_markdown` projects a *value*, not a file, so `field_markdown` / `body_markdown` (WASM `getMarkdown` / `exportMarkdown`, Python `export_markdown` / `get_markdown`) no longer grow a `\n`; `writer.set("subject", "Hello")` reads back as `"Hello"`, not `"Hello\n"`. The content fixed point is unchanged (import is newline-insensitive) (#965)
|
|
374
378
|
- **breaking** all: rename the content genus off its codec's name — crate `quillmark-richtext` → `quillmark-content`, type `RichText` → `Content` (and `RichTextLine`/`RichTextContainer`/`RichTextMark`/`RichTextIsland` → `ContentLine`/…), const `RICHTEXT_MEDIA_TYPE` → `CONTENT_MEDIA_TYPE` and its wire string `application/quillmark-richtext+json` → `application/quillmark-content+json`, `#[serde(skip)]` companion caches `FieldSchema::{default,example}_corpus` → `_content`, `SegmentMap.corpus: Range<usize>` → `.content`, Typst-emitter `EmittedContent` → `Emission` (it is markup + source map, not a Typst `content` value). Schema tokens `richtext` / `plaintext`, `FieldType::{RichText,PlainText}` variants, and the codec-specific `field_richtext` / `FieldRichtext*` / `apply_field_richtext_change` / `richtext(inline)` surface are unchanged — those name codecs, not the model. Canonical body JSON is nameless, so stored documents don't migrate; `contentMediaType` consumers pin to the new spelling. Retires the informal "corpus" noun to end the code/prose split (#976)
|
|
375
379
|
- **breaking** core,wasm,python: `getMarkdown` / `get_markdown` / `get_card_markdown` stop conflating an absent field with a present-but-not-richtext one — a present field that does not decode as richtext (a scalar/array/object a `storeField` wrote) now throws `FieldRichtextDecode` instead of reading back `undefined` / `""`; absence still returns the absent shape. Core `Card::field_markdown` becomes `Option<Result<String, RichtextDecodeError>>` (the projection twin of `field_richtext`). Rule: absence returns, mismatch raises; read the raw value with `get` (#968)
|
|
376
380
|
- feat(core,wasm): typed, anchor-preserving field revise — `TypedWriter::revise_field` / `CardWriter::revise_field` and `writer.reviseField` / `writer.card(i).reviseField` wrap core `Card::revise_field_checked` (diff-rebase surviving anchors, then schema-conform the result); the schema-bound verb lives on the writer, where the schema is (#957, #966)
|
package/README.md
CHANGED
|
@@ -94,10 +94,37 @@ For an example-filled starter use `quill.seedDocument()`. Throws on an
|
|
|
94
94
|
invalid quill reference.
|
|
95
95
|
|
|
96
96
|
### `Document.fromMarkdown(markdown)`
|
|
97
|
-
Parse markdown to a parsed document
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
Parse markdown to a parsed document, quill-free: the **transport door**
|
|
98
|
+
(migrations, `$ext` stamping, a quill that will not load, opening a document to
|
|
99
|
+
fix its `$quill`). Throws a JS `Error` (with `.diagnostics` attached, see
|
|
100
|
+
[Errors](#errors)) on any parse failure, including a missing root `$quill`
|
|
101
|
+
metadata line, malformed YAML, and inputs over the 10 MiB
|
|
102
|
+
`parse::input_too_large` limit. A content field rests as authored; `quill.parse`
|
|
103
|
+
below is the bound door that lands it at its canonical rest.
|
|
104
|
+
|
|
105
|
+
### `quill.parse(markdown)` / `quill.conform(doc)`
|
|
106
|
+
The **bound door**, and the primary ingestion path. `quill.parse` is
|
|
107
|
+
`Document.fromMarkdown` followed by `conform`: the returned document's declared
|
|
108
|
+
content fields rest at one form per codec (a `richtext` field as the canonical
|
|
109
|
+
content object, a `plaintext` field as its literal string), so `getStored`
|
|
110
|
+
answers "corpus or string?" by the field's declared codec rather than by how the
|
|
111
|
+
document was built. Parse warnings and the `conform::*` warnings both ride
|
|
112
|
+
`doc.warnings`.
|
|
113
|
+
|
|
114
|
+
`quill.conform(doc)` is the same walk in place on a document that arrived any
|
|
115
|
+
other way (`fromJson`, a stored row), returning the `conform::*` `Diagnostic[]`
|
|
116
|
+
(`[]` when everything rested). It is idempotent and a byte no-op on an
|
|
117
|
+
already-canonical document, YAML comments included, so calling it on every load
|
|
118
|
+
is safe. A `!must_fill` marker anywhere in a field's value skips that field; a
|
|
119
|
+
value the strict write refuses stays as authored under a warning. Both throw
|
|
120
|
+
when the document declares a `$quill` this quill does not answer to, before any
|
|
121
|
+
mutation.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const doc = quill.parse(markdown); // rests canonical
|
|
125
|
+
const stale = Document.fromJson(row);
|
|
126
|
+
const diags = quill.conform(stale); // converges in place
|
|
127
|
+
```
|
|
101
128
|
|
|
102
129
|
### `doc.toMarkdown()`
|
|
103
130
|
Emit canonical Quillmark Markdown. Type-fidelity round-trip safe:
|
|
@@ -259,7 +286,11 @@ card's. Reads are total over the field axis (`getStored` → `undefined`, `isFil
|
|
|
259
286
|
an absent field; only an out-of-range card throws); field writes throw on a body
|
|
260
287
|
address. `getStored` is the verbatim transport read, distinct from the interpreted
|
|
261
288
|
`quill.reader(doc).get`; `getMarkdown` is the body markdown read (a `CardAddr`; a field's
|
|
262
|
-
markdown is read through `quill.reader(doc).get(field)`).
|
|
289
|
+
markdown is read through `quill.reader(doc).get(field)`). A content field's stored
|
|
290
|
+
form follows how the document was built (a canonical content object when the
|
|
291
|
+
typed writer committed it, the authored string when a markdown parse produced
|
|
292
|
+
it), so for the corpus either way read `quill.reader(doc).getContent(addr)`, which
|
|
293
|
+
decodes through the codec the field's declared type names. Card-scoped verbs take a
|
|
263
294
|
`CardAddr` (`{ card? }`) first: `doc.getExt({ card: 2 })`, and the batch below.
|
|
264
295
|
|
|
265
296
|
Batch mutation: `doc.storeFields({}, {...})` / `doc.storeFields({ card: index }, {...})`
|
|
@@ -324,6 +355,24 @@ ed.card(2).set("body", "**note**"); // composable card, resolved
|
|
|
324
355
|
is lazy: it never throws; an out-of-range index throws `IndexOutOfRange` at the
|
|
325
356
|
write.
|
|
326
357
|
|
|
358
|
+
#### `DocumentReader` / `CardReader`: the read twin
|
|
359
|
+
|
|
360
|
+
`quill.reader(doc)` carries the writer's ephemerality and its schema authority:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
const v = quill.reader(doc);
|
|
364
|
+
v.get("subject"); // by declared type: richtext → markdown, plaintext → literal text
|
|
365
|
+
v.getContent("subject"); // the same read as a Content corpus, whichever lane stored it
|
|
366
|
+
v.getBody(); // the main body markdown (quill-free)
|
|
367
|
+
v.card(0).get("body"); // a card field, resolved by its $kind
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`get` projects and `getContent` returns the corpus; both decode through the codec
|
|
371
|
+
the field's **declared type** names, which is why they bind the quill and the
|
|
372
|
+
verbatim `doc.getStored` does not. An undeclared name throws `UnknownField`, a
|
|
373
|
+
type that is not a content leaf throws `FieldNotContent`, and an undecodable
|
|
374
|
+
value throws `FieldRichtextDecode`; an absent field reads back `undefined`.
|
|
375
|
+
|
|
327
376
|
### `engine.render(quill, parsed, opts?)` vs. `engine.open(quill, parsed)`
|
|
328
377
|
|
|
329
378
|
Use **`engine.render`** for one-shot exports (PDF/SVG/PNG): compiles, emits
|
|
@@ -32,14 +32,13 @@ export type PayloadItem =
|
|
|
32
32
|
*
|
|
33
33
|
* `$` system entries are hoisted to named fields: `kind` (the `$kind`, empty
|
|
34
34
|
* string when none), optional `quill` (the `$quill` `name@version`, main card
|
|
35
|
-
* only), optional `
|
|
35
|
+
* only), optional `ext` (`$ext`), and optional `seed`
|
|
36
36
|
* (the `$seed` per-kind overlay map, main card only). `payloadItems` carries
|
|
37
37
|
* user fields and comments in order.
|
|
38
38
|
*/
|
|
39
39
|
export interface Card {
|
|
40
40
|
kind: string;
|
|
41
41
|
quill?: string;
|
|
42
|
-
id?: string;
|
|
43
42
|
ext?: Record<string, unknown>;
|
|
44
43
|
seed?: Record<string, unknown>;
|
|
45
44
|
payloadItems: PayloadItem[];
|
|
@@ -63,7 +62,6 @@ export interface Card {
|
|
|
63
62
|
export interface CardInput {
|
|
64
63
|
kind: string;
|
|
65
64
|
quill?: string;
|
|
66
|
-
id?: string;
|
|
67
65
|
ext?: Record<string, unknown>;
|
|
68
66
|
seed?: Record<string, unknown>;
|
|
69
67
|
payloadItems?: PayloadItem[];
|
|
@@ -242,13 +240,42 @@ export type LineOp =
|
|
|
242
240
|
| { op: "setContainers"; line: number; containers: ContentContainer[] }
|
|
243
241
|
| { op: "setContinues"; line: number; continues: boolean };
|
|
244
242
|
|
|
243
|
+
/**
|
|
244
|
+
* An island edit: the only channel that reaches an island's payload, a table's
|
|
245
|
+
* cells or an image's url.
|
|
246
|
+
*
|
|
247
|
+
* Both ops move one island entry and leave the field's text and marks alone, so
|
|
248
|
+
* an island edit keeps every identity anchor in the field. That is why a table
|
|
249
|
+
* edit lowers to `applyChange` rather than `install`, which drops them all.
|
|
250
|
+
*
|
|
251
|
+
* `set` addresses an existing island by `id`; an `id` no island carries throws
|
|
252
|
+
* rather than passing silently. `insert` places a new island's slot at `at` (a
|
|
253
|
+
* post-delta USV position) together with its entry, so a slot never exists
|
|
254
|
+
* without an island behind it; its `id` must be non-empty and unused. Deleting
|
|
255
|
+
* an island needs no op: a `delta` that removes its slot drops the island.
|
|
256
|
+
*
|
|
257
|
+
* A `set` stores the `loss` it is given: nothing re-derives the class from the
|
|
258
|
+
* new `props`, so a write that changes what markdown can carry must say so.
|
|
259
|
+
*
|
|
260
|
+
* An island is *inline* (a slot inside a paragraph) unless its line says
|
|
261
|
+
* otherwise. A **block** island is one bundle of all three channels, in the
|
|
262
|
+
* order they apply: `delta` inserts the `\n` that opens the line, `islandOps`
|
|
263
|
+
* inserts the slot, `lineOps` tags the line `{ op: "setKind", kind: "island" }`.
|
|
264
|
+
* `{ op: "split" }` cannot open that line, since line ops run after island ops.
|
|
265
|
+
*/
|
|
266
|
+
export type IslandOp =
|
|
267
|
+
| ({ op: "set" } & ContentIsland)
|
|
268
|
+
| ({ op: "insert"; at: number } & ContentIsland);
|
|
269
|
+
|
|
245
270
|
/**
|
|
246
271
|
* A committed content edit bundle for `applyChange`: a text `delta` (default no
|
|
247
|
-
* text change), then `lineOps`, then `markOps` (mark ranges
|
|
248
|
-
* coordinates). Every field is
|
|
272
|
+
* text change), then `islandOps`, then `lineOps`, then `markOps` (mark ranges
|
|
273
|
+
* are in final-text coordinates: every earlier channel applied). Every field is
|
|
274
|
+
* optional.
|
|
249
275
|
*/
|
|
250
276
|
export interface ChangeBundle {
|
|
251
277
|
delta?: Delta;
|
|
278
|
+
islandOps?: IslandOp[];
|
|
252
279
|
lineOps?: LineOp[];
|
|
253
280
|
markOps?: MarkOp[];
|
|
254
281
|
}
|
|
@@ -593,6 +620,16 @@ export interface Diagnostic {
|
|
|
593
620
|
*/
|
|
594
621
|
path?: string;
|
|
595
622
|
hint?: string;
|
|
623
|
+
/**
|
|
624
|
+
* The facts `message` interpolates, keyed by name. With `code`, the
|
|
625
|
+
* substitution unit needed to word this diagnostic in another language;
|
|
626
|
+
* `prose/canon/ERROR.md` § \"Diagnostic args\" tabulates the keys per code.
|
|
627
|
+
*
|
|
628
|
+
* Declared optional explicitly because `tsify` does not read
|
|
629
|
+
* `skip_serializing_if`: without this, a field the runtime omits is
|
|
630
|
+
* declared required. `sourceChain` carries that mismatch.
|
|
631
|
+
*/
|
|
632
|
+
args?: Record<string, unknown>;
|
|
596
633
|
sourceChain?: string[];
|
|
597
634
|
}
|
|
598
635
|
|
|
@@ -714,11 +751,16 @@ export class Document {
|
|
|
714
751
|
free(): void;
|
|
715
752
|
[Symbol.dispose](): void;
|
|
716
753
|
/**
|
|
717
|
-
* **Apply** a committed content edit `bundle`
|
|
718
|
-
*
|
|
719
|
-
*
|
|
754
|
+
* **Apply** a committed content edit `bundle`
|
|
755
|
+
* (`{ delta?, islandOps?, lineOps?, markOps? }`) at `addr`, the editor
|
|
756
|
+
* splice: text delta first, then island ops, then line ops, then mark ops
|
|
757
|
+
* (mark ranges in final-text coordinates), each all-or-nothing. An absent
|
|
720
758
|
* `addr.field` targets the body, an absent `addr.card` the main card.
|
|
721
759
|
*
|
|
760
|
+
* The island channel keeps a table or image edit on the op path: it moves
|
|
761
|
+
* the island alone, so the anchors elsewhere in the field survive an edit
|
|
762
|
+
* `install` would clear.
|
|
763
|
+
*
|
|
722
764
|
* Throws on an out-of-range card, a field that is not richtext, a malformed
|
|
723
765
|
* bundle, or an op that applies out of bounds (the value is unchanged on a
|
|
724
766
|
* failed apply).
|
|
@@ -739,13 +781,6 @@ export class Document {
|
|
|
739
781
|
* verbs.
|
|
740
782
|
*/
|
|
741
783
|
card(index: number): Card;
|
|
742
|
-
/**
|
|
743
|
-
* The index of the composable card whose `$id` equals `id`, or
|
|
744
|
-
* `undefined` when none carries it. Resolves the durable card handle
|
|
745
|
-
* without a hand-rolled scan over [`cards`](Self::cards); `$id` is
|
|
746
|
-
* unique per document, so at most one card matches.
|
|
747
|
-
*/
|
|
748
|
-
cardIndexById(id: string): number | undefined;
|
|
749
784
|
clone(): Document;
|
|
750
785
|
/**
|
|
751
786
|
* Schema version this build writes via [`toJson`](Document::to_json).
|
|
@@ -813,8 +848,7 @@ export class Document {
|
|
|
813
848
|
getMarkdown(addr?: CardAddr): string;
|
|
814
849
|
/**
|
|
815
850
|
* Read the **verbatim stored value** at `addr`: the raw payload value of a
|
|
816
|
-
* field
|
|
817
|
-
* otherwise), or the **body content** when `addr.field` is absent. A bare
|
|
851
|
+
* field, or the **body content** when `addr.field` is absent. A bare
|
|
818
852
|
* string is `Addr` shorthand for `{ field }`. Reads are total over the field
|
|
819
853
|
* axis: an absent field is `undefined`; only an out-of-range `addr.card`
|
|
820
854
|
* throws `edit::index_out_of_range`. Needs no schema, so it lives on
|
|
@@ -822,6 +856,16 @@ export class Document {
|
|
|
822
856
|
* the interpreted schema-plane [`reader.get`](Self::reader_get). For the
|
|
823
857
|
* markdown projection use [`getMarkdown`](Self::get_markdown) (body) or
|
|
824
858
|
* `reader.get` (a field's declared type).
|
|
859
|
+
*
|
|
860
|
+
* **A content field at rest has one stored form per codec**: a `richtext`
|
|
861
|
+
* field holds the canonical content object, a `plaintext` field its literal
|
|
862
|
+
* string. A document that came through the bound door (`quill.parse` /
|
|
863
|
+
* `quill.conform`) is at rest, so this read no longer depends on which lane
|
|
864
|
+
* built it. A document that came through the transport door
|
|
865
|
+
* (`Document.fromMarkdown`, a legacy stored row) may rest as authored until
|
|
866
|
+
* it is conformed, and this read reports what is there. For the corpus
|
|
867
|
+
* either way, use the schema-plane `reader.getContent`, which decodes
|
|
868
|
+
* through the codec the field's declared type names.
|
|
825
869
|
*/
|
|
826
870
|
getStored(addr: Addr | string): unknown;
|
|
827
871
|
/**
|
|
@@ -1054,6 +1098,12 @@ export class Document {
|
|
|
1054
1098
|
*/
|
|
1055
1099
|
readonly main: Card;
|
|
1056
1100
|
readonly quillRef: string;
|
|
1101
|
+
/**
|
|
1102
|
+
* The non-fatal diagnostics of the load that produced this document: parse
|
|
1103
|
+
* warnings, plus the `conform::*` warnings when it came through
|
|
1104
|
+
* `quill.parse`. Session state, not document value: `equals` and the
|
|
1105
|
+
* storage DTO exclude it, and `fromJson` / `loadJson` clear it.
|
|
1106
|
+
*/
|
|
1057
1107
|
readonly warnings: Diagnostic[];
|
|
1058
1108
|
}
|
|
1059
1109
|
|
|
@@ -1185,6 +1235,23 @@ export class Quill {
|
|
|
1185
1235
|
private constructor();
|
|
1186
1236
|
free(): void;
|
|
1187
1237
|
[Symbol.dispose](): void;
|
|
1238
|
+
/**
|
|
1239
|
+
* Land `doc`'s declared content fields at their canonical rest **in
|
|
1240
|
+
* place**, returning the `conform::*` diagnostics for the values that would
|
|
1241
|
+
* not commit (an empty array when everything rested).
|
|
1242
|
+
*
|
|
1243
|
+
* The read-repair verb: a document that arrived through the transport door
|
|
1244
|
+
* (`fromMarkdown`, `fromJson`, a stored row) converges here, and is then
|
|
1245
|
+
* eligible for rewrite under its current schema tag. Idempotent, and a
|
|
1246
|
+
* no-op on an already-canonical document: an equal value is not rewritten,
|
|
1247
|
+
* so YAML comments and stored bytes survive.
|
|
1248
|
+
*
|
|
1249
|
+
* A `!must_fill` marker anywhere in a field's value skips that field (the
|
|
1250
|
+
* marker is the state), and a value the strict write refuses stays as
|
|
1251
|
+
* authored with a diagnostic. Throws when `doc` declares a different
|
|
1252
|
+
* `$quill`, before any mutation.
|
|
1253
|
+
*/
|
|
1254
|
+
conform(doc: Document): Diagnostic[];
|
|
1188
1255
|
/**
|
|
1189
1256
|
* Build a quill from a file tree. Pure: no backend, no engine; the
|
|
1190
1257
|
* declared backend is resolved later, at render time.
|
|
@@ -1195,6 +1262,21 @@ export class Quill {
|
|
|
1195
1262
|
* canonical shape.
|
|
1196
1263
|
*/
|
|
1197
1264
|
static fromTree(tree: Map<string, Uint8Array>): Quill;
|
|
1265
|
+
/**
|
|
1266
|
+
* Parse `markdown` and conform it against this quill: the **primary
|
|
1267
|
+
* ingestion path**, and the bound twin of the schema-free
|
|
1268
|
+
* `Document.fromMarkdown`. The returned document rests at its canonical
|
|
1269
|
+
* form (a `richtext` field as a content object, a `plaintext` field as its
|
|
1270
|
+
* literal string), so `getStored` no longer answers "corpus or string?"
|
|
1271
|
+
* with "depends how this document was built".
|
|
1272
|
+
*
|
|
1273
|
+
* Parse warnings and the `conform::*` diagnostics both land on
|
|
1274
|
+
* `doc.warnings`. Throws on a parse failure, or when `markdown` declares a
|
|
1275
|
+
* `$quill` this quill does not answer to: nothing conforms under the wrong
|
|
1276
|
+
* schema. To open a document whose `$quill` is stale, use the transport
|
|
1277
|
+
* door (`Document.fromMarkdown`, `setQuillRef`, then `quill.conform`).
|
|
1278
|
+
*/
|
|
1279
|
+
parse(markdown: string): Document;
|
|
1198
1280
|
/**
|
|
1199
1281
|
* The resolved-value view of `doc` against this quill's schema: for every
|
|
1200
1282
|
* declared field the value the render projection would use and the
|