use lex_core::lex::ast::elements::{
typed_content, verbatim::VerbatimBlockMode, Annotation as LexAnnotation, ContentElement,
ContentItem as LexContentItem, Definition as LexDefinition, Label, List as LexList,
ListItem as LexListItem, Paragraph as LexParagraph, Session as LexSession,
Verbatim as LexVerbatim, VerbatimContent, VerbatimLine as LexVerbatimLine,
};
use lex_core::lex::ast::range::Position;
use lex_core::lex::ast::{Data, Document as LexDocument, Parameter, Range, TextContent};
use super::nodes::{
Annotation, Definition, DocNode, Document, Heading, InlineContent, List, ListItem, Paragraph,
Table, TableCell, TableRow, Verbatim,
};
pub fn to_lex_document(doc: &Document) -> LexDocument {
let mut children = Vec::new();
for node in &doc.children {
children.extend(to_lex_content_items(node, 1));
}
let mut lex_doc = LexDocument::with_content(children);
if let Some(title_inlines) = &doc.title {
let title_text = inline_content_to_text(title_inlines);
if !title_text.is_empty() {
use lex_core::lex::ast::elements::document::DocumentTitle;
let title_tc = TextContent::from_string(title_text, None);
let subtitle_tc = doc.subtitle.as_ref().map(|sub_inlines| {
TextContent::from_string(inline_content_to_text(sub_inlines), None)
});
lex_doc.title = Some(match subtitle_tc {
Some(sub) => DocumentTitle::with_subtitle(title_tc, sub, Range::default()),
None => DocumentTitle::new(title_tc, Range::default()),
});
}
}
lex_doc
}
fn to_lex_content_items(node: &DocNode, level: usize) -> Vec<LexContentItem> {
match node {
DocNode::Document(_) => {
vec![]
}
DocNode::Heading(heading) => vec![to_lex_session(heading, level)],
DocNode::Paragraph(para) => vec![to_lex_paragraph(para)],
DocNode::List(list) => vec![to_lex_list(list)],
DocNode::ListItem(item) => vec![to_lex_list_item(item)],
DocNode::Definition(def) => vec![to_lex_definition(def)],
DocNode::Verbatim(verb) => vec![to_lex_verbatim(verb)],
DocNode::Annotation(ann) => vec![to_lex_annotation(ann, level)],
DocNode::Table(table) => vec![to_lex_table(table, level)],
DocNode::Image(_) | DocNode::Video(_) | DocNode::Audio(_) => vec![to_lex_media(node)],
DocNode::Inline(_) => {
vec![]
}
}
}
fn to_lex_session(heading: &Heading, level: usize) -> LexContentItem {
let title_text = inline_content_to_text(&heading.content);
let title = TextContent::from_string(title_text, None);
let mut children = Vec::new();
for child in &heading.children {
children.extend(to_lex_content_items(child, level + 1));
}
let session_children = typed_content::into_session_contents(children);
LexContentItem::Session(LexSession::new(title, session_children))
}
fn to_lex_table(table: &Table, level: usize) -> LexContentItem {
let registry = crate::common::verbatim::VerbatimRegistry::default_with_standard();
let node = DocNode::Table(table.clone());
if let Some(handler) = registry.get("doc.table") {
if let Some((content, params)) = handler.convert_from_ir(&node) {
let label = Label::new("doc.table".to_string());
let parameters = params
.into_iter()
.map(|(k, v)| Parameter {
key: k,
value: v,
location: default_range(),
})
.collect();
let subject = TextContent::from_string("".to_string(), None);
let lines = content
.lines()
.map(|l| VerbatimContent::VerbatimLine(LexVerbatimLine::new(l.to_string())))
.collect();
let closing_data = Data::new(label, parameters);
return LexContentItem::VerbatimBlock(Box::new(LexVerbatim::new(
subject,
lines,
closing_data,
VerbatimBlockMode::Inflow,
)));
}
}
let label = Label::new("table".to_string());
let parameters = Vec::new();
let mut children = Vec::new();
if !table.header.is_empty() {
let thead_label = Label::new("thead".to_string());
let mut thead_rows = Vec::new();
for row in &table.header {
thead_rows.push(to_lex_table_row(row, level + 1));
}
let thead = LexContentItem::Annotation(LexAnnotation::new(
thead_label,
Vec::new(),
to_content_elements(thead_rows),
));
children.push(thead);
}
let tbody_label = Label::new("tbody".to_string());
let mut tbody_rows = Vec::new();
for row in &table.rows {
tbody_rows.push(to_lex_table_row(row, level + 1));
}
let tbody = LexContentItem::Annotation(LexAnnotation::new(
tbody_label,
Vec::new(),
to_content_elements(tbody_rows),
));
children.push(tbody);
LexContentItem::Annotation(LexAnnotation::new(
label,
parameters,
to_content_elements(children),
))
}
fn to_lex_table_row(row: &TableRow, level: usize) -> LexContentItem {
let label = Label::new("tr".to_string());
let mut cells = Vec::new();
for cell in &row.cells {
cells.push(to_lex_table_cell(cell, level + 1));
}
LexContentItem::Annotation(LexAnnotation::new(
label,
Vec::new(),
to_content_elements(cells),
))
}
fn to_lex_table_cell(cell: &TableCell, level: usize) -> LexContentItem {
let label_str = if cell.header { "th" } else { "td" };
let label = Label::new(label_str.to_string());
let mut parameters = Vec::new();
let align_val = match cell.align {
crate::ir::nodes::TableCellAlignment::Left => Some("left"),
crate::ir::nodes::TableCellAlignment::Center => Some("center"),
crate::ir::nodes::TableCellAlignment::Right => Some("right"),
crate::ir::nodes::TableCellAlignment::None => None,
};
if let Some(align) = align_val {
parameters.push(Parameter {
key: "align".to_string(),
value: align.to_string(),
location: default_range(),
});
}
let mut content = Vec::new();
for child in &cell.content {
content.extend(to_lex_content_items(child, level + 1));
}
LexContentItem::Annotation(LexAnnotation::new(
label,
parameters,
to_content_elements(content),
))
}
fn to_lex_paragraph(para: &Paragraph) -> LexContentItem {
let text = inline_content_to_text(¶.content);
LexContentItem::Paragraph(LexParagraph::from_line(text))
}
fn to_lex_list(list: &List) -> LexContentItem {
let items: Vec<LexListItem> = list
.items
.iter()
.enumerate()
.map(|(i, item)| to_lex_list_item_with_style(item, &list.style, i + 1))
.collect();
LexContentItem::List(LexList::new(items))
}
fn to_lex_list_item_with_style(
item: &ListItem,
style: &super::nodes::ListStyle,
index: usize,
) -> LexListItem {
let marker = format_marker_for_style(style, index);
let text = inline_content_to_text(&item.content);
let mut child_items = Vec::new();
for child in &item.children {
child_items.extend(to_lex_content_items(child, 1));
}
let children = to_content_elements(child_items);
LexListItem::with_content(marker, text, children)
}
fn format_marker_for_style(style: &super::nodes::ListStyle, index: usize) -> String {
use super::nodes::ListStyle;
match style {
ListStyle::Bullet => "-".to_string(),
ListStyle::Numeric => format!("{index}."),
ListStyle::AlphaLower => {
let c = if (1..=26).contains(&index) {
char::from_u32((index as u32) + 96).unwrap()
} else {
return format!("{index}.");
};
format!("{c}.")
}
ListStyle::AlphaUpper => {
let c = if (1..=26).contains(&index) {
char::from_u32((index as u32) + 64).unwrap()
} else {
return format!("{index}.");
};
format!("{c}.")
}
ListStyle::RomanLower => {
let roman = to_roman_lower(index);
format!("{roman}.")
}
ListStyle::RomanUpper => {
let roman = to_roman_upper(index);
format!("{roman}.")
}
}
}
fn to_roman_lower(n: usize) -> String {
to_roman_upper(n).to_lowercase()
}
fn to_roman_upper(n: usize) -> String {
match n {
1 => "I",
2 => "II",
3 => "III",
4 => "IV",
5 => "V",
6 => "VI",
7 => "VII",
8 => "VIII",
9 => "IX",
10 => "X",
11 => "XI",
12 => "XII",
13 => "XIII",
14 => "XIV",
15 => "XV",
16 => "XVI",
17 => "XVII",
18 => "XVIII",
19 => "XIX",
20 => "XX",
_ => return n.to_string(),
}
.to_string()
}
fn to_lex_list_item(item: &ListItem) -> LexContentItem {
LexContentItem::ListItem(to_lex_list_item_struct(item))
}
fn to_lex_list_item_struct(item: &ListItem) -> LexListItem {
let marker = "-".to_string();
let text = inline_content_to_text(&item.content);
let mut child_items = Vec::new();
for child in &item.children {
child_items.extend(to_lex_content_items(child, 1));
}
let children = to_content_elements(child_items);
LexListItem::with_content(marker, text, children)
}
fn to_lex_definition(def: &Definition) -> LexContentItem {
let term_text = inline_content_to_text(&def.term);
let term = TextContent::from_string(term_text, None);
let mut child_items = Vec::new();
for child in &def.description {
child_items.extend(to_lex_content_items(child, 1));
}
let children = to_content_elements(child_items);
LexContentItem::Definition(LexDefinition::new(term, children))
}
fn to_lex_verbatim(verb: &Verbatim) -> LexContentItem {
let subject_text = verb.subject.clone().unwrap_or_default();
let subject = TextContent::from_string(subject_text, None);
let lines: Vec<VerbatimContent> = verb
.content
.lines()
.map(|line| VerbatimContent::VerbatimLine(LexVerbatimLine::new(line.to_string())))
.collect();
let label_text = verb.language.clone().unwrap_or_default();
let label = Label::new(label_text);
let closing_data = Data::new(label, Vec::new());
LexContentItem::VerbatimBlock(Box::new(LexVerbatim::new(
subject,
lines,
closing_data,
VerbatimBlockMode::Inflow,
)))
}
fn to_lex_annotation(ann: &Annotation, level: usize) -> LexContentItem {
let label = Label::new(ann.label.clone());
let parameters: Vec<Parameter> = ann
.parameters
.iter()
.map(|(k, v)| Parameter {
key: k.clone(),
value: v.clone(),
location: default_range(),
})
.collect();
let mut child_items = Vec::new();
for child in &ann.content {
child_items.extend(to_lex_content_items(child, level));
}
let children = to_content_elements(child_items);
LexContentItem::Annotation(LexAnnotation::new(label, parameters, children))
}
fn inline_content_to_text(content: &[InlineContent]) -> String {
content
.iter()
.map(|inline| match inline {
InlineContent::Text(text) => text.clone(),
InlineContent::Bold(children) => {
format!("*{}*", inline_content_to_text(children))
}
InlineContent::Italic(children) => {
format!("_{}_", inline_content_to_text(children))
}
InlineContent::Code(code) => format!("`{code}`"),
InlineContent::Math(math) => format!("#{math}#"),
InlineContent::Reference(ref_text) => format!("[{ref_text}]"),
InlineContent::Link { text, href } => format!("{text} [{href}]"),
InlineContent::Image(image) => {
let mut text = format!("", image.alt, image.src);
if let Some(title) = &image.title {
text.push_str(&format!(" \"{title}\""));
}
text
}
})
.collect()
}
fn to_content_elements(items: Vec<LexContentItem>) -> Vec<ContentElement> {
items
.into_iter()
.filter_map(|item| item.try_into().ok())
.collect()
}
fn default_range() -> Range {
Range::new(0..0, Position::new(0, 0), Position::new(0, 0))
}
fn to_lex_media(node: &DocNode) -> LexContentItem {
let registry = crate::common::verbatim::VerbatimRegistry::default_with_standard();
let label = match node {
DocNode::Image(_) => "doc.image",
DocNode::Video(_) => "doc.video",
DocNode::Audio(_) => "doc.audio",
_ => return LexContentItem::Paragraph(LexParagraph::new(vec![])),
};
if let Some(handler) = registry.get(label) {
if let Some((content, params)) = handler.convert_from_ir(node) {
let label = Label::new(label.to_string());
let parameters = params
.into_iter()
.map(|(k, v)| Parameter {
key: k,
value: v,
location: default_range(),
})
.collect();
let subject = TextContent::from_string("".to_string(), None);
let lines = content
.lines()
.map(|l| VerbatimContent::VerbatimLine(LexVerbatimLine::new(l.to_string())))
.collect();
let closing_data = Data::new(label, parameters);
return LexContentItem::VerbatimBlock(Box::new(LexVerbatim::new(
subject,
lines,
closing_data,
VerbatimBlockMode::Inflow,
)));
}
}
LexContentItem::Paragraph(LexParagraph::new(vec![]))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::nodes::*;
#[test]
fn test_paragraph_to_lex() {
let ir_para = Paragraph {
content: vec![InlineContent::Text("Hello world".to_string())],
};
let lex_item = to_lex_paragraph(&ir_para);
match lex_item {
LexContentItem::Paragraph(para) => {
assert_eq!(para.text(), "Hello world");
}
_ => panic!("Expected Paragraph"),
}
}
#[test]
fn test_heading_to_session() {
let ir_heading = Heading {
level: 1,
content: vec![InlineContent::Text("Test".to_string())],
children: vec![],
};
let lex_item = to_lex_session(&ir_heading, 1);
match lex_item {
LexContentItem::Session(session) => {
assert!(session.title.as_string().contains("Test"));
}
_ => panic!("Expected Session"),
}
}
#[test]
fn test_list_to_lex() {
let ir_list = List {
items: vec![
ListItem {
content: vec![InlineContent::Text("Item 1".to_string())],
children: vec![],
},
ListItem {
content: vec![InlineContent::Text("Item 2".to_string())],
children: vec![],
},
],
ordered: false,
style: ListStyle::Bullet,
form: ListForm::Short,
};
let lex_item = to_lex_list(&ir_list);
match lex_item {
LexContentItem::List(list) => {
assert!(!list.items.is_empty());
}
_ => panic!("Expected List"),
}
}
#[test]
fn test_verbatim_with_language() {
let ir_verb = Verbatim {
subject: None,
language: Some("rust".to_string()),
content: "fn main() {}\nlet x = 1;".to_string(),
};
let lex_item = to_lex_verbatim(&ir_verb);
match lex_item {
LexContentItem::VerbatimBlock(verb) => {
assert_eq!(verb.closing_data.label.value, "rust");
assert_eq!(verb.children.len(), 2);
}
_ => panic!("Expected VerbatimBlock"),
}
}
#[test]
fn test_inline_formatting_to_text() {
let content = vec![
InlineContent::Text("Plain ".to_string()),
InlineContent::Bold(vec![InlineContent::Text("bold".to_string())]),
InlineContent::Text(" ".to_string()),
InlineContent::Italic(vec![InlineContent::Text("italic".to_string())]),
InlineContent::Text(" ".to_string()),
InlineContent::Code("code".to_string()),
];
let text = inline_content_to_text(&content);
assert!(text.contains("Plain"));
assert!(text.contains("*bold*"));
assert!(text.contains("_italic_"));
assert!(text.contains("`code`"));
}
#[test]
fn test_round_trip_paragraph() {
use crate::{from_ir, to_ir};
use lex_core::lex::ast::ContentItem;
use lex_core::lex::ast::Document as LexDocument;
let original_lex = LexDocument::with_content(vec![ContentItem::Paragraph(
LexParagraph::from_line("Test content".to_string()),
)]);
let ir_doc = to_ir(&original_lex);
let back_to_lex = from_ir(&ir_doc);
assert!(!back_to_lex.root.children.is_empty());
}
#[test]
fn test_full_document_to_lex() {
let ir_doc = Document {
title: None,
subtitle: None,
children: vec![
DocNode::Paragraph(Paragraph {
content: vec![InlineContent::Text("First paragraph".to_string())],
}),
DocNode::Paragraph(Paragraph {
content: vec![InlineContent::Text("Second paragraph".to_string())],
}),
],
};
let lex_doc = to_lex_document(&ir_doc);
assert!(!lex_doc.root.children.is_empty());
}
}