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;
35
36pub use dto::{peek_schema_version, StorageError, StoredDocument, SCHEMA_V0_81_0, SCHEMA_V0_82_0};
37pub use edit::EditError;
38pub use meta::{is_valid_kind_name, validate_composable_kind, CardKindError};
39pub use payload::{Payload, PayloadItem};
40
41pub const FORMAT_RULES: &str = "Document format rules:
48\u{2022} Block opener is `~~~card-yaml`; closer is EXACTLY `~~~` (three tildes, no info string). Do NOT repeat `~~~card-yaml` as the closer.
49\u{2022} A blank line must precede every `~~~card-yaml` opener (unless it is line 1).
50\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>`.
51\u{2022} Reserved `$`-keys: `$quill`, `$kind`, `$id`, `$ext`. User fields use lowercase snake_case.
52\u{2022} Prose body is the text after a block's closing `~~~`, up to the next opener or EOF.
53\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.
54\u{2022} Numbers and booleans MUST be unquoted (`year: 2025`, `pinned: true`); quoting turns them into strings and fails validation.
55\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.";
56
57const BLUEPRINT_INSTRUCTION_TEMPLATE: &str =
63 "Fill in the `{quill}` blueprint below: replace each `<must-fill>` sentinel and edit the \
64body prose. Submit the filled markdown as `content` to `create_document`.";
65
66pub fn blueprint_instruction(quill_name: &str) -> String {
69 BLUEPRINT_INSTRUCTION_TEMPLATE.replace("{quill}", quill_name)
70}
71
72#[cfg(test)]
73mod tests;
74
75#[derive(Debug)]
77pub struct ParseOutput {
78 pub document: Document,
79 pub warnings: Vec<Diagnostic>,
80}
81
82#[derive(Debug, Clone, PartialEq)]
85pub struct Card {
86 payload: Payload,
87 body: String,
88}
89
90impl Card {
91 pub fn from_parts(payload: Payload, body: String) -> Self {
94 Self { payload, body }
95 }
96
97 pub fn quill(&self) -> Option<&QuillReference> {
98 self.payload.quill()
99 }
100
101 pub fn kind(&self) -> Option<&str> {
102 self.payload.kind()
103 }
104
105 pub fn id(&self) -> Option<&str> {
106 self.payload.id()
107 }
108
109 pub fn ext(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
114 self.payload.ext()
115 }
116
117 pub fn payload(&self) -> &Payload {
118 &self.payload
119 }
120
121 pub fn payload_mut(&mut self) -> &mut Payload {
122 &mut self.payload
123 }
124
125 pub fn body(&self) -> &str {
126 &self.body
127 }
128
129 pub(crate) fn overwrite_body(&mut self, body: String) {
130 self.body = body;
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(into = "StoredDocument", try_from = "StoredDocument")]
138pub struct Document {
139 main: Card,
140 cards: Vec<Card>,
141 warnings: Vec<Diagnostic>,
142}
143
144impl PartialEq for Document {
147 fn eq(&self, other: &Self) -> bool {
148 self.main == other.main && self.cards == other.cards
149 }
150}
151
152impl Document {
153 pub fn from_main_and_cards(main: Card, cards: Vec<Card>, warnings: Vec<Diagnostic>) -> Self {
156 debug_assert!(main.quill().is_some(), "main card must carry `$quill`");
157 debug_assert!(
158 cards.iter().all(|c| c.quill().is_none()),
159 "composable cards must not carry `$quill`"
160 );
161 Self {
162 main,
163 cards,
164 warnings,
165 }
166 }
167
168 pub fn from_markdown(markdown: &str) -> Result<Self, ParseError> {
169 assemble::decompose(markdown)
170 }
171
172 pub fn from_markdown_with_warnings(markdown: &str) -> Result<ParseOutput, ParseError> {
173 assemble::decompose_with_warnings(markdown)
174 .map(|(document, warnings)| ParseOutput { document, warnings })
175 }
176
177 pub fn main(&self) -> &Card {
178 &self.main
179 }
180
181 pub fn main_mut(&mut self) -> &mut Card {
182 &mut self.main
183 }
184
185 pub fn quill_reference(&self) -> QuillReference {
187 self.main
188 .quill()
189 .cloned()
190 .expect("root block's $quill is validated at parse time")
191 }
192
193 pub fn cards(&self) -> &[Card] {
194 &self.cards
195 }
196
197 pub fn cards_mut(&mut self) -> &mut [Card] {
198 &mut self.cards
199 }
200
201 pub fn warnings(&self) -> &[Diagnostic] {
203 &self.warnings
204 }
205
206 pub(crate) fn cards_vec_mut(&mut self) -> &mut Vec<Card> {
207 &mut self.cards
208 }
209
210 pub fn to_plate_json(&self) -> serde_json::Value {
227 let mut map = serde_json::Map::new();
228
229 map.insert(
230 "$quill".to_string(),
231 serde_json::Value::String(self.quill_reference().to_string()),
232 );
233
234 map.insert(
235 "$body".to_string(),
236 serde_json::Value::String(self.main.body.clone()),
237 );
238
239 let cards_array: Vec<serde_json::Value> = self
240 .cards
241 .iter()
242 .map(|card| {
243 let mut card_map = serde_json::Map::new();
244 card_map.insert(
245 "$kind".to_string(),
246 serde_json::Value::String(card.kind().unwrap_or("").to_string()),
247 );
248 card_map.insert(
249 "$body".to_string(),
250 serde_json::Value::String(card.body.clone()),
251 );
252 for (key, value) in card.payload.iter() {
253 card_map.insert(key.clone(), value.as_json().clone());
254 }
255 serde_json::Value::Object(card_map)
256 })
257 .collect();
258
259 map.insert("$cards".to_string(), serde_json::Value::Array(cards_array));
260
261 for (key, value) in self.main.payload.iter() {
262 map.insert(key.clone(), value.as_json().clone());
263 }
264
265 serde_json::Value::Object(map)
266 }
267}