use std::str::FromStr;
use indexmap::IndexMap;
use crate::error::ParseError;
use crate::value::QuillValue;
use crate::version::QuillReference;
use crate::Diagnostic;
use super::fences::{fence_opener_len, find_metadata_blocks};
use super::sentinel::extract_sentinels;
use super::{Card, Document};
#[derive(Debug)]
pub(super) struct MetadataBlock {
pub(super) start: usize, pub(super) end: usize, pub(super) yaml_value: Option<serde_json::Value>, pub(super) tag: Option<String>, pub(super) quill_ref: Option<String>, }
fn yaml_parse_options() -> serde_saphyr::Options {
let budget = serde_saphyr::Budget {
max_depth: super::limits::MAX_YAML_DEPTH,
..Default::default()
};
serde_saphyr::Options {
budget: Some(budget),
..Default::default()
}
}
pub(super) fn build_block(
markdown: &str,
abs_pos: usize,
abs_closing_pos: usize,
block_end: usize,
block_index: usize,
) -> Result<MetadataBlock, ParseError> {
let raw_content = &markdown[abs_pos + fence_opener_len(markdown, abs_pos)..abs_closing_pos];
if raw_content.len() > crate::error::MAX_YAML_SIZE {
return Err(ParseError::InputTooLarge {
size: raw_content.len(),
max: crate::error::MAX_YAML_SIZE,
});
}
let content = raw_content.trim();
let (tag, quill_ref, yaml_value) = if content.is_empty() {
(None, None, None)
} else {
match serde_saphyr::from_str_with_options::<serde_json::Value>(
content,
yaml_parse_options(),
) {
Ok(parsed) => extract_sentinels(parsed, markdown, abs_pos, block_index)?,
Err(e) => {
let line = markdown[..abs_pos].lines().count() + 1;
return Err(ParseError::YamlErrorWithLocation {
message: e.to_string(),
line,
block_index,
});
}
}
};
if let Some(serde_json::Value::Object(ref map)) = yaml_value {
let sentinel_extra = if quill_ref.is_some() || tag.is_some() {
1
} else {
0
};
if map.len() + sentinel_extra > crate::error::MAX_FIELD_COUNT {
return Err(ParseError::InputTooLarge {
size: map.len() + sentinel_extra,
max: crate::error::MAX_FIELD_COUNT,
});
}
}
Ok(MetadataBlock {
start: abs_pos,
end: block_end,
yaml_value,
tag,
quill_ref,
})
}
fn missing_quill_message(first_fence_issue: Option<(String, usize)>) -> String {
match first_fence_issue {
Some((actual, line)) if actual.eq_ignore_ascii_case("QUILL") => format!(
"Missing required QUILL field. Found `{}:` at line {} — expected `QUILL:` (uppercase). Change the key to `QUILL` to register this fence as the document frontmatter.",
actual, line
),
Some((actual, line)) => format!(
"Missing required QUILL field. The first YAML key in the frontmatter must be `QUILL:` (found `{}:` at line {}). Reorder the frontmatter so `QUILL: <name>` is the first key.",
actual, line
),
None => "Missing required QUILL field. Add `QUILL: <name>` to the frontmatter.".to_string(),
}
}
pub(super) fn decompose(markdown: &str) -> Result<Document, crate::error::ParseError> {
decompose_with_warnings(markdown).map(|(doc, _)| doc)
}
pub(super) fn decompose_with_warnings(
markdown: &str,
) -> Result<(Document, Vec<Diagnostic>), crate::error::ParseError> {
let markdown = markdown.strip_prefix('\u{FEFF}').unwrap_or(markdown);
if markdown.len() > crate::error::MAX_INPUT_SIZE {
return Err(crate::error::ParseError::InputTooLarge {
size: markdown.len(),
max: crate::error::MAX_INPUT_SIZE,
});
}
let (blocks, warnings, first_fence_issue) = find_metadata_blocks(markdown)?;
if blocks.is_empty() {
return Err(crate::error::ParseError::InvalidStructure(
missing_quill_message(first_fence_issue),
));
}
let frontmatter_block = &blocks[0];
let quill_tag = frontmatter_block.quill_ref.clone().ok_or_else(|| {
ParseError::InvalidStructure(
"Missing required QUILL field. Add `QUILL: <name>` to the frontmatter.".to_string(),
)
})?;
let mut frontmatter: IndexMap<String, QuillValue> = IndexMap::new();
match &frontmatter_block.yaml_value {
Some(serde_json::Value::Object(mapping)) => {
for (key, value) in mapping {
frontmatter.insert(key.clone(), QuillValue::from_json(value.clone()));
}
}
Some(serde_json::Value::Null) | None => {}
Some(_) => {
return Err(ParseError::InvalidStructure(
"Invalid YAML frontmatter: expected a mapping".to_string(),
));
}
}
let body_start = blocks[0].end;
let body_end = blocks
.iter()
.skip(1)
.find(|b| b.tag.is_some())
.map(|b| b.start)
.unwrap_or(markdown.len());
let global_body = markdown[body_start..body_end].to_string();
let mut cards: Vec<Card> = Vec::new();
for (idx, block) in blocks.iter().enumerate() {
if let Some(ref tag_name) = block.tag {
let mut card_fields: IndexMap<String, QuillValue> = IndexMap::new();
match &block.yaml_value {
Some(serde_json::Value::Object(mapping)) => {
for (key, value) in mapping {
card_fields.insert(key.clone(), QuillValue::from_json(value.clone()));
}
}
Some(serde_json::Value::Null) | None => {}
Some(_) => {
return Err(crate::error::ParseError::InvalidStructure(format!(
"Invalid YAML in card block '{}': expected a mapping",
tag_name
)));
}
}
let card_body_start = block.end;
let card_body_end = if idx + 1 < blocks.len() {
blocks[idx + 1].start
} else {
markdown.len()
};
let card_body = markdown[card_body_start..card_body_end].to_string();
cards.push(Card::new_internal(tag_name.clone(), card_fields, card_body));
}
}
let quill_ref = QuillReference::from_str(&quill_tag).map_err(|e| {
ParseError::InvalidStructure(format!("Invalid QUILL tag '{}': {}", quill_tag, e))
})?;
let doc = Document::new_internal(quill_ref, frontmatter, global_body, cards, warnings.clone());
Ok((doc, warnings))
}