0.94 → 0.95 — the write surface reshapes: store* verbs, one address, a schema-checked revise¶
The headline of 0.95 is a coordinated reshaping of the mutation surface across
core and both bindings (#955, #957, #960), landed as one sweep so nothing is
written under a name it immediately loses. Alongside it: the WASM card surface
loses a redundant verb, the core Payload map API grows a validation floor,
parse warnings stop living on Document, and the content-model genus renames
off its codec's name (#976). All breaking, all pre-1.0 cleanups; the rest of the
release is additive.
The content genus renames off RichText (#976)¶
RichText (the type) named the model after one of its two codecs, so
"a plaintext field holds a RichText" was the contradiction the code kept
having to narrate. The genus renames to Content — the token the Typst backend
already forks on (content vs str, emit.rs), and the token the schema
already uses for the category ("content field"). richtext and plaintext stay
as the two schema codecs into Content; a plaintext field still holds a
Content (constrained mark-free and island-free), just imported through the
literal codec instead of markdown.
Mechanical renames:
| 0.94 | 0.95 |
|---|---|
crate quillmark-richtext |
quillmark-content (pre-1.0; old versions remain on crates.io under the old name) |
type RichText |
Content |
type RichTextLine / RichTextContainer / RichTextMark / RichTextIsland (WASM .d.ts / Python) |
ContentLine / ContentContainer / ContentMark / ContentIsland |
const RICHTEXT_MEDIA_TYPE |
CONTENT_MEDIA_TYPE |
media type string application/quillmark-richtext+json |
application/quillmark-content+json (consumer-visible in transform schema contentMediaType, PLATE_DATA.md, SCHEMAS.md) |
struct field FieldSchema::default_corpus / example_corpus (#[serde(skip)] caches) |
default_content / example_content |
SegmentMap.corpus: Range<usize> (Typst emitter source map) |
SegmentMap.content: Range<usize> |
EmittedContent { markup, segments } (Typst emitter output) |
Emission — it is not a Typst content value; renaming it avoids an active ambiguity next to the Content genus and pairs with emit_*. |
type CorpusHit (core region.rs; position-hit in WASM .d.ts / Python) |
ContentHit — the informal noun in a public type name (residual sweep, #982). |
EditError::CorpusApply |
EditError::ContentApply (message CorpusApply → ContentApply). |
RichtextDecodeError::NotCorpus variant |
NotContent — the error type stays RichtextDecodeError (codec-specific, the error of Card::field_richtext); only the variant carried the informal noun. |
storage DTO CanonicalRichText (dto.rs, the body field's type) |
CanonicalContent — a Content wrapper, not a codec; the name still spelled the old model. Serde is unchanged, so no wire migration. |
Typst emitter emit_richtext / emit_richtext_inline |
emit_content / emit_content_inline — model-generic (they lower any Content, richtext or plaintext), so codec-named misdescribed them. |
informal noun "the corpus" / "the RichText corpus" |
"the content" / "a Content value" — retires the code/prose split. |
Kept — these are genuinely codec-specific, not model-generic:
- Schema type tokens
richtext/plaintext(YAMLtype: richtext). FieldType::RichText { inline }/FieldType::PlainText { inline }enum variants (they name schema codecs, not the model).Card::field_richtext/Card::apply_field_richtext_change,EditError::FieldRichtextDecode/FieldRichtextNotInline,richtext(inline)blueprint syntax — the richtext-codec API surface.- Test fixtures whose codec is richtext (
richtext_form/, etc.).
Canonical wire JSON ({text, lines, marks, islands}) is nameless — no data
migration on stored bodies or transform-schema instances. The contentMediaType
string in transform schemas changes; consumers pinned to
application/quillmark-richtext+json update to the new spelling in one line.
Rust import changes are one substitution across a project:
// 0.94
use quillmark_richtext::RichText;
use quillmark_richtext::{from_markdown, to_markdown};
// 0.95
use quillmark_content::Content;
use quillmark_content::{from_markdown, to_markdown};
Cargo.toml:
The opaque store renames set* → store* (#960)¶
The write surface has three lanes; the verb now carries which one. store =
verbatim (quill-free, coercion deferred to render), set = typed (the writer,
strict commit at the write), install / revise / apply = content
(identity-aware). One sentence in BINDINGS.md states it; the names stop needing
per-verb disambiguation. remove_* stays — removal has no lane.
| Lane | 0.94 | 0.95 |
|---|---|---|
Core Card |
set_field / set_fields / set_fill / set_ext / set_ext_namespace / set_seed_namespace |
store_field / store_fields / store_fill / store_ext / store_ext_namespace / store_seed_namespace |
WASM Document |
setField / setFields / setFill / setExt / setExtNamespace / setSeedNamespace |
storeField / storeFields / storeFill / storeExt / storeExtNamespace / storeSeedNamespace (and re-addressed — see below) |
Python Document |
set_field / set_fields / set_ext / set_card_field / set_card_ext / … |
store_field / store_fields / store_ext / store_card_field / store_card_ext / … |
The typed writer (set / set_all / set_body) keeps its names — the typed
lane. Its wasm ABI is underscored to _commitField / _commitFields and hidden
(#966, below). set_card_kind (a $kind write, structural, not a field store)
is unchanged. This makes the call #913 deferred for the Python mirror, globally.
WASM Document unifies on one address (#955)¶
The WASM Document class encoded "main vs card at index" as verb duplication —
22 methods for 11 concepts (setField / setCardField, get / getCardField,
…). They collapse onto the Addr the content lane (install / revise /
applyChange) already spoke: { card?, field? }, absent card = main, absent
field = body. A bare string is Addr shorthand for { field }, so the
common case stays terse.
0.94 0.95
doc.setField("qty", 3) doc.storeField("qty", 3)
doc.setCardField(2, "qty", 3) doc.storeField({ card: 2, field: "qty" }, 3)
doc.setFields(fields) doc.storeFields({}, fields)
doc.setCardFields(2, fields) doc.storeFields({ card: 2 }, fields)
doc.get("qty") doc.get("qty") // unchanged (string ⇒ {field})
doc.getCardField(2, "qty") doc.get({ card: 2, field: "qty" })
doc.getMarkdown("intro") doc.getMarkdown("intro") // unchanged
doc.getCardMarkdown(2, "intro") doc.getMarkdown({ card: 2, field: "intro" })
doc.removeCardField(2, "qty") doc.removeField({ card: 2, field: "qty" })
doc.setExt(map) doc.storeExt({}, map)
doc.setCardExt(2, map) doc.storeExt({ card: 2 }, map)
doc.setExtNamespace(ns, v) doc.storeExtNamespace({}, ns, v)
doc.removeExtNamespace(ns) doc.removeExtNamespace({}, ns)
doc.removeCardExt(2) doc.removeExt({ card: 2 })
doc.commitField(quill, "qty", v) writer.set("qty", v) // typed writer; ABI is _commitField (#966)
doc.commitCardField(quill, 2, "qty", v) writer.card(2).set("qty", v)
doc.commitFields(quill, fields) writer.setAll(fields)
doc.commitCardFields(quill, 2, fields) writer.card(2).setAll(fields)
Three rules govern the surface:
- Reads are total over the field axis.
get/getMarkdown/isFillreturn the value / projection / fill-flag for a present field, andundefined/undefined/falsefor an absent one — only an out-of-rangecardthrows.getMarkdownnow returnsundefined, not"", for an absent field (absence vs empty is real signal for must-fill / seeding UIs); a body address reads the body content (get) or its markdown (getMarkdown). One carve-out lands the same release:getMarkdownthrowsFieldRichtextDecodefor a present field that does not decode as richtext — a type mismatch is not absence (#968, its own section below). - Field writes require a field.
storeField/storeFill/removeField/_commitFieldthrow on a body address (nofield) — a body is never opaque and has no field schema; write it withrevise/install/writer.setBody. - Card-scoped verbs take a
CardAddr({ card? }) first and throw on a presentfield.storeFields({}, fields),storeExt({ card: 2 }, map),_commitFields(quill, {}, fields). The address is first and never shape-overloaded, becausecardis a legal field name (an optional-addressstoreFields({ card: 2 })would be ambiguous with "set fieldcard"). The main card is{};MAIN_CARD_ADDR(exported from@quillmark/wasm/runtime) is a frozen,CardAddr-typed alias for it, so the common case reads as intent —storeFields(MAIN_CARD_ADDR, fields)— while{}andundefinedstay equally valid. An address rejects unknown keys: a stray key — a typo, or the fields object handed where the address belongs (storeFields(fields, {})) — throws instead of silently reading as the empty main-card address and writing an empty batch.
New reach the twins never had: getExt(addr?) and getExtNamespace(addr, ns)
(fine-grained, non-destructive $ext reads), isFill(addr), and a card-capable
storeFill.
Removed (folded into the addressed verbs): getCardField, getCardMarkdown,
setCardField, setCardFields, setCardExt, removeCardExt,
setCardExtNamespace, removeCardExtNamespace, removeCardField,
commitCardField, commitCardFields. install / revise / applyChange widen
to accept the bare-string shorthand.
Python does not take the address — it stays name-keyed, its store_card_*
twins renamed from set_card_*. Core keeps borrow navigation (main_mut() /
card_mut(i)); the Addr lives only in the bindings.
A typed, anchor-preserving field revise (#957, #966)¶
Two writes took markdown into a richtext field with opposite identity semantics:
writer.set_body(md) (and revise) rebased surviving anchors, while
writer.set("subject", md) → commit_field cold-imported and destroyed them.
No write was both typed and anchor-preserving. New:
- Core
Card::revise_field_checked(name, md, schema) -> Delta— the primitive: diff the markdown against the field's current content so surviving anchors rebase (asrevise_field), then enforce the fieldschemaon the diffed result through the same typed-conform pathcommit_fieldruns, so arichtext(inline)field rejects a multi-block result withFieldRichtextNotInline(the error surface unchanged) while anchors survive. - Core
TypedWriter::revise_field(name, md) -> DeltaandCardWriter::revise_field(..)— the schema-bound wrapper: resolves the field's schema from the bound quill (an undeclared name isUnknownField) and calls the primitive. - WASM
writer.reviseField(name, md)/writer.card(i).reviseField(name, md)— the schema-bound verb on the writer, where the schema is.
The verb needs a schema, so it lives on the writer; Document stays quill-free.
revise / revise_field stay schema-blind; commit_field / install_field are
unchanged.
The writer is the one schema-bound door (#966)¶
Document's quill-taking methods become the hidden ABI under the writer, not
public verbs — the visible Document class then carries zero quill-taking
methods.
- WASM
commitField/commitFields/addCard→_commitField/_commitFields/_addCard, dropped from the.d.ts. The runtimewriter.set/setAll/addCarddelegate to them; the writer surface is unchanged. - WASM
doc.reviseChecked(quill, addr, md)is removed; usewriter.reviseField(name, md)(above). It had no runtime consumer. EditError::BodyImport→EditError::Import, messagebody import failed:→markdown import failed:. The variant also fires on field-path imports (revise_field), where "body" was a misnomer; bindings surface the prefix as[EditError::Import].
WASM: pushCard folds into insertCard(card, at?); replaceBody is deleted (#961)¶
One insertion verb per lane. pushCard is gone and insertCard now takes the
card first and an optional position second — absent at appends, a
number inserts at that index (0..=cardCount, out of range throws
IndexOutOfRange).
The deprecated replaceBody alias is removed. Use the content-lane revise or
the writer:
removeCard / moveCard / makeCard are unchanged. Every Card a document
returns is still a valid CardInput for insertCard.
Positioned insert, removeCard, and a cursor kind — additive (#961)¶
No action; these only add reach.
writer.addCard(kind, fields?, body?, at?)(WASM) andTypedWriter::add_card(kind, fields, body, at)(core) take a position, so a positioned typed insert is one atomic call instead ofaddCard+moveCard.TypedWriter::remove_card(i)now exists in core, mirroring the JSwriter.removeCard(i)sugar.- The JS
CardWritergains akindgetter (reads throughdoc.card(i)), mirroring coreCardWriter::kind().
core: Payload::insert / insert_fill validate and return Result (#958)¶
The direct Payload map API — reachable through the public
Card::payload_mut() — validated nothing, so payload_mut().insert(bad, v)
could seat an invalid field in a document that "cannot be invalid." Both now
enforce the field-name / value-depth invariant at the boundary and return
Result<Option<QuillValue>, FieldViolation>.
// 0.94
card.payload_mut().insert("qty".to_string(), v); // -> Option<QuillValue>
// 0.95
card.payload_mut().insert("qty".to_string(), v)?; // -> Result<_, FieldViolation>
Only direct Payload callers are affected — the Card / writer mutators
(set_field, set_fields, commit_field, set / set_all) are unchanged in
signature and behavior; they already validated. A caller that has already
validated the exact stored value (and wants to skip the re-check) uses the new
pub(crate) insert_unchecked / insert_fill_unchecked.
core: parse warnings live only on ParseOutput (#959)¶
Warnings were bookkept in three places. They are now owned solely by
ParseOutput; Document no longer carries them.
Document::warnings()— removed. ReadParseOutput::warningsfromDocument::from_markdown_with_warnings; the binding wrappers (doc.warningsin WASM / Python) are unchanged, as they always kept their own copy.Document::from_main_and_cards(main, cards, warnings)→from_main_and_cards(main, cards)— thewarningsparameter is dropped.Document'sPartialEqis now a plain derive (it only ever excludedwarnings); equality is unchanged (structural:main+cards).
from_markdown and from_markdown_with_warnings are collapsed into a single
Document::parse in the same release — see the next section (#964).
core: one parse entry — Document::parse returns Parsed (#964)¶
The two parse functions become one. Document::from_markdown (discarded the
warnings) and Document::from_markdown_with_warnings (returned them) are both
removed; the single entry is
and ParseOutput is renamed to Parsed (its fields are unchanged:
{ document, warnings }).
from_markdown_with_warnings(md)→parse(md)— the returned value has the same.document/.warningsfields.from_markdown(md)where you wanted only the document →parse(md)?.document(or.unwrap().document/.expect(..).document).parsecarries#[doc(alias = "from_markdown")], so rustdoc search for the old name still lands on it.
parse returns a Parsed, not a Document — the trade is deliberate: one
door, warnings surfaced by default instead of hidden behind a longer method a
caller might never discover, and a compile error (not silent warning loss)
when a caller forgets .document.
Bindings are unaffected in surface: the WASM Document.fromMarkdown and
Python Document.from_markdown keep their names and their doc.warnings
getter — the wrapper is a session object fusing Parsed + Document (see the
prose/canon/BINDINGS.md parity table). Only the two call sites into core were
renamed.
Additive reads — no action (#956)¶
New reads that avoid whole-document serialization; nothing to migrate.
doc.card(i)(WASM) /Document::card(i)(core) — one whole card by index without materializing thecardsarray (throws /Noneout of range).doc.cardIndexById(id)(WASM) /Document::find_card(id)(core) — resolve a$idto its index without a hand-rolled scan ($idis non-unique; first match wins).doc.seedOverlay(kind)(WASM) — the main card's$seed[kind]overlay, fed straight intoquill.seedCard(kind, overlay), without serializing the whole main card.
The markdown projection drops its trailing newline (#965)¶
to_markdown projects a content value, not a file, so it no longer appends a
final \n. Every projection inherits this: core field_markdown / body_markdown,
WASM getMarkdown / exportMarkdown, Python export_markdown / get_markdown.
A field read-back stops growing a newline — writer.set("subject", "Hello") now
reads back as "Hello", not "Hello\n", so a controlled <input> bound to
getMarkdown no longer fights the caret and value === getMarkdown(addr) holds.
Action: drop any .trimEnd() / .rstrip("\n") you added to work around the
old newline; delete the trailing \n from assertions that pinned it.
The content fixed point is unchanged: import is newline-insensitive, so a re-parse of either form yields the same content.
getMarkdown throws on a present non-richtext field (#968)¶
The markdown projection stops conflating absent with present-but-not-richtext.
Before, getMarkdown / get_markdown returned the absent shape
(undefined / "") for both, so a scalar / array / object a storeField wrote
— exactly the values an opaque store or a non-richtext schema field holds —
rendered as blank UI indistinguishable from a missing field. Now:
- absent →
undefined(WASM) /""(Python), unchanged; - present, decodes as richtext (a content, or a markdown string) → the markdown, unchanged;
- present, does not decode → throws
FieldRichtextDecode, naming the field.
The rule in one line: absence returns; mismatch raises. It refines #955's
totality (absence stays routine and total) rather than breaking it — a type
mismatch was never routine, it was a consumer bug the flattening turned into
silently blank UI. Read the raw value with get(addr) / get(name) when you
want the stored scalar rather than a projection.
Core mirrors the shape: Card::field_markdown returns
Option<Result<String, RichtextDecodeError>> (the projection twin of
field_richtext) instead of Option<String> — None absent, Some(Ok)
markdown, Some(Err) mismatch. A core caller that unwrapped the old Option
adds a second unwrap or handles the Err.
This absence-returns / mismatch-raises rule now lives on the schema-plane
view.get (see #978 below): the binding getMarkdown's field projection — where
this fix first landed — retired the same release, so the mismatch raise surfaces
through quill.view(doc).get(name):
// present field that isn't richtext (a scalar store wrote)
doc.storeField({ field: 'qty' }, 3)
doc.get('qty') // 3 — the raw value still reads (transport)
quill.view(doc).get('qty') // throws FieldRichtextDecode
The core field_markdown throw and the Python absent-"" case are unchanged;
only the binding entry point for a field's markdown moved from getMarkdown to
view.get.
A schema-bound read view; getMarkdown's field half retires (#978)¶
Breaking. The markdown projection was a schema-shaped question ("this
field's richtext, as markdown") answered by a schema-free Document — the
residue of which was #968's conflation. The projecting read now has a
schema-bound home, the read twin of quill.writer(doc), and its old home on
Document retires:
quill.view(doc)
.get(addr) // richtext → markdown
// plaintext → literal text (marks verbatim, never interpreted)
// every other type → canonical value
// absent → undefined / None
// unknown field name → throws UnknownField
// content field holding an undecodable value → throws FieldRichtextDecode
view.get interprets by the field's declared type, so it carries the
authority getMarkdown lacks: an unknown field name throws instead of reading
back the absent shape (the schema knows the field set). Core adds
Quill::view(&doc) → TypedReader, whose get returns a ReadValue
(Markdown(String) for richtext, Plaintext(String) for plaintext,
Value(QuillValue) otherwise); view.card(i) is the card cursor.
getMarkdown is now body-only. Its field projection is retired —
getMarkdown / get_markdown / get_card_markdown read a body's markdown and
nothing else. On WASM getMarkdown takes an optional CardAddr and a present
field throws ("body-only"); on Python get_markdown() / get_card_markdown(index)
drop the name parameter. The body read stays on Document (a body's type is a
format fact, not a schema fact); view.getBody() mirrors it. Migrating a
field-projection call site is a quill-in-hand rewrite:
doc.getMarkdown('subject') // was (WASM: field addr; Python: get_markdown("subject"))
quill.view(doc).get('subject') // now — throws on an unknown name
doc.getMarkdown({ card: 2, field: 'body' }) // was
quill.view(doc).card(2).get('body') // now
plaintext fields, new to view.get, project through their literal codec
(to_plaintext): a *hi* reads back as four characters, not emphasis — the same
codec the write path uses, in reverse.
datetime splits into strict date and datetime types (#991)¶
Breaking. The one datetime type was a lexical union of three kinds —
calendar date, wall-clock datetime, offset instant — that the render pipeline
silently truncated to a date (#717, #799). It splits into two types, each with a
strict grammar and no truncation in either direction:
| Type | Accepts | Rejects |
|---|---|---|
date |
YYYY-MM-DD |
any time component (not truncated) |
datetime |
YYYY-MM-DDThh:mm[:ss] |
offsets (Z, ±HH:MM), the space separator, fractional seconds, a bare date |
Seconds are the one concession — 2026-06-01T14:30 is how a person writes 2:30
PM; the Typst constructor zero-fills. Offsets are rejected, never dropped: the
engine keeps wall-clock semantics end to end and does no zone math, so an
offset-bearing value errors at the seam (convert to local time and remove the
offset) rather than being reinterpreted as document-local — there is no special
case for Z / +00:00. Storage stays the verbatim authored string.
Most datetime fields hold a bare date — those migrate to type: date,
which is a rename, not a data change: the stored string is byte-identical and a
date lowers to the same three-component datetime(year:, month:, day:) the old
type emitted. A datetime field now carries its wall-clock time-of-day through
to the six-component constructor instead of dropping it.
# 0.94
issued:
type: datetime # held "2026-06-01" — a date wearing a datetime type
# 0.95
issued:
type: date # same value, honest type, identical render
Action:
- Rename every
type: datetimefield that holds a calendar date totype: date(the common case). No document changes. - A field that genuinely carries time-of-day stays
type: datetime, but any value with an offset, a space separator, fractional seconds, or a bare date now fails coercion — convert offsets to local wall-clock time and drop them, switch the space separator toT, and give a bare-date value atype: datefield. - Projections shift: the transform schema marks
dateasformat: "date"(and keepsformat: "date-time"fordatetime), and the blueprint annotation readsdate<YYYY-MM-DD>/datetime<YYYY-MM-DDThh:mm[:ss]>. A consumer keying on the oldformat: "date-time"stamp for every date field updates to also accept"date".
There is no deprecation alias: type: datetime rejecting a bare date is the
decided end-state, and the offset/space/fraction forms break cleanly.
Dates lower to a click-to-edit value-object (#990)¶
Breaking for plates that call .display() on a date field. A present
type: date / type: datetime field no longer arrives as a bare Typst
datetime. It arrives as a two-key value-object whose display is a
closure returning region-bearing content:
// generated, per present date cell
(value: datetime(year: 2026, month: 1, day: 2),
display: (..args) => text(datetime(year: 2026, month: 1, day: 2).display(..args)))
This makes a date the first click-to-edit target in the system: because the
glyphs are born at a generated text(..) node inside the helper, they carry a
region keyed on the field's schema path — even when a vendored package places
the date, and even when a card's date rides the shared loop variable that span
tracking otherwise cannot chase. A blank date stays none, so != none guards
are untouched.
The cost is that data.<field> is a dict, not a datetime. Three call shapes
migrate:
| Was (0.94) | Now (0.95) | Why |
|---|---|---|
data.issued.display("…") |
(data.issued.display)("…") |
Typst rejects method sugar on a dict key; the stored closure needs the paren form |
data.issued in datetime math / comparison / a datetime-consuming package |
data.issued.value |
.value is the native datetime |
str(data.issued.display("…")) or other string use of the result |
data.issued.value.display("…") |
the closure returns content; .value.display is the native str |
data.issued.year() (and .month(), .weekday(), …) |
data.issued.value.year() |
components live on the native datetime |
One rule covers all of it: native anything → .value; region render →
(…display)(…).
Vendored packages count. A package's own .display() on a date it was
handed breaks the same way — the flagship tonguetoquill-* memo/letter quills
format the date inside display-date, which now dispatches on type(date):
if type(date) == str { date }
else if type(date) == datetime { date.display(pattern) } // the today() fallback stays native
else { (date.display)(pattern) } // the value-object dict
The datetime branch is load-bearing: these quills substitute
datetime.today() for a blank date, so the same call site receives both a
native datetime and a value-object — and grabbing .display off a native
datetime (the dict's paren form) is a compile error, so the two shapes cannot
share one branch.
Action: parenthesize every .display( call on a date field; route every
native datetime operation (compare, subtract, .year(), datetime-consuming
packages) through .value; audit vendored packages for internal .display()
on a passed-in date and give them a type(date) dispatch if they also see a
native datetime.
Python commits to Tier 1 + storage + render (#970)¶
Rather than mirror the reshaped store* / Addr / content-lane surface, the
Python binding cuts to its lanes. Field I/O flows through quill.writer(doc) /
quill.view(doc) exclusively; Document is quill-free data and structure. The
opaque store and the anchor-preserving content lane are WASM-only by scope,
not by lag — their audience (storage/migration tooling holding no quill, live
editors preserving anchor identity) is not a Python audience. This resolves the
half-mirror drift #970 priced: no lane that could drift remains in Python.
Removed. The opaque field store, the content lane, the quill-free field reads, and the document-free codec:
| 0.94 (Python) | 0.95 replacement |
|---|---|
doc.store_field(name, v) / doc.store_fields(m) / doc.store_fill(name, v) |
quill.writer(doc).set(name, v) / .set_all(m); a fill lands through a typed set (which clears the !must_fill marker) |
doc.store_card_field(i, name, v) / doc.store_card_fields(i, m) |
quill.writer(doc).card(i).set(name, v) / .set_all(m) |
doc.get(name) / doc.get_card_field(i, name) |
quill.view(doc).get(name) / quill.view(doc).card(i).get(name) |
doc.get_markdown() / doc.get_card_markdown(i) |
quill.view(doc).get_body() / quill.view(doc).card(i).get_body() |
doc.install(rt, …) / doc.revise(md, …) / doc.apply_change(b, …) |
none (WASM-only); the typed writer.revise_field is the anchor-preserving field write |
import_markdown / export_markdown / rebase / map_pos (module fns) |
none (WASM-only); read body markdown via quill.view(doc).get_body(), and doc.body is the content dict ({text, lines, marks, islands}) |
doc.replace_body(md) / doc.update_card_body(i, md) (deprecated aliases) |
quill.writer(doc).set_body(md) / .card(i).set_body(md) |
A field write without a loadable quill operates on the storage DTO directly
(to_json / from_json), or through core / WASM / the CLI, where the store lane
remains.
Changed — the composable-card twins fold onto one card= selector, and the
insertion verbs fold to one:
0.94 0.95
doc.store_card_ext(i, v) doc.store_ext(v, card=i)
doc.remove_card_ext(i) doc.remove_ext(card=i)
doc.store_card_ext_namespace(i,n,v) doc.store_ext_namespace(n, v, card=i)
doc.remove_card_ext_namespace(i,n) doc.remove_ext_namespace(n, card=i)
doc.remove_card_field(i, name) doc.remove_field(name, card=i)
doc.push_card(card) doc.insert_card(card) # absent at appends
doc.insert_card(i, card) doc.insert_card(card, at=i)
store_ext / remove_ext / store_ext_namespace / remove_ext_namespace and
remove_field gain a trailing card=None (main) selector; the whole-map ext
verbs and $seed namespace verbs are otherwise unchanged.
Added — the 0.95 additive surface, now mirrored:
quill.writer(doc).revise_field(name, md)/writer.card(i).revise_field(..)— the typed, anchor-preserving richtext field write (#957, #966). Python discards theDeltait returns in core / WASM (the position-mapping receipt is an editor concern, and that lane is WASM-only).quill.writer(doc).add_card(kind, fields?, body?, at=None)— the positioned typed insert (#961); absentatappends.writer.card(i).kind— the cursorkindgetter (#961).- The typed handle classes
Writer/CardWriter/View/CardVieware exported from thequillmarkpackage (forisinstance/ typing).
Python's Payload is not exposed, so #958 does not affect it; its doc.warnings
getter is unchanged by #959.