use std::io::{Cursor, Write};
use std::path::Path;
use std::process::Command;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_undoc")
}
const CONTENT_TYPES: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Default Extension="png" ContentType="image/png"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>"#;
const ROOT_RELS: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"#;
const DOCUMENT_XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<w:body>
<w:p><w:r><w:t>A paragraph, so the document is not empty.</w:t></w:r></w:p>
<w:p><w:r><w:drawing><a:blip r:embed="rIdImg"/></w:drawing></w:r></w:p>
</w:body>
</w:document>"#;
const DOCUMENT_RELS: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rIdImg" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
</Relationships>"#;
const PNG: &[u8] = &[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4,
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
0x42, 0x60, 0x82,
];
fn docx_with_one_image() -> Vec<u8> {
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
let options =
zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
for (name, body) in [
("[Content_Types].xml", CONTENT_TYPES),
("_rels/.rels", ROOT_RELS),
("word/document.xml", DOCUMENT_XML),
("word/_rels/document.xml.rels", DOCUMENT_RELS),
] {
zip.start_file(name, options).unwrap();
zip.write_all(body.as_bytes()).unwrap();
}
zip.start_file("word/media/image1.png", options).unwrap();
zip.write_all(PNG).unwrap();
zip.finish().unwrap().into_inner()
}
fn image_destinations(markdown: &str) -> Vec<String> {
let mut found = Vec::new();
let mut rest = markdown;
while let Some(start) = rest.find("![") {
rest = &rest[start..];
let Some(close) = rest.find("](") else { break };
let Some(end) = rest[close + 2..].find(')') else {
break;
};
found.push(rest[close + 2..close + 2 + end].to_string());
rest = &rest[close + 2 + end..];
}
found
}
#[test]
fn convert_renders_image_links_that_resolve_on_disk() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("with_image.docx");
std::fs::write(&input, docx_with_one_image()).unwrap();
let out = tmp.path().join("out");
let status = Command::new(bin())
.args(["convert"])
.arg(&input)
.arg("-o")
.arg(&out)
.arg("--quiet")
.status()
.unwrap();
assert!(status.success(), "convert failed: {:?}", status);
let markdown = std::fs::read_to_string(out.join("extract.md")).unwrap();
let destinations = image_destinations(&markdown);
assert_eq!(
destinations.len(),
1,
"expected exactly one image link, got {:?}\n--- markdown ---\n{}",
destinations,
markdown
);
for dest in &destinations {
assert!(
dest.starts_with("images/"),
"image link must carry the subdirectory the CLI writes into, got {:?}",
dest
);
let resolved = out.join(Path::new(dest));
assert!(
resolved.is_file(),
"image link {:?} resolves to {}, which does not exist",
dest,
resolved.display()
);
}
}
#[test]
fn convert_without_images_renders_no_image_links() {
let tmp = tempfile::tempdir().unwrap();
let input = tmp.path().join("with_image.docx");
std::fs::write(&input, docx_with_one_image()).unwrap();
let out = tmp.path().join("out");
let status = Command::new(bin())
.args(["convert"])
.arg(&input)
.arg("-o")
.arg(&out)
.arg("--no-images")
.arg("--quiet")
.status()
.unwrap();
assert!(status.success(), "convert failed: {:?}", status);
let markdown = std::fs::read_to_string(out.join("extract.md")).unwrap();
assert_eq!(
image_destinations(&markdown),
Vec::<String>::new(),
"--no-images must not leave links pointing at files it declined to write"
);
assert!(!out.join("images").exists());
}