quillmark_core/document/
mod.rs1use 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;
34pub(crate) mod yaml_hints;
35pub mod wire;
36
37pub use dto::{peek_schema_version, StorageError, StoredDocument, SCHEMA_V0_81_0, SCHEMA_V0_82_0};
38pub use edit::EditError;
39pub use meta::{is_valid_kind_name, validate_composable_kind, CardKindError};
40pub use payload::{Payload, PayloadItem};
41pub use wire::{CardWire, PayloadItemWire, WireError};
42
43pub const FORMAT_RULES: &str = "Document format rules:
50\u{2022} Block opener and closer are EXACTLY `~~~` (three tildes, no info string). The `~~~card-yaml` opener is also accepted as a non-canonical alias.
51\u{2022} A blank line must precede every `~~~` block opener (unless it is line 1), and the opener must be at column zero (no leading spaces). An indented `~~~` is an ordinary code block, not a card.
52\u{2022} The first block is the root and MUST contain `$quill: <name>@<version>` and `$kind: main`. Additional blocks declare composable cards via `$kind: <card_kind>`.
53\u{2022} Reserved `$`-keys: `$quill`, `$kind`, `$id`, `$ext`. User fields use lowercase snake_case.
54\u{2022} Prose body is the text after a block's closing `~~~`, up to the next opener or EOF. To include a literal fenced code block in prose, use a backtick fence (```); any column-zero `~~~` block is parsed as card metadata.
55\u{2022} `; delete-ok` fields carry a default \u{2014} keep the line, override the value, or delete the entire line to use the default. Do not write `field:`, `field: null`, or `field: ~` \u{2014} all three parse as explicit YAML null and fail validation.
56\u{2022} Numbers and booleans MUST be unquoted (`year: 2025`, `pinned: true`); quoting turns them into strings and fails validation.
57\u{2022} Plain-scalar values cannot start with `*` or `&` (YAML alias/anchor markers) and cannot contain `: ` (colon-space). For markdown emphasis, embedded colons, or other special prefixes, quote the value: `field: '**bold**'` or `field: \"Name: subtitle\"`. Multi-line values use `|-`, not multi-line quoted scalars.";
58
59const BLUEPRINT_INSTRUCTION_TEMPLATE: &str =
65 "Fill in the `{quill}` blueprint below: replace each `<must-fill>` sentinel and edit the \
66body prose. Submit the filled markdown as `content` to `create_document`.";
67
68pub fn blueprint_instruction(quill_name: &str) -> String {
71 BLUEPRINT_INSTRUCTION_TEMPLATE.replace("{quill}", quill_name)
72}
73
74#[cfg(test)]
75mod tests;
76
77#[derive(Debug)]
79pub struct ParseOutput {
80 pub document: Document,
81 pub warnings: Vec<Diagnostic>,
82}
83
84#[derive(Debug, Clone, PartialEq)]
87pub struct Card {
88 payload: Payload,
89 body: String,
90}
91
92impl Card {
93 pub fn from_parts(payload: Payload, body: String) -> Self {
96 Self { payload, body }
97 }
98
99 pub fn quill(&self) -> Option<&QuillReference> {
100 self.payload.quill()
101 }
102
103 pub fn kind(&self) -> Option<&str> {
104 self.payload.kind()
105 }
106
107 pub fn id(&self) -> Option<&str> {
108 self.payload.id()
109 }
110
111 pub fn ext(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
116 self.payload.ext()
117 }
118
119 pub fn payload(&self) -> &Payload {
120 &self.payload
121 }
122
123 pub fn payload_mut(&mut self) -> &mut Payload {
124 &mut self.payload
125 }
126
127 pub fn body(&self) -> &str {
128 &self.body
129 }
130
131 pub(crate) fn overwrite_body(&mut self, body: String) {
132 self.body = body;
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
139#[serde(into = "StoredDocument", try_from = "StoredDocument")]
140pub struct Document {
141 main: Card,
142 cards: Vec<Card>,
143 warnings: Vec<Diagnostic>,
144}
145
146impl PartialEq for Document {
149 fn eq(&self, other: &Self) -> bool {
150 self.main == other.main && self.cards == other.cards
151 }
152}
153
154impl Document {
155 pub fn from_main_and_cards(main: Card, cards: Vec<Card>, warnings: Vec<Diagnostic>) -> Self {
158 debug_assert!(main.quill().is_some(), "main card must carry `$quill`");
159 debug_assert!(
160 cards.iter().all(|c| c.quill().is_none()),
161 "composable cards must not carry `$quill`"
162 );
163 Self {
164 main,
165 cards,
166 warnings,
167 }
168 }
169
170 pub fn from_markdown(markdown: &str) -> Result<Self, ParseError> {
171 assemble::decompose(markdown)
172 }
173
174 pub fn from_markdown_with_warnings(markdown: &str) -> Result<ParseOutput, ParseError> {
175 assemble::decompose_with_warnings(markdown)
176 .map(|(document, warnings)| ParseOutput { document, warnings })
177 }
178
179 pub fn main(&self) -> &Card {
180 &self.main
181 }
182
183 pub fn main_mut(&mut self) -> &mut Card {
184 &mut self.main
185 }
186
187 pub fn quill_reference(&self) -> QuillReference {
189 self.main
190 .quill()
191 .cloned()
192 .expect("root block's $quill is validated at parse time")
193 }
194
195 pub fn cards(&self) -> &[Card] {
196 &self.cards
197 }
198
199 pub fn cards_mut(&mut self) -> &mut [Card] {
200 &mut self.cards
201 }
202
203 pub fn warnings(&self) -> &[Diagnostic] {
205 &self.warnings
206 }
207
208 pub(crate) fn cards_vec_mut(&mut self) -> &mut Vec<Card> {
209 &mut self.cards
210 }
211
212 pub fn to_plate_json(&self) -> serde_json::Value {
229 let mut map = serde_json::Map::new();
230
231 map.insert(
232 "$quill".to_string(),
233 serde_json::Value::String(self.quill_reference().to_string()),
234 );
235
236 map.insert(
237 "$body".to_string(),
238 serde_json::Value::String(self.main.body.clone()),
239 );
240
241 let cards_array: Vec<serde_json::Value> = self
242 .cards
243 .iter()
244 .map(|card| {
245 let mut card_map = serde_json::Map::new();
246 card_map.insert(
247 "$kind".to_string(),
248 serde_json::Value::String(card.kind().unwrap_or("").to_string()),
249 );
250 card_map.insert(
251 "$body".to_string(),
252 serde_json::Value::String(card.body.clone()),
253 );
254 for (key, value) in card.payload.iter() {
255 card_map.insert(key.clone(), value.as_json().clone());
256 }
257 serde_json::Value::Object(card_map)
258 })
259 .collect();
260
261 map.insert("$cards".to_string(), serde_json::Value::Array(cards_array));
262
263 for (key, value) in self.main.payload.iter() {
264 map.insert(key.clone(), value.as_json().clone());
265 }
266
267 serde_json::Value::Object(map)
268 }
269}