use crate::CompileError;
use serde_json::{Map, Value};
pub(crate) fn parse_yaml_frontmatter(raw: Option<&str>) -> Result<Option<Value>, CompileError> {
let raw = match raw {
Some(s) => s.trim(),
None => return Ok(None),
};
if raw.is_empty() {
return Ok(None);
}
let mut lines = raw.lines().peekable();
if let Some(Value::Object(map)) = parse_block(&mut lines, 0) {
if map.is_empty() {
Ok(None)
} else {
Ok(Some(Value::Object(map)))
}
} else {
Err(CompileError::InvalidFrontmatter("invalid block".into()))
}
}
fn parse_block(
lines: &mut std::iter::Peekable<std::str::Lines>,
current_indent: usize,
) -> Option<Value> {
let mut map = Map::new();
let mut list = Vec::new();
let mut is_list: Option<bool> = None;
while let Some(&line) = lines.peek() {
if line.trim().is_empty() {
lines.next();
continue;
}
let indent = get_indent(line);
if indent < current_indent {
break; }
if indent > current_indent {
break;
}
let line = lines.next().unwrap();
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("- ") {
if is_list == Some(false) {
return None;
}
is_list = Some(true);
let val_str = rest.trim();
if val_str.is_empty() {
if let Some(&next) = lines.peek() {
let next_ind = get_indent(next);
if next_ind > current_indent {
if let Some(v) = parse_block(lines, next_ind) {
list.push(v);
}
} else {
list.push(Value::Null);
}
}
} else {
list.push(parse_scalar(val_str));
}
} else if let Some((key, val)) = split_key_val(trimmed) {
if is_list == Some(true) {
return None;
}
is_list = Some(false);
let key = key.to_string();
if val.is_empty() {
if let Some(&next) = lines.peek() {
let next_ind = get_indent(next);
if next_ind > current_indent {
if let Some(v) = parse_block(lines, next_ind) {
map.insert(key, v);
}
} else {
map.insert(key, Value::Null);
}
} else {
map.insert(key, Value::Null);
}
} else {
map.insert(key, parse_scalar(val));
}
} else {
return None;
}
}
match is_list {
Some(true) => Some(Value::Array(list)),
Some(false) => Some(Value::Object(map)),
None => None,
}
}
fn get_indent(s: &str) -> usize {
s.chars().take_while(|c| *c == ' ').count()
}
fn split_key_val(s: &str) -> Option<(&str, &str)> {
let colon = s.find(':')?;
if colon == s.len() - 1 || s[colon + 1..].starts_with(' ') {
Some((s[..colon].trim(), s[colon + 1..].trim()))
} else {
None
}
}
fn parse_scalar(s: &str) -> Value {
if s == "true" {
Value::Bool(true)
} else if s == "false" {
Value::Bool(false)
} else if s.starts_with('[') && s.ends_with(']') {
let inner = &s[1..s.len() - 1];
let items: Vec<Value> = inner.split(',').map(|x| parse_scalar(x.trim())).collect();
Value::Array(items)
} else if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\''))
{
Value::String(s[1..s.len() - 1].to_string())
} else if let Ok(i) = s.parse::<i64>() {
Value::Number(i.into())
} else if let Ok(f) = s.parse::<f64>() {
if let Some(n) = serde_json::Number::from_f64(f) {
Value::Number(n)
} else {
Value::String(s.to_string())
}
} else {
Value::String(s.to_string())
}
}
pub fn parse_yaml_str(yaml_text: &str) -> Result<Value, String> {
match parse_yaml_frontmatter(Some(yaml_text)) {
Ok(Some(v)) => Ok(v),
Ok(None) => Ok(Value::Null),
Err(e) => Err(e.to_string()),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn read_yaml_file(path: &std::path::Path) -> Result<Value, String> {
let s = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
parse_yaml_str(&s)
}