taskc 0.1.0

Scan markdown task markers and generate a beautiful static task catalog page.
Documentation
use std::fs;
use std::path::Path;
use std::process::Command;

use tempfile::TempDir;
use walkdir::WalkDir;

#[test]
fn binary_generates_expected_catalog_from_fixture() {
    let fixture = Path::new("examples/manual-e2e-fixture");
    assert!(fixture.exists(), "fixture folder must exist");

    let temp = TempDir::new().expect("create temp dir");
    copy_dir_all(fixture, temp.path());

    let binary = env!("CARGO_BIN_EXE_taskc");
    let output = Command::new(binary)
        .current_dir(temp.path())
        .output()
        .expect("run taskc binary");

    assert!(output.status.success(), "binary should exit successfully");

    let stdout = String::from_utf8(output.stdout).expect("utf8 stdout");
    assert!(
        stdout.contains("Generated 3 tasks from 4 markdown files"),
        "stdout was: {stdout}"
    );

    let html_path = temp.path().join("tasks.html");
    assert!(html_path.exists(), "tasks.html should be generated");

    let html = fs::read_to_string(&html_path).expect("read generated html");

    assert!(html.contains("Root task: onboarding"));
    assert!(html.contains("App task: release checklist"));
    assert!(html.contains("Docs task: write guide"));

    assert!(!html.contains("Ignored node_modules task"));
    assert!(!html.contains("Ignored target task"));
    assert!(!html.contains("Ignored hidden task"));

    assert!(html.contains("Markdown files scanned: 4"));
    assert!(html.contains("Tasks: 3"));
    assert!(html.contains("id=\"toggle-collapse\""));
    assert!(html.contains("id=\"toggle-theme\""));
    assert!(html.contains("<details class=\"task-card\""));
    assert!(html.contains("<body data-theme=\"dark\">"));
    assert!(
        html.contains("<ul>"),
        "markdown list should render as HTML list"
    );
    assert!(
        html.contains("Nested detail"),
        "deeper header should remain in the captured task body"
    );
    assert!(
        !html.contains("Unmarked heading"),
        "same-level unmarked header should terminate task body"
    );
}

fn copy_dir_all(src: &Path, dst: &Path) {
    for entry in WalkDir::new(src).into_iter().filter_map(Result::ok) {
        let path = entry.path();
        let rel = path
            .strip_prefix(src)
            .expect("entry path should be inside source");

        if rel.as_os_str().is_empty() {
            continue;
        }

        let target = dst.join(rel);
        if entry.file_type().is_dir() {
            fs::create_dir_all(&target).expect("create dir");
        } else {
            if let Some(parent) = target.parent() {
                fs::create_dir_all(parent).expect("create parent");
            }
            fs::copy(path, &target).expect("copy file");
        }
    }
}