use crate::elements::{self, Element, TextSegment};
use anyhow::Result;
use std::fs::File;
use std::io::Read;
pub fn markdown_to_text(markdown: &str) -> String {
let elements = elements::parse_markdown(markdown);
elements_to_text(&elements)
}
fn elements_to_text(elements: &[Element]) -> String {
let mut text = String::new();
for elem in elements {
match elem {
Element::Heading { text: t, .. } => {
text.push_str(t);
text.push('\n');
}
Element::Paragraph { text: t } => {
text.push_str(t);
text.push('\n');
}
Element::RichParagraph { segments } => {
for segment in segments {
match segment {
TextSegment::Plain(t) | TextSegment::Bold(t) | TextSegment::Italic(t) | TextSegment::BoldItalic(t) => {
text.push_str(t);
}
TextSegment::Code(c) => {
text.push('`');
text.push_str(c);
text.push('`');
}
TextSegment::MathInline(expr) => {
text.push('$');
text.push_str(expr);
text.push('$');
}
TextSegment::Link { text: t, url } => {
text.push('[');
text.push_str(t);
text.push_str("](");
text.push_str(url);
text.push(')');
}
}
}
text.push('\n');
}
Element::UnorderedListItem { text: t, .. } => {
text.push_str("• ");
text.push_str(t);
text.push('\n');
}
Element::OrderedListItem { number: _, text: t, .. } => {
text.push_str("• ");
text.push_str(t);
text.push('\n');
}
Element::TaskListItem { checked, text: t } => {
if *checked {
text.push_str("[x] ");
} else {
text.push_str("[ ] ");
}
text.push_str(t);
text.push('\n');
}
Element::CodeBlock { code, .. } => {
text.push('\n');
text.push_str(code);
text.push_str("\n\n");
}
Element::TableRow { cells, is_separator, alignments: _ } => {
if *is_separator {
let sep: Vec<String> = cells.iter().map(|c| "-".repeat(c.len().max(4))).collect();
text.push_str(&sep.join(" "));
} else {
text.push_str(&cells.join(" "));
}
text.push_str(" \n");
}
Element::DefinitionItem { term, definition } => {
text.push_str(term);
text.push_str(": ");
text.push_str(definition);
text.push('\n');
}
Element::Footnote { label, text: t } => {
text.push_str(&format!("[{}] {}", label, t));
text.push('\n');
}
Element::BlockQuote { text: t, depth } => {
let prefix = "> ".repeat(*depth as usize);
text.push_str(&prefix);
text.push_str(t);
text.push('\n');
}
Element::InlineCode { code } => {
text.push_str(code);
text.push('\n');
}
Element::Link { text: t, url } => {
text.push_str(t);
text.push_str(" (");
text.push_str(url);
text.push_str(")\n");
}
Element::Image { alt, path } => {
text.push_str("[Image: ");
text.push_str(alt);
text.push_str("] (");
text.push_str(path);
text.push_str(")\n");
}
Element::StyledText { text: t, .. } => {
text.push_str(t);
text.push('\n');
}
Element::MathBlock { expression } => {
text.push_str("$$\n");
text.push_str(expression);
text.push_str("\n$$\n");
}
Element::MathInline { expression } => {
text.push('$');
text.push_str(expression);
text.push_str("$\n");
}
Element::PageBreak => {
text.push_str("\n---\n");
}
Element::HorizontalRule => {
text.push_str("---\n");
}
Element::EmptyLine => {}
}
}
text
}
pub fn markdown_to_pdf(markdown_file: &str, pdf_file: &str) -> Result<()> {
markdown_to_pdf_with_options(markdown_file, pdf_file, "Helvetica", 12.0)
}
pub fn markdown_to_pdf_with_options(
markdown_file: &str,
pdf_file: &str,
font: &str,
font_size: f32,
) -> Result<()> {
markdown_to_pdf_full(
markdown_file,
pdf_file,
font,
font_size,
crate::pdf_generator::PageOrientation::Portrait,
)
}
pub fn markdown_to_pdf_full(
markdown_file: &str,
pdf_file: &str,
font: &str,
font_size: f32,
orientation: crate::pdf_generator::PageOrientation,
) -> Result<()> {
let mut file = File::open(markdown_file)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let elements = elements::parse_markdown(&content);
let layout = crate::pdf_generator::PageLayout::from_orientation(orientation);
crate::pdf_generator::create_pdf_from_elements_with_layout(
pdf_file, &elements, font, font_size, layout,
)?;
Ok(())
}
pub fn watch_markdown_to_pdf(
markdown_file: &str,
pdf_file: &str,
font: &str,
font_size: f32,
orientation: crate::pdf_generator::PageOrientation,
interval_ms: Option<u64>,
) -> Result<()> {
use std::time::Duration;
use std::thread;
let interval = Duration::from_millis(interval_ms.unwrap_or(1000));
let mut last_modified = std::fs::metadata(markdown_file)?.modified()?;
println!("[watch] Generating initial PDF from {}", markdown_file);
markdown_to_pdf_full(markdown_file, pdf_file, font, font_size, orientation)?;
println!("[watch] Watching {} for changes (interval: {:?}). Press Ctrl+C to stop.", markdown_file, interval);
loop {
thread::sleep(interval);
let metadata = match std::fs::metadata(markdown_file) {
Ok(m) => m,
Err(e) => {
eprintln!("[watch] Error reading file metadata: {}", e);
continue;
}
};
let current_modified = match metadata.modified() {
Ok(m) => m,
Err(e) => {
eprintln!("[watch] Error reading modification time: {}", e);
continue;
}
};
if current_modified > last_modified {
last_modified = current_modified;
println!("[watch] Change detected at {:?}, regenerating PDF...", current_modified);
match markdown_to_pdf_full(markdown_file, pdf_file, font, font_size, orientation) {
Ok(_) => println!("[watch] PDF updated: {}", pdf_file),
Err(e) => eprintln!("[watch] Error regenerating PDF: {}", e),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_markdown_to_pdf_full_roundtrip() {
let tmp_dir = std::env::temp_dir();
let md_path = tmp_dir.join("test_watch.md");
let pdf_path = tmp_dir.join("test_watch.pdf");
{
let mut f = std::fs::File::create(&md_path).unwrap();
f.write_all(b"# Hello\n\nWorld.").unwrap();
}
markdown_to_pdf_full(
&md_path.to_string_lossy(),
&pdf_path.to_string_lossy(),
"Helvetica",
12.0,
crate::pdf_generator::PageOrientation::Portrait,
).unwrap();
assert!(pdf_path.exists(), "PDF should be created");
let bytes1 = std::fs::read(&pdf_path).unwrap();
assert!(!bytes1.is_empty(), "PDF should not be empty");
std::thread::sleep(std::time::Duration::from_millis(50));
{
let mut f = std::fs::File::create(&md_path).unwrap();
f.write_all(b"# Updated\n\nNew content here.").unwrap();
}
markdown_to_pdf_full(
&md_path.to_string_lossy(),
&pdf_path.to_string_lossy(),
"Helvetica",
12.0,
crate::pdf_generator::PageOrientation::Portrait,
).unwrap();
let bytes2 = std::fs::read(&pdf_path).unwrap();
assert!(!bytes2.is_empty(), "Regenerated PDF should not be empty");
let content = String::from_utf8_lossy(&bytes2);
assert!(content.contains("Updated") || content.contains("New content"),
"Updated PDF should contain new content");
let _ = std::fs::remove_file(&md_path);
let _ = std::fs::remove_file(&pdf_path);
}
}