use serde::{Deserialize, Serialize};
use quillmark_content::import::{from_markdown as import_markdown, ImportError};
use quillmark_content::Content;
use crate::error::ParseError;
use crate::version::QuillReference;
use crate::Diagnostic;
pub(crate) fn import_body(md: &str) -> Result<Content, ImportError> {
if md.is_empty() {
Ok(Content::empty())
} else {
import_markdown(md)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RichtextDecodeError {
NotContent(String),
BadMarkdown(String),
}
impl RichtextDecodeError {
pub fn into_message(self) -> String {
match self {
RichtextDecodeError::NotContent(m) | RichtextDecodeError::BadMarkdown(m) => m,
}
}
}
pub(crate) fn decode_richtext_value(
value: &serde_json::Value,
) -> Option<Result<Content, RichtextDecodeError>> {
match value {
serde_json::Value::Object(_) => Some(
quillmark_content::serial::from_canonical_value(value)
.map_err(|e| RichtextDecodeError::NotContent(e.to_string())),
),
serde_json::Value::String(md) => {
Some(import_body(md).map_err(|e| RichtextDecodeError::BadMarkdown(e.to_string())))
}
_ => None,
}
}
pub(crate) fn decode_plaintext_value(
value: &serde_json::Value,
) -> Option<Result<Content, String>> {
match value {
serde_json::Value::Object(_) => {
Some(quillmark_content::serial::from_canonical_value(value).map_err(|e| e.to_string()))
}
serde_json::Value::String(s) => Some(Ok(quillmark_content::from_plaintext(s))),
_ => None,
}
}
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_93_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>`. Its `$kind` is `main` by position \u{2014} an explicit `$kind: main` is accepted but not required. 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} A field that already shows a concrete value carries a default and is shippable as-is \u{2014} keep the line, override the value, or delete it to fall back to the default. A blank or null value (`field:`, `field: null`, `field: ~`) is treated the same as omitting the field: it falls back to the default, or to the type-empty zero value.
\u{2022} `field: !must_fill <value>` marks a placeholder awaiting your input \u{2014} replace it with a real value and drop the `!must_fill` tag before shipping. A bare `field: !must_fill` is an empty placeholder. A leftover marker never blocks rendering, but it is reported as a warning until you replace it.
\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` placeholder with a real \
value 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)]
#[must_use = "carries parse warnings; read `.document`/`.warnings` or bind it"]
pub struct Parsed {
pub document: Document,
pub warnings: Vec<Diagnostic>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Card {
payload: Payload,
body: Content,
}
impl Card {
pub fn from_parts(payload: Payload, body: Content) -> 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) -> &Content {
&self.body
}
pub fn body_markdown(&self) -> String {
quillmark_content::export::to_markdown(&self.body)
}
pub(crate) fn overwrite_body(&mut self, body: Content) {
self.body = body;
}
pub(crate) fn body_mut(&mut self) -> &mut Content {
&mut self.body
}
pub fn field_richtext(&self, name: &str) -> Option<Result<Content, RichtextDecodeError>> {
let value = self.payload.get(name)?.as_json();
Some(match crate::document::decode_richtext_value(value) {
Some(result) => result,
None => match value {
serde_json::Value::Null => Ok(Content::empty()),
_ => Err(RichtextDecodeError::NotContent(
"expected a richtext content object or a markdown string".to_string(),
)),
},
})
}
pub fn field_markdown(&self, name: &str) -> Option<Result<String, RichtextDecodeError>> {
Some(self.field_richtext(name)?.map(|rt| quillmark_content::export::to_markdown(&rt)))
}
pub fn field_plaintext(&self, name: &str) -> Option<Result<String, RichtextDecodeError>> {
Some(self.field_richtext(name)?.map(|rt| quillmark_content::export::to_plaintext(&rt)))
}
}
#[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, PartialEq, Serialize, Deserialize)]
#[serde(into = "StoredDocument", try_from = "StoredDocument")]
pub struct Document {
main: Card,
cards: Vec<Card>,
}
impl Document {
pub fn new(quill: QuillReference) -> Self {
let mut payload = Payload::new();
payload.set_quill(quill);
payload.set_kind("main");
Self {
main: Card::from_parts(payload, Content::empty()),
cards: Vec::new(),
}
}
pub fn from_main_and_cards(main: Card, cards: Vec<Card>) -> 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 }
}
#[doc(alias = "from_markdown")]
pub fn parse(markdown: &str) -> Result<Parsed, ParseError> {
assemble::decompose_with_warnings(markdown)
.map(|(document, warnings)| Parsed { 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 card(&self, index: usize) -> Option<&Card> {
self.cards.get(index)
}
pub fn find_card(&self, id: &str) -> Option<(usize, &Card)> {
self.cards
.iter()
.enumerate()
.find(|(_, card)| card.id() == Some(id))
}
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(),
quillmark_content::serial::to_canonical_value(self.main.body()),
);
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(),
quillmark_content::serial::to_canonical_value(card.body()),
);
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)
}
}