Skip to main content

quillmark_core/document/
mod.rs

1//! Parsing and typed in-memory model for Quillmark card-yaml documents.
2//!
3//! ## Key types
4//!
5//! - [`Document`]: Root card plus ordered composable cards.
6//! - [`Card`]: A single card block carrying a [`Payload`] and a Markdown body.
7//! - [`Payload`]: Source-ordered items — `$quill`/`$kind`/`$id` metadata, user
8//!   fields, and comments — from a block's YAML content.
9//! - [`PayloadItem`]: The item variant — `Quill`/`Kind`/`Id`/`Field`/`Comment`.
10//!
11//! ## Errors
12//!
13//! [`Document::from_markdown`] returns errors for malformed YAML, unclosed
14//! fences, a missing root `$quill`, unknown `$` keys, or reserved field names.
15//!
16//! See [MARKDOWN.md](https://github.com/quillmark-org/quillmark/blob/main/prose/canon/MARKDOWN.md)
17//! for the card-yaml format specification.
18
19use serde::{Deserialize, Serialize};
20
21use crate::error::ParseError;
22use crate::version::QuillReference;
23use crate::Diagnostic;
24
25pub mod assemble;
26pub mod dto;
27pub mod edit;
28pub mod emit;
29pub mod fences;
30pub mod limits;
31pub mod meta;
32pub mod payload;
33pub mod prescan;
34
35pub use dto::{peek_schema_version, StorageError, StoredDocument, SCHEMA_V0_81_0, SCHEMA_V0_82_0};
36pub use edit::EditError;
37pub use meta::{is_valid_kind_name, validate_composable_kind, CardKindError};
38pub use payload::{Payload, PayloadItem};
39
40#[cfg(test)]
41mod tests;
42
43/// Parse result with the document and any non-fatal warnings.
44#[derive(Debug)]
45pub struct ParseOutput {
46    pub document: Document,
47    pub warnings: Vec<Diagnostic>,
48}
49
50/// A single card-yaml block (root or composable). `body` is `""` when no
51/// content follows the closing fence; check `card.body().is_empty()`.
52#[derive(Debug, Clone, PartialEq)]
53pub struct Card {
54    payload: Payload,
55    body: String,
56}
57
58impl Card {
59    /// Create a `Card` from its parts without validation. For user-facing
60    /// construction of composable cards use [`Card::new`].
61    pub fn from_parts(payload: Payload, body: String) -> Self {
62        Self { payload, body }
63    }
64
65    pub fn quill(&self) -> Option<&QuillReference> {
66        self.payload.quill()
67    }
68
69    pub fn kind(&self) -> Option<&str> {
70        self.payload.kind()
71    }
72
73    pub fn id(&self) -> Option<&str> {
74        self.payload.id()
75    }
76
77    pub fn payload(&self) -> &Payload {
78        &self.payload
79    }
80
81    pub fn payload_mut(&mut self) -> &mut Payload {
82        &mut self.payload
83    }
84
85    pub fn body(&self) -> &str {
86        &self.body
87    }
88
89    pub(crate) fn overwrite_body(&mut self, body: String) {
90        self.body = body;
91    }
92}
93
94/// A fully-parsed Quillmark document. Serde routes through [`StoredDocument`];
95/// for the plate wire shape see [`Document::to_plate_json`].
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(into = "StoredDocument", try_from = "StoredDocument")]
98pub struct Document {
99    main: Card,
100    cards: Vec<Card>,
101    warnings: Vec<Diagnostic>,
102}
103
104// `warnings` are parse-time observations and vary on round-trips; equality
105// covers only structural content (`main` and `cards`).
106impl PartialEq for Document {
107    fn eq(&self, other: &Self) -> bool {
108        self.main == other.main && self.cards == other.cards
109    }
110}
111
112impl Document {
113    /// Create a `Document` from a pre-built main card and composable cards.
114    /// `main` must carry `$quill`; composable cards must not.
115    pub fn from_main_and_cards(main: Card, cards: Vec<Card>, warnings: Vec<Diagnostic>) -> Self {
116        debug_assert!(main.quill().is_some(), "main card must carry `$quill`");
117        debug_assert!(
118            cards.iter().all(|c| c.quill().is_none()),
119            "composable cards must not carry `$quill`"
120        );
121        Self {
122            main,
123            cards,
124            warnings,
125        }
126    }
127
128    pub fn from_markdown(markdown: &str) -> Result<Self, ParseError> {
129        assemble::decompose(markdown)
130    }
131
132    pub fn from_markdown_with_warnings(markdown: &str) -> Result<ParseOutput, ParseError> {
133        assemble::decompose_with_warnings(markdown)
134            .map(|(document, warnings)| ParseOutput { document, warnings })
135    }
136
137    pub fn main(&self) -> &Card {
138        &self.main
139    }
140
141    pub fn main_mut(&mut self) -> &mut Card {
142        &mut self.main
143    }
144
145    /// The `$quill` reference from the root block. Always present on parsed documents.
146    pub fn quill_reference(&self) -> QuillReference {
147        self.main
148            .quill()
149            .cloned()
150            .expect("root block's $quill is validated at parse time")
151    }
152
153    pub fn cards(&self) -> &[Card] {
154        &self.cards
155    }
156
157    pub fn cards_mut(&mut self) -> &mut [Card] {
158        &mut self.cards
159    }
160
161    /// Non-fatal warnings from the parse; empty for programmatically built documents.
162    pub fn warnings(&self) -> &[Diagnostic] {
163        &self.warnings
164    }
165
166    pub(crate) fn cards_vec_mut(&mut self) -> &mut Vec<Card> {
167        &mut self.cards
168    }
169
170    /// Serialize to the JSON wire shape consumed by backend plates. This is
171    /// the **only** place in `quillmark-core` that produces this shape:
172    ///
173    /// ```json
174    /// {
175    ///   "QUILL": "<ref>", "<field>": <value>, ...,
176    ///   "BODY": "<global-body>",
177    ///   "CARDS": [{ "CARD": "<tag>", "<field>": <value>, ..., "BODY": "<card-body>" }]
178    /// }
179    /// ```
180    pub fn to_plate_json(&self) -> serde_json::Value {
181        let mut map = serde_json::Map::new();
182
183        map.insert(
184            "QUILL".to_string(),
185            serde_json::Value::String(self.quill_reference().to_string()),
186        );
187
188        for (key, value) in self.main.payload.iter() {
189            map.insert(key.clone(), value.as_json().clone());
190        }
191
192        map.insert(
193            "BODY".to_string(),
194            serde_json::Value::String(self.main.body.clone()),
195        );
196
197        let cards_array: Vec<serde_json::Value> = self
198            .cards
199            .iter()
200            .map(|card| {
201                let mut card_map = serde_json::Map::new();
202                card_map.insert(
203                    "CARD".to_string(),
204                    serde_json::Value::String(card.kind().unwrap_or("").to_string()),
205                );
206                for (key, value) in card.payload.iter() {
207                    card_map.insert(key.clone(), value.as_json().clone());
208                }
209                card_map.insert(
210                    "BODY".to_string(),
211                    serde_json::Value::String(card.body.clone()),
212                );
213                serde_json::Value::Object(card_map)
214            })
215            .collect();
216
217        map.insert("CARDS".to_string(), serde_json::Value::Array(cards_array));
218
219        serde_json::Value::Object(map)
220    }
221}