use crate::{CodegenError, InferredConfig, TokenSource, TokenSourceKind};
const HEADER: &str =
"# Plumb configuration — bootstrapped by `plumb init --from <path>` from the sources below.";
const TAILWIND_HINT: &str =
"# Tailwind config detected. Plumb merges Tailwind theme tokens at lint time —";
pub fn render_toml(inferred: &InferredConfig) -> Result<String, CodegenError> {
let mut out = String::new();
out.push_str(HEADER);
out.push('\n');
if inferred.sources.is_empty() {
out.push_str("# No design-token sources were discovered. Edit the values below to match your system.\n");
} else {
out.push_str("#\n");
write_source_list(&mut out, &inferred.sources);
}
if has_tailwind(&inferred.sources) {
out.push_str("#\n");
out.push_str(TAILWIND_HINT);
out.push('\n');
out.push_str("# run `plumb lint` from the same directory and the adapter will resolve Tailwind's theme.\n");
}
out.push('\n');
let body = toml::to_string_pretty(&inferred.config)?;
out.push_str(&body);
if !out.ends_with('\n') {
out.push('\n');
}
Ok(out)
}
fn write_source_list(out: &mut String, sources: &[TokenSource]) {
use std::fmt::Write as _;
let mut sorted: Vec<&TokenSource> = sources.iter().collect();
sorted.sort_by(|a, b| {
a.kind
.cmp(&b.kind)
.then_with(|| a.relative_path.cmp(&b.relative_path))
});
for source in sorted {
let label = source.kind.label();
let path = display_path(&source.relative_path);
let _ = writeln!(out, "# - {label}: {path}");
}
}
fn has_tailwind(sources: &[TokenSource]) -> bool {
sources
.iter()
.any(|s| s.kind == TokenSourceKind::TailwindConfig)
}
fn display_path(path: &std::path::Path) -> String {
path.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/")
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use plumb_core::Config;
use std::path::PathBuf;
fn fixture(sources: Vec<TokenSource>) -> InferredConfig {
InferredConfig {
config: Config::default(),
summary: Vec::new(),
sources,
}
}
#[test]
fn empty_inputs_render_with_header_note() {
let rendered = render_toml(&fixture(Vec::new())).unwrap();
assert!(rendered.starts_with(HEADER));
assert!(rendered.contains("No design-token sources were discovered"));
}
#[test]
fn renders_sources_in_stable_order() {
let inferred = fixture(vec![
TokenSource {
kind: TokenSourceKind::CssCustomProperties,
relative_path: PathBuf::from("z.css"),
},
TokenSource {
kind: TokenSourceKind::CssCustomProperties,
relative_path: PathBuf::from("a.css"),
},
TokenSource {
kind: TokenSourceKind::TailwindConfig,
relative_path: PathBuf::from("tailwind.config.ts"),
},
]);
let rendered = render_toml(&inferred).unwrap();
let tw_pos = rendered.find("tailwind.config.ts").unwrap();
let a_pos = rendered.find("a.css").unwrap();
let z_pos = rendered.find("z.css").unwrap();
assert!(tw_pos < a_pos, "tailwind should come first");
assert!(a_pos < z_pos, "css files should be alphabetical");
assert!(rendered.contains("Tailwind config detected"));
}
#[test]
fn render_emits_canonical_toml_body() {
let mut config = Config::default();
config
.color
.tokens
.insert("bg/canvas".into(), "#ffffff".into());
let rendered = render_toml(&InferredConfig {
config,
summary: Vec::new(),
sources: Vec::new(),
})
.unwrap();
assert!(rendered.contains("[color]"));
assert!(rendered.contains("bg/canvas"));
assert!(rendered.ends_with('\n'));
}
}