iced-shadcn-ui 0.1.1

CLI to add shadcn/ui-inspired iced 0.14 components as copy-paste Rust source
const START: &str = "// iced-shadcn managed start";
const END: &str = "// iced-shadcn managed end";

pub fn patch_mod_rs(existing: &str, modules: &[&str]) -> String {
    let mut module_set: Vec<String> = existing
        .lines()
        .filter_map(|line| {
            line.trim()
                .strip_prefix("pub mod ")
                .and_then(|rest| rest.strip_suffix(';'))
                .map(str::to_string)
        })
        .collect();

    for module in modules {
        if !module_set.iter().any(|m| m == module) {
            module_set.push((*module).to_string());
        }
    }
    module_set.sort();

    let managed = module_set
        .iter()
        .map(|m| format!("pub mod {m};"))
        .collect::<Vec<_>>()
        .join("\n");

    let managed_block = format!("{START}\n{managed}\n{END}");

    if let (Some(start), Some(end)) = (existing.find(START), existing.find(END)) {
        let mut out = String::new();
        out.push_str(&existing[..start]);
        out.push_str(&managed_block);
        out.push_str(&existing[end + END.len()..]);
        return out;
    }

    format!("{existing}{managed_block}\n")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn inserts_modules_into_managed_section_idempotently() {
        let original = "// user code\n";
        let once = patch_mod_rs(original, &["theme", "button"]);
        let twice = patch_mod_rs(&once, &["theme", "button", "card"]);
        assert!(once.contains("pub mod theme;"));
        assert!(once.contains("pub mod button;"));
        assert!(twice.contains("pub mod card;"));
        assert_eq!(twice.matches("pub mod theme;").count(), 1);
    }

    #[test]
    fn accepts_snake_case_module_names() {
        let output = patch_mod_rs("", &["scroll_area", "theme"]);
        assert!(output.contains("pub mod scroll_area;"));
        assert!(!output.contains("scroll-area"));
    }
}