use super::{FrontMatterFormat, Metadata, ParseLimits};
use crate::error::MetadataError;
use crate::MetadataMap;
use alloc::format;
#[cfg(any(feature = "yaml", feature = "toml", feature = "json"))]
use alloc::string::String;
use alloc::string::ToString;
#[cfg(any(feature = "yaml", feature = "toml", feature = "json"))]
use alloc::vec::Vec;
#[cfg(any(feature = "yaml", feature = "json"))]
use core::ops::Range;
pub(super) fn parse_block(
format: FrontMatterFormat,
raw: &str,
limits: &ParseLimits,
) -> Result<Metadata, MetadataError> {
check_size(raw, limits)?;
let map: Result<MetadataMap, MetadataError> = match format {
#[cfg(feature = "yaml")]
FrontMatterFormat::Yaml => yaml::flatten_block(raw, limits),
#[cfg(feature = "toml")]
FrontMatterFormat::Toml => toml_fm::flatten_block(raw),
#[cfg(feature = "json")]
FrontMatterFormat::Json => json::flatten_block(raw),
#[cfg(not(all(
feature = "yaml",
feature = "toml",
feature = "json"
)))]
other => Err(MetadataError::UnsupportedFormatError(
other.to_string(),
)),
};
map.map(Metadata::new)
}
pub(super) fn deserialize_block<
T: serde::de::DeserializeOwned + 'static,
>(
format: FrontMatterFormat,
raw: &str,
limits: &ParseLimits,
) -> Result<T, MetadataError> {
check_size(raw, limits)?;
match format {
#[cfg(feature = "yaml")]
FrontMatterFormat::Yaml => yaml::deserialize(raw, limits),
#[cfg(feature = "toml")]
FrontMatterFormat::Toml => toml_fm::deserialize(raw),
#[cfg(feature = "json")]
FrontMatterFormat::Json => json::deserialize(raw),
#[cfg(not(all(
feature = "yaml",
feature = "toml",
feature = "json"
)))]
other => Err(MetadataError::UnsupportedFormatError(
other.to_string(),
)),
}
}
pub(super) fn deserialize_borrowed<'a, T: serde::Deserialize<'a>>(
format: FrontMatterFormat,
raw: &'a str,
limits: &ParseLimits,
) -> Result<T, MetadataError> {
check_size(raw, limits)?;
match format {
#[cfg(feature = "yaml")]
FrontMatterFormat::Yaml => {
yaml::deserialize_borrowed(raw, limits)
}
#[cfg(feature = "toml")]
FrontMatterFormat::Toml => toml_fm::deserialize(raw),
#[cfg(feature = "json")]
FrontMatterFormat::Json => json::deserialize(raw),
#[cfg(not(all(
feature = "yaml",
feature = "toml",
feature = "json"
)))]
other => Err(MetadataError::UnsupportedFormatError(
other.to_string(),
)),
}
}
fn check_size(
raw: &str,
limits: &ParseLimits,
) -> Result<(), MetadataError> {
if raw.len() > limits.max_block_bytes {
return Err(MetadataError::ExtractionError {
message: format!(
"Front matter block of {} bytes exceeds the {}-byte limit.",
raw.len(),
limits.max_block_bytes
),
});
}
Ok(())
}
#[cfg(any(feature = "yaml", feature = "json"))]
fn line_col_span(
text: &str,
line: usize,
column: usize,
) -> Range<usize> {
let mut offset = 0;
for (i, l) in text.split('\n').enumerate() {
if i + 1 == line.max(1) {
offset += column.saturating_sub(1).min(l.len());
break;
}
offset += l.len() + 1;
}
let start = offset.min(text.len());
start..(start + 1).min(text.len()).max(start)
}
#[cfg(any(feature = "yaml", feature = "toml", feature = "json"))]
fn join_key(prefix: &str, key: &str) -> String {
if prefix.is_empty() {
key.to_string()
} else {
format!("{}.{}", prefix, key)
}
}
#[cfg(any(feature = "yaml", feature = "toml", feature = "json"))]
fn inline_list<I: Iterator<Item = String>>(items: I) -> String {
format!("[{}]", items.collect::<Vec<String>>().join(", "))
}
#[cfg(feature = "yaml")]
pub(super) mod yaml {
use super::*;
use noyalib::ParserConfig;
fn config(limits: &ParseLimits) -> ParserConfig {
ParserConfig::strict()
.max_depth(limits.max_depth)
.max_document_length(limits.max_block_bytes.max(1))
}
fn parse_error(text: &str, e: noyalib::Error) -> MetadataError {
let span = e
.location()
.map(|loc| line_col_span(text, loc.line(), loc.column()));
MetadataError::parse(FrontMatterFormat::Yaml, span, e)
}
pub(in crate::metadata) fn flatten_block(
raw: &str,
limits: &ParseLimits,
) -> Result<MetadataMap, MetadataError> {
let collapsed = collapse_multiline_quoted_scalars(raw);
let value: noyalib::Value =
noyalib::from_str_with_config(&collapsed, &config(limits))
.map_err(|e| parse_error(&collapsed, e))?;
Ok(flatten_yaml(&value))
}
pub(in crate::metadata) fn deserialize<
T: serde::de::DeserializeOwned + 'static,
>(
raw: &str,
limits: &ParseLimits,
) -> Result<T, MetadataError> {
let collapsed = collapse_multiline_quoted_scalars(raw);
noyalib::from_str_with_config::<T>(&collapsed, &config(limits))
.map_err(|e| parse_error(&collapsed, e))
}
pub(in crate::metadata) fn deserialize_borrowed<
'a,
T: serde::Deserialize<'a>,
>(
raw: &'a str,
limits: &ParseLimits,
) -> Result<T, MetadataError> {
noyalib::from_str_borrowing_with_config::<T>(
raw,
&config(limits),
)
.map_err(|e| parse_error(raw, e))
}
pub(in crate::metadata) fn collapse_multiline_quoted_scalars(
block: &str,
) -> String {
let mut out = String::with_capacity(block.len());
let lines: Vec<&str> = block.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
match open_quoted_scalar(line) {
Some(head) => {
let (joined, next) =
join_quoted_continuation(head, &lines, i + 1);
out.push_str(&joined);
i = next;
}
None => {
out.push_str(line);
i += 1;
}
}
out.push('\n');
}
out
}
fn open_quoted_scalar(line: &str) -> Option<&str> {
let eq_pos = line.find(": \"")?;
let after_quote = &line[eq_pos + 3..];
after_quote.trim().is_empty().then(|| &line[..eq_pos + 3])
}
fn join_quoted_continuation(
head: &str,
lines: &[&str],
mut i: usize,
) -> (String, usize) {
let mut joined = String::from(head);
while i < lines.len() {
let next = lines[i];
i += 1;
if let Some(close) = next.find('"') {
joined.push_str(next[..close].trim_start());
joined.push_str(&next[close..]);
return (joined, i);
}
joined.push_str(next.trim_start());
joined.push(' ');
}
(joined.trim_end().to_string(), i)
}
pub(in crate::metadata) fn flatten_yaml(
value: &noyalib::Value,
) -> MetadataMap {
let mut map = MetadataMap::new();
if !value.is_null() {
flatten_yaml_recursive(value, String::new(), &mut map);
}
map
}
pub(in crate::metadata) fn flatten_yaml_recursive(
value: &noyalib::Value,
prefix: String,
map: &mut MetadataMap,
) {
match value {
noyalib::Value::Mapping(m) => {
for (k, v) in m {
flatten_yaml_recursive(
v,
join_key(&prefix, k.as_str()),
map,
);
}
}
noyalib::Value::Sequence(seq) => {
map.insert(
prefix,
inline_list(seq.iter().map(yaml_scalar)),
);
}
_ => {
map.insert(prefix, yaml_scalar(value));
}
}
}
fn yaml_scalar(value: &noyalib::Value) -> String {
match value.as_str() {
Some(s) => s.to_string(),
None => value.to_string(),
}
}
}
#[cfg(feature = "toml")]
pub(super) mod toml_fm {
use super::*;
use toml::Value as TomlValue;
fn parse_error(e: toml::de::Error) -> MetadataError {
let span = e.span();
MetadataError::parse(FrontMatterFormat::Toml, span, e)
}
pub(in crate::metadata) fn flatten_block(
raw: &str,
) -> Result<MetadataMap, MetadataError> {
let value: TomlValue =
toml::from_str(raw).map_err(parse_error)?;
let mut map = MetadataMap::new();
flatten_toml(&value, &mut map, String::new());
Ok(map)
}
pub(in crate::metadata) fn deserialize<
'a,
T: serde::Deserialize<'a>,
>(
raw: &'a str,
) -> Result<T, MetadataError> {
toml::from_str::<T>(raw).map_err(parse_error)
}
pub(in crate::metadata) fn flatten_toml(
value: &TomlValue,
map: &mut MetadataMap,
prefix: String,
) {
match value {
TomlValue::Table(table) => {
for (k, v) in table {
flatten_toml(v, map, join_key(&prefix, k));
}
}
TomlValue::Array(arr) => {
map.insert(
prefix,
inline_list(arr.iter().map(toml_scalar)),
);
}
_ => {
map.insert(prefix, toml_scalar(value));
}
}
}
fn toml_scalar(value: &TomlValue) -> String {
match value {
TomlValue::String(s) => s.clone(),
TomlValue::Datetime(dt) => dt.to_string(),
other => other.to_string(),
}
}
}
#[cfg(feature = "json")]
pub(super) mod json {
use super::*;
use serde_json::Value as JsonValue;
fn parse_error(text: &str, e: serde_json::Error) -> MetadataError {
let span = Some(line_col_span(text, e.line(), e.column()));
MetadataError::parse(FrontMatterFormat::Json, span, e)
}
pub(in crate::metadata) fn flatten_block(
raw: &str,
) -> Result<MetadataMap, MetadataError> {
let object: serde_json::Map<String, JsonValue> =
serde_json::from_str(raw)
.map_err(|e| parse_error(raw, e))?;
let mut map = MetadataMap::new();
for (k, v) in object {
flatten_json(&v, &mut map, k);
}
Ok(map)
}
pub(in crate::metadata) fn deserialize<
'a,
T: serde::Deserialize<'a>,
>(
raw: &'a str,
) -> Result<T, MetadataError> {
serde_json::from_str::<T>(raw).map_err(|e| parse_error(raw, e))
}
pub(in crate::metadata) fn flatten_json(
value: &JsonValue,
map: &mut MetadataMap,
prefix: String,
) {
match value {
JsonValue::Object(obj) => {
for (k, v) in obj {
flatten_json(v, map, join_key(&prefix, k));
}
}
JsonValue::Array(arr) => {
map.insert(
prefix,
inline_list(arr.iter().map(json_scalar)),
);
}
_ => {
map.insert(prefix, json_scalar(value));
}
}
}
fn json_scalar(value: &JsonValue) -> String {
match value {
JsonValue::String(s) => s.clone(),
JsonValue::Null => "null".to_string(),
other => other.to_string(),
}
}
}