docspec-docx-reader 1.12.0

DOCX to DocSpec event stream reader
Documentation
//! Build configuration for `docspec-docx-reader`.

use core::fmt::Write as _;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

fn main() -> Result<(), Box<dyn core::error::Error>> {
    println!("cargo::rustc-check-cfg=cfg(coverage)");
    generate_corpus_tests("pandoc", "pandoc_corpus_tests.rs")?;
    generate_corpus_tests("docspec", "docspec_corpus_tests.rs")?;
    Ok(())
}

fn generate_corpus_tests(
    corpus: &str,
    output_file: &str,
) -> Result<(), Box<dyn core::error::Error>> {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
    let fixture_dir = manifest_dir.join(format!("../../tests/fixtures/docx/{corpus}"));
    println!("cargo::rerun-if-changed={}", fixture_dir.display());

    let corpus_literal = rust_string_literal(corpus);
    let mut generated = String::from("// @generated by crates/docspec-docx-reader/build.rs\n\n");

    if fixture_dir.try_exists()? {
        let mut fixtures = fs::read_dir(&fixture_dir)?
            .filter_map(|entry_result| fixture_file_name(entry_result).transpose())
            .collect::<Result<Vec<_>, _>>()?;
        fixtures.sort();

        for fixture in fixtures {
            let test_name = test_name_for_fixture(&fixture)?;
            let fixture_literal = rust_string_literal(&fixture);
            writeln!(
                &mut generated,
                "#[test]\nfn {test_name}() {{\n    super::assert_fixture_snapshot({corpus_literal}, {fixture_literal});\n}}\n"
            )?;
        }
    }

    let out_dir = PathBuf::from(env::var("OUT_DIR")?);
    fs::write(out_dir.join(output_file), generated)?;
    Ok(())
}

fn rust_string_literal(value: &str) -> String {
    let mut literal = String::from("\"");
    for character in value.chars() {
        literal.extend(character.escape_default());
    }
    literal.push('"');
    literal
}

fn fixture_file_name(entry_result: io::Result<fs::DirEntry>) -> io::Result<Option<String>> {
    let directory_entry = entry_result?;
    let path = directory_entry.path();
    if path.extension() != Some(OsStr::new("docx")) {
        return Ok(None);
    }

    path.file_name()
        .and_then(OsStr::to_str)
        .map(|file_name| Some(file_name.to_owned()))
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "fixture filename is not UTF-8"))
}

fn test_name_for_fixture(file_name: &str) -> Result<String, Box<dyn core::error::Error>> {
    let stem = Path::new(file_name)
        .file_stem()
        .and_then(OsStr::to_str)
        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "fixture stem is not UTF-8"))?;

    let test_name = match stem {
        "0_level_headers" => "_0_level_headers",
        "compact-style-removal" => "compact_style_removal",
        "custom-style-reference" => "custom_style_reference",
        "german-reference" => "german_reference",
        "instrText_hyperlink" => "instr_text_hyperlink",
        "lists-compact" => "lists_compact",
        "nested_instrText" => "nested_instr_text",
        "preformatted-boundaries" => "preformatted_boundaries",
        other => other,
    };

    if is_valid_rust_identifier(test_name) {
        Ok(test_name.to_owned())
    } else {
        Err(format!(
            "Pandoc fixture {file_name:?} does not map to a valid Rust test name; add an explicit case in build.rs"
        )
        .into())
    }
}

fn is_valid_rust_identifier(identifier: &str) -> bool {
    let mut chars = identifier.chars();
    let Some(first) = chars.next() else {
        return false;
    };

    (first == '_' || first.is_ascii_alphabetic())
        && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}