#[non_exhaustive]pub struct Content {
pub text: String,
pub lines: Vec<Line>,
pub marks: Vec<Mark>,
pub islands: Vec<Island>,
}Expand description
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 (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.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 [&'static str]
pub const RESERVED_MARK_TYPES: &'static [&'static str]
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).
Two enforcement points, on two lanes. Content::validate catches an
in-process Rust construction. The wire never reaches it: a decoder resolves
the built-in name before the Unknown fallthrough, so a reserved tag
becomes the built-in rather than arriving as an Unknown. The authored
lane therefore rejects the shape up front
(serial::from_authored_value for
whole content; ops::mark_op_from_value
and ops::line_op_from_value for the op
wire), while storage decode stays lenient by design; see there.
This list and its two siblings are re-spelled by hand on the TypeScript
surface: the unions in crates/bindings/wasm/src/engine.rs and the
isUnknown* guards’ tables in
crates/bindings/wasm/runtime/runtime.js. Both are pinned to these
constants by crates/bindings/wasm/tests/known_names_drift.rs.
Slices, not arrays, on all three: an array’s length is part of its type,
and promoting a name into the projection is the motion these lists exist
to absorb.
Sourcepub const RESERVED_LINE_KINDS: &'static [&'static str]
pub const RESERVED_LINE_KINDS: &'static [&'static str]
Line kind names the projection reserves: the LineKind twin of
RESERVED_MARK_TYPES, for the same
injectivity reason.
Sourcepub const RESERVED_CONTAINERS: &'static [&'static str]
pub const RESERVED_CONTAINERS: &'static [&'static str]
Container names the projection reserves: the Container twin of
RESERVED_MARK_TYPES.
Sourcepub fn new(text: String, lines: Vec<Line>) -> Self
pub fn new(text: String, lines: Vec<Line>) -> Self
The text and its per-line attributes: what a content always carries.
Marks and islands start empty and have with_marks /
with_islands beside them.
Constructing does not normalize or check: the invariants in this type’s
docs are the caller’s until validate runs. The codecs
(crate::import, Content::from_canonical_json) establish them, and
are what a consumer normally builds through.
Sourcepub fn with_marks(self, marks: Vec<Mark>) -> Self
pub fn with_marks(self, marks: Vec<Mark>) -> Self
Set marks, the range-anchored marks over the text.
Sourcepub fn with_islands(self, islands: Vec<Island>) -> Self
pub fn with_islands(self, islands: Vec<Island>) -> Self
Set islands, one per ISLAND_SLOT in slot order.
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
IslandOp::Insert, 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().
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_island_ops(&mut self, ops: &[IslandOp]) -> Result<(), ApplyError>
pub fn apply_island_ops(&mut self, ops: &[IslandOp]) -> Result<(), ApplyError>
Apply island ops: replace an entry by id, or insert a slot and its entry together.
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,
bundle: &ChangeBundle,
) -> Result<(), ApplyError>
pub fn apply_field_change( &mut self, bundle: &ChangeBundle, ) -> Result<(), ApplyError>
One committed field edit bundle: text delta, then island ops, 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 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.
Stage order is a coordinate contract, not a convenience: each stage
reads the text the earlier ones left. Island ops sit between the delta
and the line ops because both neighbors need them there. An island insert
splices a slot, so a LineOp::SetKind { kind: Island } in the same bundle
can only validate against a line that already carries it; Split/Join
and every mark range are then measured in a frame that includes the new
slots. The one-bundle block island (IslandOp::Insert) follows from
that.
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.