use super::{LiNo, parse_lino_to_links};
pub use crate::readable::{format_value_single_line, format_value_verbatim, quote, unescape};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormatError {
MissingField(String),
InvalidInput(String),
}
impl std::fmt::Display for FormatError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FormatError::MissingField(field) => write!(f, "Missing required field: {}", field),
FormatError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
}
}
}
impl std::error::Error for FormatError {}
pub fn escape_reference(value: &str) -> String {
let needs_escaping = value
.chars()
.any(|c| c.is_whitespace() || c == '(' || c == ')' || c == '\'' || c == '"' || c == ':')
|| value.contains('\n');
if !needs_escaping {
return value.to_string();
}
let has_single = value.contains('\'');
let has_double = value.contains('"');
if has_single && !has_double {
return format!("\"{}\"", value);
}
if has_double && !has_single {
return format!("'{}'", value);
}
if has_single && has_double {
let single_count = value.chars().filter(|&c| c == '\'').count();
let double_count = value.chars().filter(|&c| c == '"').count();
if double_count < single_count {
let escaped = value.replace('"', "\"\"");
return format!("\"{}\"", escaped);
}
let escaped = value.replace('\'', "''");
return format!("'{}'", escaped);
}
format!("'{}'", value)
}
pub fn unescape_reference(s: &str) -> String {
s.replace("\"\"", "\"").replace("''", "'")
}
fn format_indented_value(value: &str) -> String {
let has_single = value.contains('\'');
let has_double = value.contains('"');
if has_double && !has_single {
return format!("'{}'", value);
}
if has_single && !has_double {
return format!("\"{}\"", value);
}
if has_single && has_double {
let escaped = value.replace('\'', "''");
return format!("'{}'", escaped);
}
format!("\"{}\"", value)
}
pub fn format_indented<S: ::std::hash::BuildHasher>(
id: &str,
obj: &HashMap<String, String, S>,
indent: &str,
) -> Result<String, FormatError> {
if id.is_empty() {
return Err(FormatError::MissingField("id".to_string()));
}
let mut lines = vec![id.to_string()];
for (key, value) in obj {
let escaped_key = escape_reference(key);
let formatted_value = format_indented_value(value);
lines.push(format!("{}{} {}", indent, escaped_key, formatted_value));
}
Ok(lines.join("\n"))
}
pub fn format_indented_ordered(
id: &str,
pairs: &[(&str, &str)],
indent: &str,
) -> Result<String, FormatError> {
if id.is_empty() {
return Err(FormatError::MissingField("id".to_string()));
}
let mut lines = vec![id.to_string()];
for (key, value) in pairs {
let escaped_key = escape_reference(key);
let formatted_value = format_indented_value(value);
lines.push(format!("{}{} {}", indent, escaped_key, formatted_value));
}
Ok(lines.join("\n"))
}
pub fn parse_indented(text: &str) -> Result<(String, HashMap<String, String>), FormatError> {
if text.is_empty() {
return Err(FormatError::InvalidInput(
"text is required for parse_indented".to_string(),
));
}
let lines: Vec<&str> = text.lines().collect();
if lines.is_empty() {
return Err(FormatError::InvalidInput(
"text must have at least one line (the identifier)".to_string(),
));
}
let non_empty_lines: Vec<&str> = lines
.iter()
.filter(|l| !l.trim().is_empty())
.copied()
.collect();
if non_empty_lines.is_empty() {
return Err(FormatError::InvalidInput(
"text must have at least one non-empty line (the identifier)".to_string(),
));
}
let first_line = non_empty_lines[0].trim();
let lino_text = if first_line.ends_with(':') {
non_empty_lines.join("\n")
} else {
format!("{}:\n{}", first_line, non_empty_lines[1..].join("\n"))
};
let parsed = parse_lino_to_links(&lino_text)
.map_err(|e| FormatError::InvalidInput(format!("Parse error: {:?}", e)))?;
if parsed.is_empty() {
return Err(FormatError::InvalidInput(
"Failed to parse indented Links Notation".to_string(),
));
}
let main_link = &parsed[0];
let (result_id, values) = match main_link {
LiNo::Link { id, values } => (id.clone().unwrap_or_default(), values),
LiNo::Ref(id) => (id.clone(), &vec![]),
};
let mut obj = HashMap::new();
for child in values {
if let LiNo::Link {
values: child_values,
..
} = child
{
if child_values.len() == 2 {
let key_ref = &child_values[0];
let value_ref = &child_values[1];
let key = match key_ref {
LiNo::Ref(k) => k.clone(),
LiNo::Link { id, .. } => id.clone().unwrap_or_default(),
};
let value = match value_ref {
LiNo::Ref(v) => v.clone(),
LiNo::Link { id, .. } => id.clone().unwrap_or_default(),
};
obj.insert(key, value);
}
}
}
Ok((result_id, obj))
}