use crate::format::Format;
use crate::formats::lex::formatting_rules::FormattingRules;
use crate::formats::lex::LexFormat;
use lex_core::lex::ast::elements::typed_content::ContentElement;
use lex_core::lex::ast::{ContentItem, Document, List, ListItem, Session};
pub fn serialize_to_lex(doc: &Document) -> Result<String, String> {
let format = LexFormat::default();
format.serialize(doc).map_err(|e| e.to_string())
}
pub fn serialize_to_lex_with_rules(
doc: &Document,
rules: FormattingRules,
) -> Result<String, String> {
let format = LexFormat::new(rules);
format.serialize(doc).map_err(|e| e.to_string())
}
pub fn format_lex_source(source: &str) -> Result<String, String> {
use lex_core::lex::transforms::standard::STRING_TO_AST;
let mut doc = STRING_TO_AST
.run(source.to_string())
.map_err(|e| e.to_string())?;
normalize_footnotes(&mut doc);
serialize_to_lex(&doc)
}
fn normalize_footnotes(doc: &mut Document) {
if let Some(ContentItem::Session(last_session)) = doc.root.children.as_mut_vec().last_mut() {
let title = last_session.title.as_string();
if title.trim().eq_ignore_ascii_case("Notes")
|| title.trim().eq_ignore_ascii_case("Footnotes")
{
convert_session_notes_to_list(last_session);
}
}
}
fn convert_session_notes_to_list(session: &mut Session) {
let has_legacy_content = session.children.iter().any(|c| match c {
ContentItem::Session(s) => split_numbered_title(s.title.as_string()).is_some(),
ContentItem::List(_) | ContentItem::BlankLineGroup(_) => true,
_ => false,
});
if !has_legacy_content {
return;
}
let mut new_children = Vec::new();
let mut current_list_items = Vec::new();
let children_vec = session.children.as_mut_vec();
let old_children = std::mem::take(children_vec);
for mut child in old_children {
let mut handled = false;
if let ContentItem::Session(inner_session) = &child {
let title = inner_session.title.as_string();
if let Some((number_part, content_part)) = split_numbered_title(title) {
handled = true;
let mut children_elements = Vec::new();
for inner_child in inner_session.children.iter().cloned() {
if let Ok(el) = ContentElement::try_from(inner_child) {
children_elements.push(el);
}
}
let list_item = ListItem::with_content(
number_part.to_string(),
content_part.trim().to_string(),
children_elements,
);
current_list_items.push(list_item);
}
} else if let ContentItem::List(l) = &mut child {
handled = true;
let items = std::mem::take(l.items.as_mut_vec());
for item in items {
if let ContentItem::ListItem(li) = item {
current_list_items.push(li);
}
}
} else if let ContentItem::BlankLineGroup(_) = child {
handled = true;
}
if !handled {
if !current_list_items.is_empty() {
new_children.push(ContentItem::List(List::new(std::mem::take(
&mut current_list_items,
))));
}
new_children.push(child);
}
}
if !current_list_items.is_empty() {
new_children.push(ContentItem::List(List::new(current_list_items)));
}
*session.children.as_mut_vec() = new_children;
}
fn split_numbered_title(title: &str) -> Option<(&str, &str)> {
let title = title.trim();
let number_len = title.chars().take_while(|c| c.is_ascii_digit()).count();
if number_len > 0 && title.chars().nth(number_len) == Some('.') {
let (num, rest) = title.split_at(number_len + 1);
return Some((num, rest));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use lex_core::lex::ast::Paragraph;
#[test]
fn test_serialize_to_lex() {
let doc = Document::with_content(vec![ContentItem::Paragraph(Paragraph::from_line(
"Test".to_string(),
))]);
let result = serialize_to_lex(&doc);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Test\n");
}
#[test]
fn test_serialize_with_custom_rules() {
let doc = Document::with_content(vec![ContentItem::Paragraph(Paragraph::from_line(
"Test".to_string(),
))]);
let rules = FormattingRules {
indent_string: " ".to_string(),
..Default::default()
};
let result = serialize_to_lex_with_rules(&doc, rules);
assert!(result.is_ok());
}
#[test]
fn test_format_lex_source() {
let source = "Hello world\n";
let formatted = format_lex_source(source);
assert!(formatted.is_ok());
assert_eq!(formatted.unwrap(), "Hello world\n");
}
#[test]
fn test_round_trip_simple() {
let original = "Introduction\n\n This is a session.\n";
let formatted = format_lex_source(original).unwrap();
use lex_core::lex::transforms::standard::STRING_TO_AST;
let doc1 = STRING_TO_AST.run(original.to_string()).unwrap();
let doc2 = STRING_TO_AST.run(formatted.clone()).unwrap();
assert_eq!(doc1.root.children.len(), doc2.root.children.len());
}
#[test]
fn test_normalize_footnotes() {
let original = "Title\n\n Content\n\nNotes\n\n 1. Note One\n\n 2. Note Two\n";
let formatted = format_lex_source(original).unwrap();
use lex_core::lex::transforms::standard::STRING_TO_AST;
let doc = STRING_TO_AST.run(formatted.clone()).unwrap();
let last_session = doc.root.children.last().unwrap();
if let ContentItem::Session(s) = last_session {
assert_eq!(s.title.as_string().trim(), "Notes");
assert_eq!(s.children.len(), 1);
if let ContentItem::List(l) = &s.children[0] {
assert_eq!(l.items.len(), 2);
if let ContentItem::ListItem(item) = &l.items[0] {
assert_eq!(item.marker().trim(), "1.");
assert_eq!(item.text().trim(), "Note One");
} else {
panic!("Expected ListItem, found {:?}", l.items[0]);
}
} else {
panic!("Expected List, found {:?}", s.children[0]);
}
} else {
panic!("Expected Session");
}
}
}