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 mod wire;
35pub(crate) mod yaml_hints;
36
37pub use dto::{peek_schema_version, StorageError, StoredDocument, SCHEMA_V0_92_0};
38pub use edit::EditError;
39pub use meta::{is_valid_kind_name, validate_composable_kind, CardKindError};
40pub use payload::{MetaKey, 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`, `$seed`. 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, PartialEq, Default)]
144pub struct SeedOverlay {
145 pub fields: indexmap::IndexMap<String, crate::value::QuillValue>,
147 pub body: Option<String>,
149}
150
151impl SeedOverlay {
152 pub fn from_json(value: &serde_json::Value) -> Option<Self> {
158 value.as_object().map(Self::from_json_map)
159 }
160
161 fn from_json_map(map: &serde_json::Map<String, serde_json::Value>) -> Self {
167 let mut fields = indexmap::IndexMap::new();
168 let mut body = None;
169 for (key, value) in map {
170 if key == "$body" {
171 if let Some(s) = value.as_str() {
172 body = Some(s.to_string());
173 }
174 } else if key.starts_with('$') {
175 continue;
178 } else {
179 fields.insert(
180 key.clone(),
181 crate::value::QuillValue::from_json(value.clone()),
182 );
183 }
184 }
185 SeedOverlay { fields, body }
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
192#[serde(into = "StoredDocument", try_from = "StoredDocument")]
193pub struct Document {
194 main: Card,
195 cards: Vec<Card>,
196 warnings: Vec<Diagnostic>,
197}
198
199impl PartialEq for Document {
202 fn eq(&self, other: &Self) -> bool {
203 self.main == other.main && self.cards == other.cards
204 }
205}
206
207impl Document {
208 pub fn from_main_and_cards(main: Card, cards: Vec<Card>, warnings: Vec<Diagnostic>) -> Self {
211 debug_assert!(main.quill().is_some(), "main card must carry `$quill`");
212 debug_assert!(
213 cards.iter().all(|c| c.quill().is_none()),
214 "composable cards must not carry `$quill`"
215 );
216 debug_assert!(
217 cards.iter().all(|c| c.seed().is_none()),
218 "composable cards must not carry `$seed`"
219 );
220 Self {
221 main,
222 cards,
223 warnings,
224 }
225 }
226
227 pub fn from_markdown(markdown: &str) -> Result<Self, ParseError> {
228 assemble::decompose(markdown)
229 }
230
231 pub fn from_markdown_with_warnings(markdown: &str) -> Result<ParseOutput, ParseError> {
232 assemble::decompose_with_warnings(markdown)
233 .map(|(document, warnings)| ParseOutput { document, warnings })
234 }
235
236 pub fn main(&self) -> &Card {
237 &self.main
238 }
239
240 pub fn main_mut(&mut self) -> &mut Card {
241 &mut self.main
242 }
243
244 pub fn quill_reference(&self) -> QuillReference {
246 self.main
247 .quill()
248 .cloned()
249 .expect("root block's $quill is validated at parse time")
250 }
251
252 pub fn cards(&self) -> &[Card] {
253 &self.cards
254 }
255
256 pub fn cards_mut(&mut self) -> &mut [Card] {
257 &mut self.cards
258 }
259
260 pub fn warnings(&self) -> &[Diagnostic] {
262 &self.warnings
263 }
264
265 pub(crate) fn cards_vec_mut(&mut self) -> &mut Vec<Card> {
266 &mut self.cards
267 }
268
269 pub fn to_plate_json(&self) -> serde_json::Value {
286 let mut map = serde_json::Map::new();
287
288 map.insert(
289 "$quill".to_string(),
290 serde_json::Value::String(self.quill_reference().to_string()),
291 );
292
293 map.insert(
294 "$body".to_string(),
295 serde_json::Value::String(self.main.body.clone()),
296 );
297
298 let cards_array: Vec<serde_json::Value> = self
299 .cards
300 .iter()
301 .map(|card| {
302 let mut card_map = serde_json::Map::new();
303 card_map.insert(
304 "$kind".to_string(),
305 serde_json::Value::String(card.kind().unwrap_or("").to_string()),
306 );
307 card_map.insert(
308 "$body".to_string(),
309 serde_json::Value::String(card.body.clone()),
310 );
311 for (key, value) in card.payload.iter() {
312 card_map.insert(key.clone(), value.as_json().clone());
313 }
314 serde_json::Value::Object(card_map)
315 })
316 .collect();
317
318 map.insert("$cards".to_string(), serde_json::Value::Array(cards_array));
319
320 for (key, value) in self.main.payload.iter() {
321 map.insert(key.clone(), value.as_json().clone());
322 }
323
324 serde_json::Value::Object(map)
325 }
326}