use super::error::{BinaryError, BinaryResult};
use super::mapping::LinoDocument;
use crate::{parse_lino_to_links, LiNo};
pub fn parse_document(text: &str) -> BinaryResult<LinoDocument> {
if text
.bytes()
.all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
{
return Ok(Vec::new());
}
let links =
parse_lino_to_links(text).map_err(|error| BinaryError::InvalidLino(error.to_string()))?;
Ok(links.into_iter().map(canonical).collect())
}
pub fn canonical(link: LiNo<String>) -> LiNo<String> {
match link {
LiNo::Link {
id: None,
mut values,
} if values.len() == 1 && matches!(values[0], LiNo::Ref(_)) => {
values.pop().expect("one value")
}
LiNo::Link { id, values } => LiNo::Link {
id,
values: values.into_iter().map(canonical).collect(),
},
reference => reference,
}
}
pub fn format_document(document: &[LiNo<String>]) -> String {
document
.iter()
.map(format_top_level)
.collect::<Vec<_>>()
.join("\n")
}
fn format_top_level(link: &LiNo<String>) -> String {
match link {
LiNo::Link { id: None, values } if values.len() >= 2 => join_values(values),
link => format_link(link),
}
}
pub fn format_link(link: &LiNo<String>) -> String {
match link {
LiNo::Ref(reference) => format_reference(reference),
LiNo::Link { id: None, values } => match values.as_slice() {
[LiNo::Ref(reference)] => format!("(({}))", format_reference(reference)),
values => format!("({})", join_values(values)),
},
LiNo::Link {
id: Some(id),
values,
} => {
if values.is_empty() {
format!("({}:)", format_reference(id))
} else {
format!("({}: {})", format_reference(id), join_values(values))
}
}
}
}
fn join_values(values: &[LiNo<String>]) -> String {
values.iter().map(format_link).collect::<Vec<_>>().join(" ")
}
pub fn format_reference(reference: &str) -> String {
let needs_quotes = reference.is_empty()
|| reference.starts_with('#')
|| reference.chars().any(|character| {
character.is_whitespace()
|| matches!(
character,
'\u{1c}'..='\u{1f}' | '\u{feff}' | '(' | ')' | ':' | '\'' | '"' | '`'
)
});
if !needs_quotes {
return reference.to_string();
}
let first = reference.chars().next();
let (quote, count) = ['\'', '"', '`']
.into_iter()
.filter(|"e| first != Some(quote))
.map(|quote| (quote, (longest_run(reference, quote) + 1) | 1))
.min_by_key(|&(_, count)| count)
.expect("a reference starts with at most one of three quote characters");
let delimiter = quote.to_string().repeat(count);
format!("{delimiter}{reference}{delimiter}")
}
fn longest_run(text: &str, quote: char) -> usize {
let (mut longest, mut current) = (0, 0);
for character in text.chars() {
current = if character == quote { current + 1 } else { 0 };
longest = longest.max(current);
}
longest
}