use anyhow::Result;
use crate::{
ansi::{export_to_ansi_with_options, AnsiOptions},
document::*,
ColorDepth, ExportFormat,
};
pub fn export_document(document: &Document, format: &ExportFormat) -> Result<()> {
match format {
ExportFormat::Markdown => export_to_markdown(document),
ExportFormat::Text => export_to_text(document),
ExportFormat::Csv => export_to_csv(document),
ExportFormat::Json => export_to_json(document),
ExportFormat::Ansi => export_to_ansi(document),
}
}
pub fn format_as_markdown(document: &Document) -> String {
let mut markdown = String::new();
markdown.push_str(&format!("# {}\n\n", document.title));
markdown.push_str("## Document Information\n\n");
markdown.push_str(&format!("- **File**: {}\n", document.metadata.file_path));
markdown.push_str(&format!("- **Pages**: {}\n", document.metadata.page_count));
markdown.push_str(&format!("- **Words**: {}\n", document.metadata.word_count));
if let Some(author) = &document.metadata.author {
markdown.push_str(&format!("- **Author**: {author}\n"));
}
markdown.push_str("\n---\n\n");
for element in &document.elements {
match element {
DocumentElement::Heading {
level,
text,
number,
} => {
let prefix = "#".repeat(*level as usize);
let heading_text = if let Some(number) = number {
format!("{number} {text}")
} else {
text.clone()
};
markdown.push_str(&format!("{prefix} {heading_text}\n\n"));
}
DocumentElement::Paragraph { runs } => {
let mut paragraph_text = String::new();
for run in runs {
let mut formatted_text = run.text.clone();
if run.formatting.bold {
formatted_text = format!("**{formatted_text}**");
}
if run.formatting.italic {
formatted_text = format!("*{formatted_text}*");
}
if run.formatting.strikethrough {
formatted_text = format!("~~{formatted_text}~~");
}
paragraph_text.push_str(&formatted_text);
}
markdown.push_str(&format!("{paragraph_text}\n\n"));
}
DocumentElement::List { items, ordered } => {
for (i, item) in items.iter().enumerate() {
let indent = " ".repeat(item.level as usize);
let bullet = if *ordered {
format!("{}. ", i + 1)
} else {
"- ".to_string()
};
let mut item_text = String::new();
for run in &item.runs {
let mut formatted_text = run.text.clone();
if run.formatting.bold {
formatted_text = format!("**{formatted_text}**");
}
if run.formatting.italic {
formatted_text = format!("*{formatted_text}*");
}
if run.formatting.strikethrough {
formatted_text = format!("~~{formatted_text}~~");
}
item_text.push_str(&formatted_text);
}
markdown.push_str(&format!("{indent}{bullet}{item_text}\n"));
}
markdown.push('\n');
}
DocumentElement::Table { table } => {
if let Some(title) = &table.metadata.title {
markdown.push_str(&format!("### {title}\n\n"));
}
let header_content: Vec<String> =
table.headers.iter().map(|h| h.content.clone()).collect();
markdown.push_str(&format!("| {} |\n", header_content.join(" | ")));
let alignment_row: Vec<String> = table
.metadata
.column_alignments
.iter()
.map(|align| match align {
TextAlignment::Left => ":---".to_string(),
TextAlignment::Right => "---:".to_string(),
TextAlignment::Center => ":---:".to_string(),
TextAlignment::Justify => ":---".to_string(),
})
.collect();
markdown.push_str(&format!("| {} |\n", alignment_row.join(" | ")));
for row in &table.rows {
let row_content: Vec<String> =
row.iter().map(|cell| cell.content.clone()).collect();
markdown.push_str(&format!("| {} |\n", row_content.join(" | ")));
}
markdown.push('\n');
}
DocumentElement::Image {
description,
width,
height,
image_path,
..
} => {
let alt = description;
let url = image_path
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| description.clone());
let dimensions = match (width, height) {
(Some(w), Some(h)) => format!(" <!-- {w}x{h} -->"),
_ => String::new(),
};
markdown.push_str(&format!("{dimensions}\n\n"));
}
DocumentElement::Equation { latex, .. } => {
markdown.push_str(&format!("$${latex}$$\n\n"));
}
DocumentElement::CodeBlock { text } => {
markdown.push_str("```\n");
markdown.push_str(text);
if !text.ends_with('\n') {
markdown.push('\n');
}
markdown.push_str("```\n\n");
}
DocumentElement::TextBox { lines } => {
for line in lines {
markdown.push_str(&format!("> {line}\n"));
}
markdown.push('\n');
}
DocumentElement::PageBreak => {
markdown.push_str("\n---\n\n");
}
}
}
markdown
}
pub fn export_to_markdown(document: &Document) -> Result<()> {
print!("{}", format_as_markdown(document));
Ok(())
}
pub fn format_as_text(document: &Document) -> String {
let mut text = String::new();
text.push_str(&format!("{}\n", document.title));
text.push_str(&"=".repeat(document.title.len()));
text.push_str("\n\n");
for element in &document.elements {
match element {
DocumentElement::Heading {
level,
text: heading_text,
..
} => {
let underline = match level {
1 => "=",
2 => "-",
_ => "~",
};
text.push_str(&format!("{heading_text}\n"));
text.push_str(&underline.repeat(heading_text.len()));
text.push_str("\n\n");
}
DocumentElement::Paragraph { runs } => {
let para_text: String = runs.iter().map(|run| run.text.as_str()).collect();
text.push_str(&format!("{para_text}\n\n"));
}
DocumentElement::List { items, ordered } => {
for (i, item) in items.iter().enumerate() {
let bullet = if *ordered {
format!("{}. ", i + 1)
} else {
"* ".to_string()
};
let indent = " ".repeat(item.level as usize);
let item_text: String = item.runs.iter().map(|run| run.text.as_str()).collect();
text.push_str(&format!("{indent}{bullet}{item_text}\n"));
}
text.push('\n');
}
DocumentElement::Table { table } => {
if let Some(title) = &table.metadata.title {
text.push_str(&format!("{title}\n"));
text.push_str(&"=".repeat(title.len()));
text.push_str("\n\n");
}
let col_widths = &table.metadata.column_widths;
let top_border = generate_text_table_border(col_widths, "┌", "┬", "┐", "─");
text.push_str(&format!("{top_border}\n"));
let header_line = render_text_table_row(&table.headers, col_widths, true);
text.push_str(&format!("{header_line}\n"));
let separator = generate_text_table_border(col_widths, "├", "┼", "┤", "─");
text.push_str(&format!("{separator}\n"));
for row in &table.rows {
let row_line = render_text_table_row(row, col_widths, false);
text.push_str(&format!("{row_line}\n"));
}
let bottom_border = generate_text_table_border(col_widths, "└", "┴", "┘", "─");
text.push_str(&format!("{bottom_border}\n"));
text.push('\n');
}
DocumentElement::PageBreak => {
text.push_str("---\n\n");
}
DocumentElement::Image {
description,
image_path,
..
} => {
if let Some(path) = image_path {
match crate::terminal_image::TerminalImageRenderer::with_options(
document.image_options.max_width,
document.image_options.max_height,
document.image_options.scale,
)
.render_image_from_path(path, description)
{
Ok(_) => {
text.push('\n');
}
Err(_) => {
text.push_str(&format!("[Image: {description}]\n\n"));
}
}
} else {
text.push_str(&format!("[Image: {description}]\n\n"));
}
}
DocumentElement::Equation { latex, .. } => {
text.push_str(&format!("Equation: {latex}\n\n"));
}
DocumentElement::CodeBlock { text: code } => {
text.push_str(code);
if !code.ends_with('\n') {
text.push('\n');
}
text.push('\n');
}
DocumentElement::TextBox { lines } => {
let width = lines.iter().map(|s| s.len()).max().unwrap_or(0) + 2;
let bar = "-".repeat(width);
text.push_str(&format!("+{bar}+\n"));
for line in lines {
text.push_str(&format!("| {line:<width$} |\n", width = width - 2));
}
text.push_str(&format!("+{bar}+\n\n"));
}
}
}
text
}
pub fn export_to_text(document: &Document) -> Result<()> {
export_to_text_with_images(document);
Ok(())
}
fn export_to_text_with_images(document: &Document) {
println!("{}\n", document.title);
println!("Document Information:");
println!("- File: {}", document.metadata.file_path);
println!("- Pages: {}", document.metadata.page_count);
println!("- Words: {}", document.metadata.word_count);
if let Some(author) = &document.metadata.author {
println!("- Author: {author}");
}
println!("\n{}\n", "=".repeat(50));
for element in &document.elements {
match element {
DocumentElement::Heading {
level,
text,
number,
} => {
let prefix = "#".repeat(*level as usize);
let heading_text = if let Some(number) = number {
format!("{number} {text}")
} else {
text.clone()
};
println!("{prefix} {heading_text}\n");
}
DocumentElement::Paragraph { runs } => {
let mut paragraph_text = String::new();
for run in runs {
let mut formatted_text = run.text.clone();
if run.formatting.bold {
formatted_text = format!("**{formatted_text}**");
}
if run.formatting.italic {
formatted_text = format!("*{formatted_text}*");
}
if run.formatting.underline {
formatted_text = format!("_{formatted_text}_");
}
if run.formatting.strikethrough {
formatted_text = format!("~~{formatted_text}~~");
}
paragraph_text.push_str(&formatted_text);
}
println!("{paragraph_text}\n");
}
DocumentElement::List { items, .. } => {
for item in items {
let item_text: String = item.runs.iter().map(|run| run.text.as_str()).collect();
println!("- {item_text}");
}
println!();
}
DocumentElement::Table { table } => {
for row in &table.rows {
let row_content: Vec<String> =
row.iter().map(|cell| cell.content.clone()).collect();
println!("| {} |", row_content.join(" | "));
}
println!();
}
DocumentElement::Image {
description,
image_path,
..
} => {
if let Some(path) = image_path {
match crate::terminal_image::TerminalImageRenderer::with_options(
document.image_options.max_width,
document.image_options.max_height,
document.image_options.scale,
)
.render_image_from_path(path, description)
{
Ok(_) => {
println!();
}
Err(_) => {
println!("[Image: {description}]\n");
}
}
} else {
println!("[Image: {description}]\n");
}
}
DocumentElement::Equation { latex, .. } => {
println!("Equation: {latex}\n");
}
DocumentElement::CodeBlock { text } => {
print!("{text}");
if !text.ends_with('\n') {
println!();
}
println!();
}
DocumentElement::TextBox { lines } => {
let width = lines.iter().map(|s| s.len()).max().unwrap_or(0) + 2;
let bar = "-".repeat(width);
println!("+{bar}+");
for line in lines {
println!("| {line:<width$} |", width = width - 2);
}
println!("+{bar}+\n");
}
DocumentElement::PageBreak => {
println!("{}\n", "-".repeat(50));
}
}
}
}
pub fn export_to_csv(document: &Document) -> Result<()> {
let mut csv_output = Vec::new();
for (table_index, element) in document.elements.iter().enumerate() {
if let DocumentElement::Table { table } = element {
if table_index > 0 {
csv_output.push(String::new()); csv_output.push(format!("# Table {}", table_index + 1));
}
if let Some(title) = &table.metadata.title {
csv_output.push(format!("# {title}"));
}
let header_line = table
.headers
.iter()
.map(|h| escape_csv_field(&h.content))
.collect::<Vec<_>>()
.join(",");
csv_output.push(header_line);
for row in &table.rows {
let row_line = row
.iter()
.map(|cell| escape_csv_field(&cell.content))
.collect::<Vec<_>>()
.join(",");
csv_output.push(row_line);
}
}
}
if csv_output.is_empty() {
println!("No tables found in document");
} else {
for line in csv_output {
println!("{line}");
}
}
Ok(())
}
pub fn export_to_json(document: &Document) -> Result<()> {
let json_output = serde_json::to_string_pretty(document)?;
println!("{json_output}");
Ok(())
}
#[allow(dead_code)]
pub fn extract_citations(document: &Document) -> Result<Vec<Citation>> {
let mut citations = Vec::new();
for (index, element) in document.elements.iter().enumerate() {
let text = match element {
DocumentElement::Heading { text, .. } => text,
DocumentElement::Paragraph { runs } => {
&runs.iter().map(|run| run.text.as_str()).collect::<String>()
}
_ => continue,
};
let citation_patterns = [
r"\([A-Z][a-z]+,\s*\d{4}\)", r"\[[0-9]+\]", r"\([A-Z][a-z]+\s+et\s+al\.,\s*\d{4}\)", ];
for pattern in &citation_patterns {
if let Ok(regex) = regex::Regex::new(pattern) {
for mat in regex.find_iter(text) {
citations.push(Citation {
text: mat.as_str().to_string(),
element_index: index,
citation_type: CitationType::InText,
});
}
}
}
}
Ok(citations)
}
#[allow(dead_code)]
pub fn extract_bibliography(document: &Document) -> Result<Vec<Citation>> {
let mut bibliography = Vec::new();
for (index, element) in document.elements.iter().enumerate() {
if let DocumentElement::Heading { text, .. } = element {
if text.to_lowercase().contains("reference")
|| text.to_lowercase().contains("bibliography")
|| text.to_lowercase().contains("works cited")
{
for (bib_index, bib_element) in document.elements[index + 1..].iter().enumerate() {
match bib_element {
DocumentElement::Paragraph { runs } => {
let text: String = runs.iter().map(|run| run.text.as_str()).collect();
if !text.trim().is_empty() {
bibliography.push(Citation {
text: text.clone(),
element_index: index + bib_index + 1,
citation_type: CitationType::Bibliography,
});
}
}
DocumentElement::List { items, .. } => {
for item in items {
let text: String =
item.runs.iter().map(|run| run.text.as_str()).collect();
bibliography.push(Citation {
text,
element_index: index + bib_index + 1,
citation_type: CitationType::Bibliography,
});
}
}
DocumentElement::Heading { .. } => break, _ => {}
}
}
break;
}
}
}
Ok(bibliography)
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Citation {
pub text: String,
pub element_index: usize,
pub citation_type: CitationType,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum CitationType {
InText,
Bibliography,
}
fn escape_csv_field(field: &str) -> String {
if field.contains(',') || field.contains('"') || field.contains('\n') {
format!("\"{}\"", field.replace('"', "\"\""))
} else {
field.to_string()
}
}
fn generate_text_table_border(
column_widths: &[usize],
left: &str,
middle: &str,
right: &str,
fill: &str,
) -> String {
let mut border = String::new();
border.push_str(left);
for (i, &width) in column_widths.iter().enumerate() {
border.push_str(&fill.repeat(width + 2)); if i < column_widths.len() - 1 {
border.push_str(middle);
}
}
border.push_str(right);
border
}
fn render_text_table_row(cells: &[TableCell], column_widths: &[usize], _is_header: bool) -> String {
let mut row = String::new();
row.push('│');
for (i, cell) in cells.iter().enumerate() {
let width = column_widths.get(i).copied().unwrap_or(10);
let aligned_content = align_text_cell_content(&cell.content, cell.alignment, width);
row.push(' ');
row.push_str(&aligned_content);
row.push(' ');
row.push('│');
}
row
}
fn align_text_cell_content(content: &str, alignment: TextAlignment, width: usize) -> String {
let trimmed = content.trim();
match alignment {
TextAlignment::Left => format!("{trimmed:<width$}"),
TextAlignment::Right => format!("{trimmed:>width$}"),
TextAlignment::Center => {
let padding = width.saturating_sub(trimmed.len());
let left_pad = padding / 2;
let right_pad = padding - left_pad;
format!(
"{}{}{}",
" ".repeat(left_pad),
trimmed,
" ".repeat(right_pad)
)
}
TextAlignment::Justify => {
format!("{trimmed:<width$}")
}
}
}
pub fn export_to_ansi(document: &Document) -> Result<()> {
let options = AnsiOptions::default();
let ansi_output = export_to_ansi_with_options(document, &options)?;
print!("{ansi_output}");
Ok(())
}
pub fn export_to_ansi_with_cli_options(
document: &Document,
terminal_width: Option<usize>,
color_depth: &ColorDepth,
) -> Result<()> {
let options = AnsiOptions {
terminal_width: terminal_width.unwrap_or_else(|| {
std::env::var("COLUMNS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(80)
}),
color_depth: color_depth.clone(),
};
let ansi_output = export_to_ansi_with_options(document, &options)?;
print!("{ansi_output}");
Ok(())
}