Skip to content

v0.95.0

Choose a tag to compare

@quillmark-tagger quillmark-tagger released this 19 Jul 13:08
8894866
  • release: re-cut to finish binding dists after the first publish run cancelled mid-flight
  • breaking typst: a present date / datetime field lowers to a click-to-edit value-object — (value: datetime(..), display: (..args) => text(value.display(..args))) — instead of a bare Typst datetime, so the rendered glyphs are born at a generated text(..) node carrying a region keyed on the field's schema path: a date placed by a vendored package, or a card's date riding a shared loop variable, is click-to-edit. A blank date stays none, so != none guards hold. Plates migrate two shapes: (data.f.display)("…") (paren form — the stored display is a closure on a dict, not a method) and data.f.value for anything native (comparison, .year()-family components, datetime-consuming packages). The tonguetoquill flagship quills' display-date dispatches on type(date), since their datetime.today() blank-date fallback stays a native datetime (#990)
  • breaking core,typst,pdfform: split the datetime field type into strict date and datetime (#717/#799 resolved) — date accepts a bare YYYY-MM-DD and rejects any time component; datetime accepts offset-less wall-clock YYYY-MM-DDThh:mm[:ss] (seconds zero-filled) and rejects timezone offsets, the space separator, fractional seconds, and bare dates. Offsets are rejected, never dropped (the engine does no zone math); storage stays verbatim; no truncation in either direction. A date lowers to the three-component Typst datetime(year:, month:, day:) (unchanged emission) and a datetime to the six-component constructor, carrying the wall-clock time. The transform schema marks date as format: "date" (keeping format: "date-time" for datetime), and the blueprint reads date<YYYY-MM-DD> / datetime<YYYY-MM-DDThh:mm[:ss]>. Most deployed datetime fields hold a bare date and migrate to type: date with byte-identical data; the fixtures (usaf_memo, cmu_letter) do so. No deprecation alias — datetime rejecting a bare date is the decided end-state (#991)
  • breaking python: the binding commits to the typed lanes — field I/O flows through quill.writer(doc) / quill.view(doc) exclusively and Document is quill-free data and structure. Removed (WASM-only by scope, not lag — their audience is not a Python audience): the opaque field store (store_field / store_fields / store_fill and card twins), the content lane (install / revise / apply_change), the quill-free field reads (get / get_card_field), and the module codec fns (import_markdown / export_markdown / rebase / map_pos). The composable $ext / field-remove card twins fold onto a trailing card=None selector and push_card folds into insert_card(card, at=None). Mirrored additions: writer.revise_field (the Delta receipt is not surfaced), writer.add_card(kind, fields=None, body=None, at=None), writer.card(i).kind, and the Writer / CardWriter / View / CardView handle classes exported from the package (#970)
  • breaking core,wasm,python: complete the RichTextContent residual sweep (#976 folds into #982) — retire the last informal-corpus / model-RichText identifiers the mechanical rename missed: CorpusHitContentHit, EditError::CorpusApplyEditError::ContentApply, RichtextDecodeError::NotCorpusNotContent (the codec-specific RichtextDecodeError type itself is kept), storage DTO CanonicalRichTextCanonicalContent (serde is unchanged — no wire migration), and the model-generic Typst emitter emit_richtext / emit_richtext_inlineemit_content / emit_content_inline (they lower any Content, richtext or plaintext). Also fixes the ParseError display strings richtext json …content json …, the doubled-word find-replace debris (content content modelcontent model), and stale prose/comments across canon and rustdoc. Schema/codec names are untouched — richtext / plaintext tokens, FieldType::{RichText,PlainText}, field_richtext / FieldRichtext* / apply_field_richtext_change, richtext(inline) — they name codecs, not the model. "corpus" is purged from the tree entirely, including the ordinary-English test-set names that meant a collection of fixtures (fixture_corpusfixtures, synthetic_corpussynthetic_inputs) (#982)
  • 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)
  • 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)
  • 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)
  • 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". .qmd files still end in one newline (owned by Document::to_markdown, the file writer) and the content fixed point is unchanged (import is newline-insensitive) (#965)
  • breaking all: rename the content genus off its codec's name — crate quillmark-richtextquillmark-content, type RichTextContent (and RichTextLine/RichTextContainer/RichTextMark/RichTextIslandContentLine/…), const RICHTEXT_MEDIA_TYPECONTENT_MEDIA_TYPE and its wire string application/quillmark-richtext+jsonapplication/quillmark-content+json, #[serde(skip)] companion caches FieldSchema::{default,example}_corpus_content, SegmentMap.corpus: Range<usize>.content, Typst-emitter EmittedContentEmission (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)
  • 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)
  • 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)
  • breaking wasm: the quill-taking Document methods become the hidden ABI under the writer — commitField / commitFields / addCard_commitField / _commitFields / _addCard, dropped from the .d.ts; remove doc.reviseChecked (no runtime consumer — use writer.reviseField). The visible Document class then carries zero quill-taking methods (#966)
  • breaking core: rename EditError::BodyImportEditError::Import (message body import failed:markdown import failed:) — the variant also fires on field-path imports (revise_field), where "body" misnamed it (#966)
  • breaking wasm: fold pushCard into insertCard(card, at?) — one insertion verb per lane, absent at appends; insertCard's parameters reorder to (card, at?). Delete the deprecated replaceBody alias (use revise({}, md) or writer.setBody) (#961)
  • feat(core,wasm): positioned card insert — TypedWriter::add_card / writer.addCard and the addCard ABI take an at position, so a positioned typed insert is one atomic call instead of addCard + moveCard; add TypedWriter::remove_card (mirrors JS writer.removeCard) and a JS CardWriter.kind getter (mirrors core CardWriter::kind()) (#961)
  • breaking core: Payload::insert / insert_fill now validate the field-name and value-depth invariant at the boundary and return Result<_, FieldViolation>, closing the payload_mut().insert(...) hole that let a direct caller build an invalid document; pre-validated internal callers use the new pub(crate) insert_unchecked / insert_fill_unchecked (#958)
  • feat(wasm,core): single-card reads — doc.card(i) (throws out of range), doc.cardIndexById(id) (first match; $id is non-unique), and doc.seedOverlay(kind), backed by core Document::card(i) / find_card(id). Reading one card, resolving a $id, or fetching a $seed overlay no longer serializes the whole cards array or main card (#956)
  • breaking core: parse warnings live only on ParseOutput — the redundant Document::warnings field + warnings() getter are dropped and Document::from_main_and_cards no longer takes a warnings param (Document PartialEq is now a plain derive) (#959)
  • breaking core: collapse the two parse functions into one entry — Document::from_markdown and Document::from_markdown_with_warnings are removed in favor of Document::parse(md) -> Result<Parsed, ParseError>, and ParseOutput is renamed Parsed. A document-only caller writes parse(md)?.document. Bindings are unaffected: WASM Document.fromMarkdown / Python Document.from_markdown keep their names and their doc.warnings getter (#964)
  • feat(wasm,python): keyed card reads getCardField(index, name) / getCardMarkdown(index, name?) (py get_card_field / get_card_markdown) — the card-indexed twins of get / getMarkdown, mirroring the commitCardField / setCardField write verbs so card reads no longer require a payloadItems walk (#953)
  • feat(content,wasm,python): LineOp::SetContinues { line, continues } — hard breaks lower op-wise. Split, join, and a text-delta \n all mint continues: false lines, so a within-block hard break (a paragraph hard break, a code fence's interior line) had no op and fell back to a whole-install, losing that edit's identity anchors. Threaded through the wire codec into WASM applyChange (TS union updated) and Python; continues: true on line 0 is rejected with ApplyError::FirstLineContinues before the write, leaving the content untouched (#949)
  • feat(wasm): the runtime root re-exports the edit vocabulary its own signatures reference — Content / ContentLine / ContentContainer / ContentMark / ContentIsland, Addr / Delta / Assoc / LineOp / MarkOp / ChangeBundle, CardInput / PathStep — as type-only exports (single entry point preserved; no /core subpath), with a presence guard so a dropped re-export fails npm run typecheck (#948)