pub struct Card { /* private fields */ }Expand description
A single card-yaml block (root or composable). body is the content
(Content) form of the prose after the closing fence — the empty content
when none follows; check card.body().is_blank(). Markdown is a projection:
Card::body_markdown re-emits it.
Implementations§
Source§impl Card
impl Card
Sourcepub fn new(kind: impl Into<String>) -> Result<Self, EditError>
pub fn new(kind: impl Into<String>) -> Result<Self, EditError>
Create a composable card with the given kind, no fields, and an empty body.
Sourcepub fn store_field(
&mut self,
name: &str,
value: impl Into<QuillValue>,
) -> Result<(), EditError>
pub fn store_field( &mut self, name: &str, value: impl Into<QuillValue>, ) -> Result<(), EditError>
Store a payload field verbatim, clearing any !must_fill marker on that
key — the opaque store (store = verbatim, coercion deferred to render;
contrast the typed TypedWriter::set). Scalars
convert in place (store_field("qty", 3)); see the From impls on
QuillValue.
Returns EditError::InvalidFieldName when name does not match
[A-Za-z_][A-Za-z0-9_]*.
Sourcepub fn store_fill(
&mut self,
name: &str,
value: impl Into<QuillValue>,
) -> Result<(), EditError>
pub fn store_fill( &mut self, name: &str, value: impl Into<QuillValue>, ) -> Result<(), EditError>
Store a payload field verbatim and mark it as a !must_fill placeholder.
Null emits as key: !must_fill; scalars/sequences as key: !must_fill <value>.
The opaque store’s fill variant (quill-free, verbatim); same validation as
Card::store_field.
Sourcepub fn store_fields<K, V, I>(
&mut self,
fields: I,
) -> Result<(), Vec<(String, EditError)>>
pub fn store_fields<K, V, I>( &mut self, fields: I, ) -> Result<(), Vec<(String, EditError)>>
Store several payload fields verbatim and atomically, clearing any
!must_fill marker on each key — the opaque store’s batch (contrast the
typed TypedWriter::set_all). The whole
batch is validated first — on any violation nothing is applied and every
offending field is reported as a (name, error) pair, so a caller feeding
externally-sourced names (database columns, form keys) sees all violations
in one pass instead of fix-rerun-repeat. Per-field rules are those of
Card::store_field; insertion order follows the iterator, and a
repeated name behaves like repeated store_field calls (last value
wins, first position kept).
Sourcepub fn remove_field(
&mut self,
name: &str,
) -> Result<Option<QuillValue>, EditError>
pub fn remove_field( &mut self, name: &str, ) -> Result<Option<QuillValue>, EditError>
Remove a payload field; returns Ok(None) if the name is absent.
Removal has no lane — the one verb serves every write path. Same
validation as Card::store_field.
Sourcepub fn store_ext(&mut self, value: Map<String, Value>) -> Result<(), EditError>
pub fn store_ext(&mut self, value: Map<String, Value>) -> Result<(), EditError>
Replace the card’s opaque $ext map wholesale, inserting it at the
canonical position (after $quill/$kind/$id, before user fields)
when none existed. Passing an empty map records an explicit $ext: {}.
$ext carries out-of-band consumer state (editor renames, agent
annotations, …) and is stripped from Document::to_plate_json, so a
write here can never affect a render. Any nested comments attached to a
replaced $ext are dropped.
Returns EditError::ValueTooDeep when the map nests past the §8
depth limit — $ext never reaches the plate JSON, but it does flow
through the recursive emit and DTO paths, so it carries the same
depth bound as user fields.
Quill-free and never coerced — an opaque store_* verb by the vocabulary
rule, not a typed set.
Sourcepub fn remove_ext(&mut self) -> Option<Map<String, Value>>
pub fn remove_ext(&mut self) -> Option<Map<String, Value>>
Remove the card’s $ext map entirely, returning the previous map if
present. This is a blunt escape hatch — it discards every namespace
($ext.editor, $ext.agent, …) at once. To clear consumer
state, prefer Card::remove_ext_namespace, which drops only your
own slot and leaves sibling consumers’ state intact.
Sourcepub fn store_ext_namespace(
&mut self,
namespace: impl Into<String>,
value: Value,
) -> Result<(), EditError>
pub fn store_ext_namespace( &mut self, namespace: impl Into<String>, value: Value, ) -> Result<(), EditError>
Merge value into the card’s $ext map under namespace, creating
the map when absent and replacing any existing value at that key.
This is the recommended way to write $ext: it preserves sibling
namespaces, so independent consumers keying on their own slot
($ext.editor, $ext.agent, …) don’t clobber each other.
Returns EditError::ValueTooDeep when the merged map nests past
the §8 depth limit (see Card::store_ext); the card’s $ext is
unchanged on error. Quill-free and never coerced — an opaque store_*
verb.
Sourcepub fn remove_ext_namespace(&mut self, namespace: &str) -> Option<Value>
pub fn remove_ext_namespace(&mut self, namespace: &str) -> Option<Value>
Remove namespace from the card’s $ext map, returning the value
that was stored there (or None when the map or the key was absent).
This is the recommended way to clear $ext state: it is the
namespace-scoped inverse of Card::store_ext_namespace and preserves
sibling namespaces, where Card::remove_ext would wipe them all.
When removing the last namespace empties the map, the $ext entry is
dropped entirely (not left as $ext: {}), so
store_ext_namespace(ns, v) followed by remove_ext_namespace(ns)
restores a card that had no $ext to its original state.
Sourcepub fn seed(&self) -> Option<&Map<String, Value>>
pub fn seed(&self) -> Option<&Map<String, Value>>
The raw $seed map (keyed by card-kind), or None. For a parsed,
per-kind overlay, index this map by kind and pass the entry to
crate::SeedOverlay::from_json. Only the main card carries $seed.
Sourcepub fn store_seed_namespace(
&mut self,
card_kind: impl Into<String>,
value: Value,
) -> Result<(), EditError>
pub fn store_seed_namespace( &mut self, card_kind: impl Into<String>, value: Value, ) -> Result<(), EditError>
Merge a card-kind’s seed overlay value into the card’s $seed map
under card_kind, creating the map when absent and replacing any
existing overlay for that kind. Sibling kinds are preserved — this is
the per-kind-safe writer, the seed analogue of
Card::store_ext_namespace. card_kind must be a valid, non-reserved
composable kind (EditError::InvalidKindName / EditError::ReservedKind
otherwise) — $seed is keyed by composable card-kind, unlike the
free-form namespaces of $ext. Returns EditError::ValueTooDeep when
the merged map nests past the §8 depth limit; the card is unchanged on
error. Quill-free and never coerced — an opaque store_* verb.
Sourcepub fn remove_seed_namespace(&mut self, card_kind: &str) -> Option<Value>
pub fn remove_seed_namespace(&mut self, card_kind: &str) -> Option<Value>
Remove card_kind from the card’s $seed map, returning the overlay
stored there (or None). When removing the last kind empties the map,
the $seed entry is dropped entirely (not left as $seed: {}).
The seed analogue of Card::remove_ext_namespace.
Sourcepub fn install_body(&mut self, content: Content)
pub fn install_body(&mut self, content: Content)
Install the body content directly from a pre-built Content — value
semantics, the native richtext writer. A content is valid by
construction, so this is infallible: no markdown import, no diff, no
schema check; the identity anchors of the previous body are gone
(install-this-exact-value, so a to_markdown → install round-trip cannot
resurrect them). Use it when the caller already holds a content (a decoded
canonical-JSON body, another field’s value, an editor’s serialized state).
For “here’s new authored markdown,” use revise_body,
which rebases surviving anchors; the cold-import path is spelled at the
call site as install_body(import_body(md)?).
Sourcepub fn install_field(
&mut self,
name: &str,
content: Content,
) -> Result<(), EditError>
pub fn install_field( &mut self, name: &str, content: Content, ) -> Result<(), EditError>
Install a richtext field’s content directly from a pre-built Content
— the field-level twin of install_body. Value
semantics: stores the canonical content JSON verbatim (identity marks and
content-only marks such as underline intact), no diff, no schema check
(schema-blind, like apply_field_richtext_change
— commit_field is the typed door). Returns
EditError::InvalidFieldName for a malformed name.
Sourcepub fn commit_field(
&mut self,
name: &str,
value: impl Into<QuillValue>,
schema: &FieldSchema,
) -> Result<(), EditError>
pub fn commit_field( &mut self, name: &str, value: impl Into<QuillValue>, schema: &FieldSchema, ) -> Result<(), EditError>
Write-time commit: validate and normalize value per the field’s schema
type and store the canonical form. The typed sibling of the opaque
store_field — the one write verb for every field
type (richtext today, any future content model tomorrow), dispatching on
the FieldSchema rather than growing a per-type method.
The two write disciplines: store_field stores the
value opaquely and defers coercion to render (keystroke-level state,
data-in-flight); commit_field canonicalizes now and fails now (an
editor blur/save, an agent write). Neither is forced on the other.
Behavior by type:
- richtext — imports a markdown string / adopts a content object and
stores canonical content JSON, so identity marks (anchors, island ids)
live on the stored value from the write; a
richtext(inline)schema rejects a multi-block value withEditError::FieldRichtextNotInline. - scalars (
string/integer/number/boolean/datetime) — stores the coerced canonical ("3"→3), applying only value-parsing normalizations; a cross-type value that the render floor would coerce (e.g.1→true) or a shape mismatch fails here instead. - array / object — coerces each element/property against the element/property schema.
- null — passes through unchanged (the null ≡ absent rule); nothing
is coerced (a richtext
nullreads back as the empty content viafield_richtext).
The caller supplies the schema because a Document holds only a
$quill reference, not the resolved schema; an editor holds it (see
crate::TypedWriter, which resolves the schema per field and calls
this).
Returns EditError::InvalidFieldName for a malformed name,
EditError::FieldRichtextDecode / EditError::FieldRichtextNotInline
for a richtext field, EditError::FieldConform for any other type
mismatch, and EditError::ValueTooDeep when the stored value nests
past the §8 depth limit.
Sourcepub fn revise_body(
&mut self,
body: impl Into<String>,
) -> Result<Delta, EditError>
pub fn revise_body( &mut self, body: impl Into<String>, ) -> Result<Delta, EditError>
Revise the body from an authored markdown string — edit semantics,
the whole-document (stale-text / LLM / MCP) writer, and the receipt-
returning default write path. Imports the markdown, diffs it against the
current body, and rebases surviving identity anchors onto the new text
(cold import + diff_import), then returns the text Delta from the
old body to the new one — the change an editor bridge maps its own
positions through across a whole-document replace (Delta::map_pos).
Surviving identity anchors rebase; formatting marks are re-derived by the
fresh import. A pathologically over-nested input (> MAX_NESTING_DEPTH)
returns EditError::Import rather than silently degrading to the
empty content. Discard the receipt with let _ = card.revise_body(md)?;
when caret stability is not needed.
Sourcepub fn revise_field(
&mut self,
name: &str,
body: impl Into<String>,
) -> Result<Delta, EditError>
pub fn revise_field( &mut self, name: &str, body: impl Into<String>, ) -> Result<Delta, EditError>
Revise a richtext field from an authored markdown string — the
field-level twin of revise_body, and the
field-level diff_import the write surface previously lacked (the only
field-content writers were the cold commit_field
and the splice apply_field_richtext_change,
so an LLM rewriting a richtext field’s markdown had no anchor-preserving
path). Decodes the field’s current content as the diff base (an absent
field cold-imports from empty), rebases surviving anchors onto the new
text, re-stores the canonical content, and returns the text Delta.
Schema-blind by design — the content-writer stratum splices without the
quill (like apply_field_richtext_change);
commit_field is the typed door that enforces
richtext(inline), and a violation otherwise surfaces at validate/render.
Returns EditError::InvalidFieldName for a malformed name,
EditError::FieldRichtextDecode when the field is present but is not a
richtext content (a scalar a store_field wrote), and
EditError::Import on an over-nested markdown input.
Sourcepub fn revise_field_checked(
&mut self,
name: &str,
body: impl Into<String>,
schema: &FieldSchema,
) -> Result<Delta, EditError>
pub fn revise_field_checked( &mut self, name: &str, body: impl Into<String>, schema: &FieldSchema, ) -> Result<Delta, EditError>
Revise a richtext field from markdown with schema enforcement — the
typed and anchor-preserving field write that neither
revise_field nor commit_field
provides alone. revise_field rebases anchors but is
schema-blind; commit_field enforces the schema but
cold-imports (the previous value’s anchors are gone). This does both: diff
the markdown against the field’s current content so surviving anchors rebase
(as revise_field), then enforce schema on the
diffed result through the same typed-conform path
commit_field runs — so a richtext(inline) schema
rejects a multi-block result with EditError::FieldRichtextNotInline,
the error surface unchanged, while the anchors survive. Returns the text
Delta receipt.
The primitive that TypedWriter::revise_field
and CardWriter::revise_field wrap: they
resolve schema from the bound quill and call here. The schema runs on the
content the diff produced, so a non-richtext schema (nothing to preserve)
fails with the same EditError::FieldConform
commit_field would raise.
Errors: EditError::InvalidFieldName, EditError::FieldRichtextDecode
when the field is present but not a richtext content, EditError::Import
on an over-nested markdown input, and the conform errors of
commit_field on the diffed result. On any error the
field is unchanged.
Sourcepub fn apply_body_change(
&mut self,
text_delta: &Delta,
line_ops: &[LineOp],
mark_ops: &[MarkOp],
) -> Result<(), EditError>
pub fn apply_body_change( &mut self, text_delta: &Delta, line_ops: &[LineOp], mark_ops: &[MarkOp], ) -> Result<(), EditError>
Apply a committed field-change bundle to the body content — the native
form-editor writer. Order is text delta → line ops → mark ops, then one
terminal normalization (Content::apply_field_change); mark ranges are
in final-text coordinates. Returns
EditError::ContentApply when an op is out of bounds; the apply is
all-or-nothing (Content::apply_field_change), so the body is
unchanged on error — apply the bundle against the body the delta was
computed from.
Sourcepub fn apply_field_richtext_change(
&mut self,
name: &str,
text_delta: &Delta,
line_ops: &[LineOp],
mark_ops: &[MarkOp],
) -> Result<(), EditError>
pub fn apply_field_richtext_change( &mut self, name: &str, text_delta: &Delta, line_ops: &[LineOp], mark_ops: &[MarkOp], ) -> Result<(), EditError>
Splice a content field-change bundle into a richtext-valued field’s
stored content — the field-path twin of apply_body_change,
and what lets identity marks (anchors, island ids) persist on field
content across incremental edits. Decodes the field’s canonical content,
applies the text delta plus any line/mark ops in the same all-or-nothing
bundle, and re-stores the canonical result.
Returns EditError::FieldRichtextDecode when the field is absent or its
stored value is not a richtext content (the caller addresses a field it
knows is richtext, exactly as when writing it), and
EditError::ContentApply when the bundle applies out of bounds.
Source§impl Card
impl Card
Sourcepub fn from_parts(payload: Payload, body: Content) -> Self
pub fn from_parts(payload: Payload, body: Content) -> Self
Create a Card from its parts without validation. body is the content
form; to build from an authored markdown string, import it first via the
crate-internal import_body boundary. For user-facing construction of
composable cards use Card::new.
pub fn quill(&self) -> Option<&QuillReference>
pub fn kind(&self) -> Option<&str>
pub fn id(&self) -> Option<&str>
Sourcepub fn ext(&self) -> Option<&Map<String, Value>>
pub fn ext(&self) -> Option<&Map<String, Value>>
Opaque $ext map for out-of-band extension data (UI editor state,
agent annotations, …). Carried through Markdown and storage DTO
round-trips; never emitted into the plate JSON consumed by
backends.
pub fn payload(&self) -> &Payload
pub fn payload_mut(&mut self) -> &mut Payload
Sourcepub fn body(&self) -> &Content
pub fn body(&self) -> &Content
The card body as a Content content — the canonical content model.
For the markdown projection use Card::body_markdown.
Sourcepub fn body_markdown(&self) -> String
pub fn body_markdown(&self) -> String
The card body rendered back to its markdown projection. This is a
derived view (export ∘ body), not stored state; a Document round-trip
therefore canonicalizes the body (e.g. __b__ → **b**).
Sourcepub fn field_richtext(
&self,
name: &str,
) -> Option<Result<Content, RichtextDecodeError>>
pub fn field_richtext( &self, name: &str, ) -> Option<Result<Content, RichtextDecodeError>>
Read a richtext-valued user field back as a Content content — the
field-level twin of Card::body. Decodes the stored value through the
same object-or-markdown dispatch the writer
(commit_field) commits, so a field
stored as a canonical content reads back losslessly (identity marks
intact) and a still-authored markdown string imports.
None— the field is absent.Some(Ok(rt))— decoded content.Some(Err(_))— the field is present but neither a content object nor an importable markdown string (e.g. a bare number astore_fieldwrote).
A Document carries no schema, so this cannot itself tell a richtext
field from a plain string field; the caller names a field it knows is
richtext, exactly as it does when writing.
Sourcepub fn field_markdown(
&self,
name: &str,
) -> Option<Result<String, RichtextDecodeError>>
pub fn field_markdown( &self, name: &str, ) -> Option<Result<String, RichtextDecodeError>>
The markdown projection of a richtext-valued field (export ∘ decode) —
the field-level twin of Card::body_markdown, and the projection an
emit or a markdown save writes for a content-valued field. The projection
twin of field_richtext, carrying its Ok/Err
decode outcome:
None— the field is absent.Some(Ok(md))— the projected markdown.Some(Err(_))— the field is present but does not decode as richtext (a scalar/array/object astore_fieldwrote, or a non-content object).
Absence returns None; a present non-richtext value returns Some(Err),
so the projection surfaces the type mismatch instead of blanking on it.
Sourcepub fn field_plaintext(
&self,
name: &str,
) -> Option<Result<String, RichtextDecodeError>>
pub fn field_plaintext( &self, name: &str, ) -> Option<Result<String, RichtextDecodeError>>
The plaintext projection of a content-valued field (to_plaintext ∘ decode) — the literal-codec twin of field_markdown,
for a plaintext-typed field: marks are never interpreted, so the text is
verbatim both ways. Carries field_richtext’s
Ok/Err decode outcome:
None— the field is absent.Some(Ok(text))— the projected literal text.Some(Err(_))— the field is present but does not decode as content.