use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use tdoc::{formatter, markdown, parse};
fn collect_ftml_fixtures() -> Vec<PathBuf> {
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_dir.push("tests/data/ftml");
let entries = fs::read_dir(&test_dir)
.unwrap_or_else(|e| panic!("Failed to read test directory {:?}: {}", test_dir, e));
let mut files: Vec<PathBuf> = entries
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.extension().and_then(|ext| ext.to_str()) == Some("ftml") {
Some(path)
} else {
None
}
})
.collect();
files.sort();
assert!(!files.is_empty(), "No .ftml files found in {:?}", test_dir);
files
}
fn render_ftml(document: &tdoc::Document) -> tdoc::Result<String> {
let mut buffer = Vec::new();
tdoc::write(&mut buffer, document)?;
Ok(String::from_utf8(buffer)?)
}
fn load_ftml_document(path: &Path, file_name: &str) -> Option<tdoc::Document> {
let ftml_content = match fs::read_to_string(path) {
Ok(content) => content,
Err(e) => {
eprintln!("Failed to read {}: {}", file_name, e);
return None;
}
};
match parse(Cursor::new(&ftml_content)) {
Ok(doc) => Some(doc),
Err(e) => {
eprintln!("Failed to parse {}: {}", file_name, e);
None
}
}
}
#[test]
fn test_ftml_to_markdown_snapshots() {
for path in collect_ftml_fixtures() {
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_else(|| panic!("Invalid filename: {:?}", path));
let base_name = file_name.strip_suffix(".ftml").unwrap_or(file_name);
let document = match load_ftml_document(&path, file_name) {
Some(doc) => doc,
None => continue,
};
let mut markdown_output = Vec::new();
markdown::write(&mut markdown_output, &document)
.unwrap_or_else(|e| panic!("Failed to convert {} to markdown: {}", file_name, e));
if matches!(file_name, "test_full_doc.ftml" | "testdocument.ftml") {
let snapshot_name = format!("{}.md", base_name);
insta::assert_binary_snapshot!(snapshot_name.as_str(), markdown_output);
eprintln!(
"Skipping canonical round-trip assertions for {} due to known fidelity limitations.",
file_name
);
continue;
}
let markdown_string = String::from_utf8(markdown_output.clone())
.unwrap_or_else(|e| panic!("Markdown output for {} not UTF-8: {}", file_name, e));
let reparsed_document = markdown::parse(Cursor::new(&markdown_string))
.unwrap_or_else(|e| panic!("Failed to re-parse markdown for {}: {}", file_name, e));
if reparsed_document != document {
let min_len = document
.paragraphs
.len()
.min(reparsed_document.paragraphs.len());
let mut reported = false;
for idx in 0..min_len {
if document.paragraphs[idx] != reparsed_document.paragraphs[idx] {
eprintln!(
"Round-trip mismatch for {} at paragraph {} ({:?})",
file_name,
idx,
document.paragraphs[idx].paragraph_type()
);
if matches!(
document.paragraphs[idx].paragraph_type(),
tdoc::ParagraphType::UnorderedList | tdoc::ParagraphType::OrderedList
) {
let orig_counts: Vec<_> = document.paragraphs[idx]
.entries()
.iter()
.map(|entry| entry.len())
.collect();
let new_counts: Vec<_> = reparsed_document.paragraphs[idx]
.entries()
.iter()
.map(|entry| entry.len())
.collect();
eprintln!(
"List entry paragraph counts, original={:?} reparsed={:?}",
orig_counts, new_counts
);
let min_entries = document.paragraphs[idx]
.entries()
.len()
.min(reparsed_document.paragraphs[idx].entries().len());
for entry_idx in 0..min_entries {
if document.paragraphs[idx].entries()[entry_idx]
!= reparsed_document.paragraphs[idx].entries()[entry_idx]
{
eprintln!(
"Entry {} diff:\noriginal: {:#?}\nreparsed: {:#?}",
entry_idx,
document.paragraphs[idx].entries()[entry_idx],
reparsed_document.paragraphs[idx].entries()[entry_idx]
);
break;
}
}
} else {
eprintln!(
"Original paragraph: {:#?}\nReparsed paragraph: {:#?}",
document.paragraphs[idx], reparsed_document.paragraphs[idx]
);
}
reported = true;
break;
}
}
if !reported {
eprintln!(
"Round-trip mismatch for {}: paragraph count differs (original={}, reparsed={})",
file_name,
document.paragraphs.len(),
reparsed_document.paragraphs.len()
);
}
}
let original_ftml = render_ftml(&document)
.unwrap_or_else(|e| panic!("Failed to render original FTML for {}: {}", file_name, e));
let roundtrip_ftml = render_ftml(&reparsed_document).unwrap_or_else(|e| {
panic!("Failed to render round-trip FTML for {}: {}", file_name, e)
});
if roundtrip_ftml != original_ftml {
eprintln!(
"FTML mismatch for {}:\noriginal:\n{}\nreparsed:\n{}",
file_name, original_ftml, roundtrip_ftml
);
}
assert_eq!(
reparsed_document, document,
"Document mismatch for {}",
file_name
);
let snapshot_name = format!("{}.md", base_name);
insta::assert_binary_snapshot!(snapshot_name.as_str(), markdown_output);
}
}
#[test]
fn test_ftml_to_ascii_snapshots() {
for path in collect_ftml_fixtures() {
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_else(|| panic!("Invalid filename: {:?}", path));
let base_name = file_name.strip_suffix(".ftml").unwrap_or(file_name);
let document = match load_ftml_document(&path, file_name) {
Some(doc) => doc,
None => continue,
};
let mut ascii_output = Vec::new();
let mut formatter = formatter::Formatter::new_ascii(&mut ascii_output);
formatter
.write_document(&document)
.unwrap_or_else(|e| panic!("Failed to format {} as ASCII: {}", file_name, e));
let snapshot_name = format!("ascii__{}.txt", base_name);
insta::assert_binary_snapshot!(snapshot_name.as_str(), ascii_output);
}
}
#[test]
fn test_simple_paragraph_to_markdown() {
let ftml = "<p>Hello, world!</p>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_bold_text_to_markdown() {
let ftml = "<p>This is <b>bold</b> text.</p>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_italic_text_to_markdown() {
let ftml = "<p>This is <i>italic</i> text.</p>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_code_text_to_markdown() {
let ftml = "<p>This is <code>code</code> text.</p>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_strikethrough_to_markdown() {
let ftml = "<p>This is <s>strikethrough</s> text.</p>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_headers_to_markdown() {
let ftml = "<h1>Header 1</h1><h2>Header 2</h2><h3>Header 3</h3>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_blockquote_to_markdown() {
let ftml = "<blockquote><p>This is a quote.</p></blockquote>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_unordered_list_to_markdown() {
let ftml = "<ul><li><p>Item 1</p></li><li><p>Item 2</p></li><li><p>Item 3</p></li></ul>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_ordered_list_to_markdown() {
let ftml = "<ol><li><p>First</p></li><li><p>Second</p></li><li><p>Third</p></li></ol>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_nested_lists_to_markdown() {
let ftml = "<ul><li><p>Item 1</p><ul><li><p>Nested 1</p></li><li><p>Nested 2</p></li></ul></li><li><p>Item 2</p></li></ul>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_mixed_inline_styles_to_markdown() {
let ftml =
"<p>This has <b>bold</b>, <i>italic</i>, <code>code</code>, and <s>strikethrough</s>.</p>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_nested_inline_styles_to_markdown() {
let ftml = "<p>This is <b>bold with <i>italic</i> inside</b>.</p>";
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}
#[test]
fn test_complex_document_to_markdown() {
let ftml = r#"<h1>Title</h1>
<p>Introduction paragraph with <b>bold</b> text.</p>
<h2>Section 1</h2>
<p>Some content here.</p>
<ul>
<li><p>List item 1</p></li>
<li><p>List item 2</p></li>
</ul>
<blockquote>
<p>A quoted paragraph.</p>
</blockquote>"#;
let doc = parse(Cursor::new(ftml)).unwrap();
let mut output = Vec::new();
markdown::write(&mut output, &doc).unwrap();
let markdown = String::from_utf8(output).unwrap();
insta::assert_snapshot!(markdown);
}