use serde::{Deserialize, Serialize};
use crate::error::ParseError;
use crate::version::QuillReference;
use crate::Diagnostic;
pub mod assemble;
pub mod dto;
pub mod edit;
pub mod emit;
pub mod fences;
pub mod limits;
pub mod meta;
pub mod payload;
pub mod prescan;
pub mod wire;
pub(crate) mod yaml_hints;
pub use dto::{peek_schema_version, StorageError, StoredDocument, SCHEMA_V0_92_0};
pub use edit::EditError;
pub use meta::{is_valid_kind_name, validate_composable_kind, CardKindError};
pub use payload::{MetaKey, Payload, PayloadItem};
pub use wire::{CardWire, PayloadItemWire, WireError};
pub const FORMAT_RULES: &str = "Document format rules:
\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.
\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.
\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>`.
\u{2022} Reserved `$`-keys: `$quill`, `$kind`, `$id`, `$ext`, `$seed`. User fields use lowercase snake_case.
\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.
\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.
\u{2022} Numbers and booleans MUST be unquoted (`year: 2025`, `pinned: true`); quoting turns them into strings and fails validation.
\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.";
const BLUEPRINT_INSTRUCTION_TEMPLATE: &str =
"Fill in the `{quill}` blueprint below: replace each `<must-fill>` sentinel and edit the \
body prose. Submit the filled markdown as `content` to `create_document`.";
pub fn blueprint_instruction(quill_name: &str) -> String {
BLUEPRINT_INSTRUCTION_TEMPLATE.replace("{quill}", quill_name)
}
#[cfg(test)]
mod tests;
#[derive(Debug)]
pub struct ParseOutput {
pub document: Document,
pub warnings: Vec<Diagnostic>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Card {
payload: Payload,
body: String,
}
impl Card {
pub fn from_parts(payload: Payload, body: String) -> Self {
Self { payload, body }
}
pub fn quill(&self) -> Option<&QuillReference> {
self.payload.quill()
}
pub fn kind(&self) -> Option<&str> {
self.payload.kind()
}
pub fn id(&self) -> Option<&str> {
self.payload.id()
}
pub fn ext(&self) -> Option<&serde_json::Map<String, serde_json::Value>> {
self.payload.ext()
}
pub fn payload(&self) -> &Payload {
&self.payload
}
pub fn payload_mut(&mut self) -> &mut Payload {
&mut self.payload
}
pub fn body(&self) -> &str {
&self.body
}
pub(crate) fn overwrite_body(&mut self, body: String) {
self.body = body;
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SeedOverlay {
pub fields: indexmap::IndexMap<String, crate::value::QuillValue>,
pub body: Option<String>,
}
impl SeedOverlay {
pub fn from_json(value: &serde_json::Value) -> Option<Self> {
value.as_object().map(Self::from_json_map)
}
fn from_json_map(map: &serde_json::Map<String, serde_json::Value>) -> Self {
let mut fields = indexmap::IndexMap::new();
let mut body = None;
for (key, value) in map {
if key == "$body" {
if let Some(s) = value.as_str() {
body = Some(s.to_string());
}
} else if key.starts_with('$') {
continue;
} else {
fields.insert(
key.clone(),
crate::value::QuillValue::from_json(value.clone()),
);
}
}
SeedOverlay { fields, body }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(into = "StoredDocument", try_from = "StoredDocument")]
pub struct Document {
main: Card,
cards: Vec<Card>,
warnings: Vec<Diagnostic>,
}
impl PartialEq for Document {
fn eq(&self, other: &Self) -> bool {
self.main == other.main && self.cards == other.cards
}
}
impl Document {
pub fn from_main_and_cards(main: Card, cards: Vec<Card>, warnings: Vec<Diagnostic>) -> Self {
debug_assert!(main.quill().is_some(), "main card must carry `$quill`");
debug_assert!(
cards.iter().all(|c| c.quill().is_none()),
"composable cards must not carry `$quill`"
);
debug_assert!(
cards.iter().all(|c| c.seed().is_none()),
"composable cards must not carry `$seed`"
);
Self {
main,
cards,
warnings,
}
}
pub fn from_markdown(markdown: &str) -> Result<Self, ParseError> {
assemble::decompose(markdown)
}
pub fn from_markdown_with_warnings(markdown: &str) -> Result<ParseOutput, ParseError> {
assemble::decompose_with_warnings(markdown)
.map(|(document, warnings)| ParseOutput { document, warnings })
}
pub fn main(&self) -> &Card {
&self.main
}
pub fn main_mut(&mut self) -> &mut Card {
&mut self.main
}
pub fn quill_reference(&self) -> QuillReference {
self.main
.quill()
.cloned()
.expect("root block's $quill is validated at parse time")
}
pub fn cards(&self) -> &[Card] {
&self.cards
}
pub fn cards_mut(&mut self) -> &mut [Card] {
&mut self.cards
}
pub fn warnings(&self) -> &[Diagnostic] {
&self.warnings
}
pub(crate) fn cards_vec_mut(&mut self) -> &mut Vec<Card> {
&mut self.cards
}
pub fn to_plate_json(&self) -> serde_json::Value {
let mut map = serde_json::Map::new();
map.insert(
"$quill".to_string(),
serde_json::Value::String(self.quill_reference().to_string()),
);
map.insert(
"$body".to_string(),
serde_json::Value::String(self.main.body.clone()),
);
let cards_array: Vec<serde_json::Value> = self
.cards
.iter()
.map(|card| {
let mut card_map = serde_json::Map::new();
card_map.insert(
"$kind".to_string(),
serde_json::Value::String(card.kind().unwrap_or("").to_string()),
);
card_map.insert(
"$body".to_string(),
serde_json::Value::String(card.body.clone()),
);
for (key, value) in card.payload.iter() {
card_map.insert(key.clone(), value.as_json().clone());
}
serde_json::Value::Object(card_map)
})
.collect();
map.insert("$cards".to_string(), serde_json::Value::Array(cards_array));
for (key, value) in self.main.payload.iter() {
map.insert(key.clone(), value.as_json().clone());
}
serde_json::Value::Object(map)
}
}