Skip to main content

quillmark_content/
lib.rs

1//! `Content`: the canonical content model for Quillmark.
2//!
3//! One [`Content`] per content field: a single text sequence carrying line
4//! attributes, anchored marks, and embedded islands, over one coordinate space
5//! of Unicode scalar values. Markdown is demoted to a *projection* (import
6//! ([`import::from_markdown`]) and export ([`export::to_markdown`]) codecs) so
7//! every edit is a splice and all structure moves with it.
8//!
9//! `core`, `quillmark`, and both backends (`typst`, `pdfform`) consume this
10//! crate: the seam carries content JSON, storage embeds it structurally (see
11//! `prose/canon/DOCUMENT_STORAGE.md`), and the content edit surface
12//! (`delta`, `ops`) drives per-field splices.
13//!
14//! ## Layout
15//!
16//! - [`model`]: the [`Content`] type, the mark set, normalization (the three
17//!   Spike-A rules), and invariants. The freeze.
18//! - [`serial`]: canonical, byte-deterministic JSON. One encoding for the seam
19//!   and for storage.
20//! - [`island`]: [`KnownIslandType`], the closed dispatch authority for island
21//!   types; adding a type is one variant and the compiler enforces every arm.
22//! - [`import`]: markdown → content (normalize → pulldown → content).
23//! - [`export`]: content → markdown, per island loss class.
24//! - [`delta`], the per-field edit surface: a text-splice change set
25//!   (`retain`/`insert`/`delete`, CodeMirror `ChangeSet` semantics) plus the
26//!   cold-parse + content-diff stale-text writer with a block-move detector. The
27//!   text-splice channel is the positional core; mark and line-attribute edits
28//!   are separate op channels, not op attributes; see [`delta`].
29//! - [`ops`], mark and line op channels:
30//!   [`Content::apply_text_delta`], [`apply_mark_ops`](Content::apply_mark_ops),
31//!   [`apply_line_ops`](Content::apply_line_ops).
32//! - [`normalize`]: the markdown-string input primitive (line endings, bidi
33//!   strip, HTML-comment fence repair), applied at the import boundary.
34//! - [`usv`]: USV → UTF-8 byte-offset conversion for slicing the content.
35
36pub mod delta;
37pub mod export;
38pub mod import;
39pub mod island;
40pub mod model;
41pub mod normalize;
42pub mod ops;
43pub mod serial;
44pub mod usv;
45
46pub use delta::{diff_import, Assoc, Delta, Op};
47pub use export::{to_markdown, to_plaintext};
48pub use import::{from_markdown, from_plaintext};
49pub use island::KnownIslandType;
50pub use model::{
51    Container, Fidelity, Invariant, Island, Line, LineKind, LineKindMismatch, Loss, Mark, MarkKind,
52    Content, Usv,
53};
54pub use normalize::normalize_markdown;
55pub use ops::{
56    change_bundle_from_value, line_op_from_value, line_op_to_value, mark_op_from_value,
57    mark_op_to_value, ApplyError, LineOp, MarkOp,
58};
59pub use serial::ParseError;
60
61/// Maximum container nesting depth the markdown codecs accept before erroring.
62/// The import guard ([`import::from_markdown`]) and the typst backend's markup
63/// converter share this one limit (the backend re-exports it via
64/// `quillmark_core::error::MAX_NESTING_DEPTH`), so a document that imports also
65/// renders.
66pub const MAX_NESTING_DEPTH: usize = 100;
67
68/// Maximum nesting depth of an opaque JSON payload (an island's `props`, an
69/// unknown line/container/mark's `attrs`) measured in container levels from the
70/// bag itself.
71///
72/// The payload axis of [`MAX_NESTING_DEPTH`]: `is_value_key_sorted`,
73/// `sort_keys_owned`, and `serde_json::Value`'s own `Drop` each recurse one
74/// frame per level, so an unbounded bag overflows the stack, on wasm32, an
75/// unrecoverable trap rather than a catchable error. The bag is refused at the
76/// decode boundary, before it is cloned out of the wire, and
77/// [`Content::validate`](model::Content::validate) restates it as an invariant
78/// for the hand-built content that never went through a decoder.
79///
80/// 128 is what `serde_json::from_str` already enforces, so nothing a stored blob
81/// can carry is refused. The string lane counts from the document root rather
82/// than the bag, so it admits ~3 levels fewer; both bound the recursion, and the
83/// per-bag reading is the one the recursive consumers actually walk.
84pub const MAX_JSON_DEPTH: usize = 128;