pub struct Content {
pub text: String,
pub lines: Vec<Line>,
pub marks: Vec<Mark>,
pub islands: Vec<Island>,
}Expand description
The canonical content model — re-exported so consumers of the
document mutators (Card::install_body, Card::apply_body_change)
can name the type without depending on quillmark-content directly.
One content field as a content: the text plus the structure that rides on it.
Invariants (established once by import normalization, checked by
Content::validate): the text holds no \r and no bidi controls; the
count of ISLAND_SLOT equals islands.len(); lines.len() equals the
number of \n-separated segments; marks are normalized (sorted, unioned).
Fields§
§text: StringThe content. \n is a line boundary; ISLAND_SLOT is an island slot.
lines: Vec<Line>One entry per \n-separated segment of text, in order. The line tree
is derived from this flat list plus each line’s containers path — it
is never stored, so a split/join is a single-char edit with no identity
crisis (there are no paragraph IDs).
marks: Vec<Mark>Marks over char ranges, kept normalized: sorted by
(start, end, kind-ord, attrs), same-kind formatting marks unioned.
islands: Vec<Island>One entry per ISLAND_SLOT, in slot order (ascending char position).
Implementations§
Source§impl Content
impl Content
Sourcepub const RESERVED_MARK_TYPES: [&'static str; 7]
pub const RESERVED_MARK_TYPES: [&'static str; 7]
Mark type names the projection reserves; an MarkKind::Unknown may
not reuse one (its serialization would parse back as the built-in,
silently dropping its attrs — non-injective). Checked by Content::validate.
Sourcepub fn is_inline(&self) -> bool
pub fn is_inline(&self) -> bool
Whether this content satisfies the richtext(inline) constraint: exactly
one Para line, sitting in no container, with no islands. A single line
can never continues (line 0 is always false), so that dimension is
implied. Content::empty is inline (one empty Para), so a blank or
zero-filled inline field passes.
Sourcepub fn is_plain(&self) -> bool
pub fn is_plain(&self) -> bool
Whether this content satisfies the plaintext constraint: no marks, no
islands, and every line is a plain Para sitting in no container. It is
the multi-line generalization of is_inline (which
additionally pins the content to one line) with the mark/island exclusion
made explicit — a plaintext value carries prose the author navigates but
no formatting. continues is unconstrained: a lone \n may be a
within-paragraph break. Content::empty is plain.
This is the plaintext analogue of is_inline, enforced at coercion and
validation with the NotPlain error; the distinguishing property of
plaintext over richtext { marks: [] } is the literal codec
(crate::import::from_plaintext), not this predicate.
Sourcepub fn is_blank(&self) -> bool
pub fn is_blank(&self) -> bool
Whether the content carries no renderable content: the text is empty or
whitespace-only. An island slot (ISLAND_SLOT, U+FFFC) is not
whitespace, so an island-bearing content is never blank. Body-disabled
validation and round-trip emit key on it.
Sourcepub fn segment_count(&self) -> usize
pub fn segment_count(&self) -> usize
Number of \n-separated segments — the required lines.len().
Sourcepub fn normalize(&mut self)
pub fn normalize(&mut self)
Normalize marks in place: drop zero-width formatting, union same-kind formatting that is adjacent or overlapping, recursively key-sort island props and unknown-mark attrs, then sort marks canonically. Idempotent — the fixed point the canonical serialization commits to.
Source§impl Content
impl Content
Sourcepub fn apply_text_delta(&mut self, delta: &Delta) -> Result<(), ApplyError>
pub fn apply_text_delta(&mut self, delta: &Delta) -> Result<(), ApplyError>
Splice text via delta, rebase marks, sync lines to \n changes,
cascade island removal for any deleted slot, then normalize.
Islands stay in lockstep with their ISLAND_SLOT chars: a delta that
deletes a slot drops the corresponding Island (the content goes
away with its slot); a delta that inserts a raw slot is rejected
(ApplyError::IslandSlotInInsert) — islands are created through their
own channel, never a text splice, so a slot arriving here would orphan.
Inserted text is sanitized first: \r and Unicode bidi controls — the
chars Content::validate forbids — are stripped, mirroring the
normalization import applies at the string boundary. The text-delta
channel is the other way text enters the content, so without this an
insert of \r or a bidi control returned Ok while leaving a content
that fails validate() (see issue #899).
Sourcepub fn apply_mark_ops(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError>
pub fn apply_mark_ops(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError>
Apply mark ops in final-text coordinates, then normalize.
Sourcepub fn apply_line_ops(&mut self, ops: &[LineOp]) -> Result<(), ApplyError>
pub fn apply_line_ops(&mut self, ops: &[LineOp]) -> Result<(), ApplyError>
Apply line ops — split/join splice \n; set ops touch metadata only.
Sourcepub fn apply_field_change(
&mut self,
text_delta: &Delta,
line_ops: &[LineOp],
mark_ops: &[MarkOp],
) -> Result<(), ApplyError>
pub fn apply_field_change( &mut self, text_delta: &Delta, line_ops: &[LineOp], mark_ops: &[MarkOp], ) -> Result<(), ApplyError>
One committed field edit bundle: text delta, then line ops, then marks,
canonicalized by a single terminal normalize.
All-or-nothing: on any op’s error self is left exactly as it was, so a
caller need not snapshot-and-restore around a failed bundle. A bundle
carrying line or mark ops has several fallible stages that would
otherwise partially commit, so it is staged on a scratch copy and
swapped in only once every stage succeeds. The pure-text-delta path (the
per-keystroke hot path) skips the clone: apply_text_delta validates the
delta before mutating, so it is already atomic on the errors a caller can
provoke.
The stages run on their non-normalizing inner forms and normalize runs
once at the end. One terminal normalize suffices because split/join
rebase marks through their \n splice
(map_pos semantics): the
formatting-edge \n-trim then commutes with the line ops (trim-per-stage
and trim-once converge), and MarkOp::Remove is coverage-set
subtraction, which commutes with normalize’s same-kind union
((A ∪ B) \ R = (A\R) ∪ (B\R)). One canonicalization point, one pass.
Source§impl Content
impl Content
Sourcepub fn to_canonical_json(&self) -> String
pub fn to_canonical_json(&self) -> String
Serialize to canonical JSON bytes. Normalizes a copy first, so the output
is canonical regardless of the caller’s mark/island order. Every object
key is sorted recursively so the bytes do not depend on
serde_json’s preserve_order feature being enabled in the consumer’s
crate graph — the canonical form is feature-independent.
Sourcepub fn from_canonical_json(s: &str) -> Result<Content, ParseError>
pub fn from_canonical_json(s: &str) -> Result<Content, ParseError>
Parse canonical JSON, normalize (idempotent), and validate. Returns
ParseError::Invalid for a content that violates its invariants, so
storage cannot silently round-trip a malformed value.
from_canonical_json(to_canonical_json(x)) round-trips to a canonical
value and re-serializes to identical bytes.