use std::{
collections::BTreeMap,
fs,
path::{Path, PathBuf},
};
use float_pigment_mlp::{
context::{Context, Parse},
node::{attribute::Attribute, NodeType},
};
fn main() {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let cases_dir = PathBuf::from(&manifest).join("tests/cases");
if !cases_dir.exists() {
return;
}
let out = std::env::var("OUT_DIR").unwrap();
let out_gen = PathBuf::from(&out).join("generated");
let mut by_topic: BTreeMap<String, Vec<(String, String, String, bool)>> = BTreeMap::new();
let mut entries = walk(&cases_dir, &cases_dir);
entries.sort();
for (rel, abs) in entries {
if abs.extension().and_then(|e| e.to_str()) != Some("html") {
continue;
}
let rel_str = rel.with_extension("").to_string_lossy().replace('\\', "/");
let mut parts = rel_str.split('/');
let topic = parts.next().unwrap_or("misc").to_string();
let name = parts.collect::<Vec<_>>().join("_");
let name_ident = name.replace('-', "_");
let html = fs::read_to_string(&abs).unwrap_or_default();
let ignore = html.contains("data-ignore=\"true\"");
let body = translate_html(&html);
by_topic
.entry(topic)
.or_default()
.push((name, name_ident, body, ignore));
}
write_cases_tree(&out_gen, &by_topic, true);
let is_ci = std::env::var("CI").is_ok();
let is_publish_verify = manifest.contains("target/package");
if !is_ci && !is_publish_verify {
let gen_dir = PathBuf::from(&manifest).join("tests/generated");
write_cases_tree(&gen_dir, &by_topic, false);
}
println!("cargo:rerun-if-changed=tests/cases");
println!("cargo:rerun-if-changed=build.rs");
}
fn write_cases_tree(
gen_dir: &Path,
by_topic: &BTreeMap<String, Vec<(String, String, String, bool)>>,
emit_all_rs: bool,
) {
let _ = fs::remove_dir_all(gen_dir);
fs::create_dir_all(gen_dir).unwrap();
let mut all_rs = String::from("// AUTO-GENERATED by build.rs. Do not edit.\n\n");
let mut top_mod = String::from("// AUTO-GENERATED by build.rs. Do not edit.\n\n");
for (topic, cases) in by_topic {
let topic_dir = gen_dir.join(topic);
fs::create_dir_all(&topic_dir).unwrap();
let topic_ident = topic.replace('-', "_");
let mut topic_mod = String::from("// AUTO-GENERATED. Do not edit.\n\n");
for (name, name_ident, body, ignore) in cases {
let fn_name = format!("html_{}_{}", topic_ident, name_ident);
let ignore_attr = if *ignore { "#[ignore]\n" } else { "" };
let case_rs = format!(
"// AUTO-GENERATED from tests/cases/{topic}/{name}.html. Do not edit.\n\n{ignore_attr}#[rustfmt::skip]\n#[test]\nfn {fn_name}() {{\n use crate::TestCtx;\n{body}}}\n"
);
fs::write(topic_dir.join(format!("{name}.rs")), &case_rs).unwrap();
all_rs.push_str(&format!("include!(\"{topic}/{name}.rs\");\n"));
topic_mod.push_str(&format!("mod {name_ident};\n"));
}
fs::write(topic_dir.join("mod.rs"), topic_mod).unwrap();
top_mod.push_str(&format!("mod {topic_ident};\n"));
}
fs::write(gen_dir.join("mod.rs"), top_mod).unwrap();
if emit_all_rs {
fs::write(gen_dir.join("all.rs"), all_rs).unwrap();
}
}
fn translate_html(html: &str) -> String {
let mut parse_ctx = Context::create(None);
parse_ctx.parse(html);
let mut out = String::from(" let mut ctx = TestCtx::new();\n");
let mut counter = Counter::default();
if let Some(tree) = parse_ctx.tree() {
if let Some(root) = tree.root() {
let _ = gen_node(&mut out, root, None, &mut counter);
}
}
out.push_str(" ctx.layout_imperative();\n");
for a in &counter.asserts {
out.push_str(a);
}
out
}
#[derive(Default)]
struct Counter {
n: usize,
t: usize,
asserts: Vec<String>,
}
fn gen_node(out: &mut String, node: &NodeType, parent: Option<&str>, c: &mut Counter) -> String {
match node {
NodeType::Element(e) => {
let var = format!("n{}", c.n);
c.n += 1;
out.push_str(&format!(
" let {var} = ctx.create_node({});\n",
rust_str_literal(e.tag())
));
let attrs = e.attributes();
if let Some(style) = attrs.get("style") {
if !style.is_empty() {
out.push_str(&format!(
" ctx.set_style({var}, {});\n",
rust_str_literal(&style)
));
}
}
if is_measure_text_slot(e.tag()) {
let len = attrs
.get("len")
.and_then(|v| v.trim().parse::<usize>().ok())
.unwrap_or(0);
let font_size = attrs
.get("fontSize")
.and_then(|v| v.trim().parse::<f32>().ok())
.unwrap_or(16.0);
out.push_str(&format!(
" ctx.set_measure_text({var}, {len}, {});\n",
parse_value(&font_size.to_string())
));
}
collect_asserts(&var, attrs, c);
if let Some(p) = parent {
out.push_str(&format!(" ctx.append({}, {var});\n", p));
}
let children: Vec<_> = e.children_mut().iter().cloned().collect();
for child in children.iter() {
gen_node(out, child.as_ref(), Some(&var), c);
}
var
}
NodeType::Text(t) => {
let var = format!("t{}", c.t);
c.t += 1;
out.push_str(&format!(
" let {var} = ctx.create_text({});\n",
rust_str_literal(t.text())
));
if let Some(p) = parent {
out.push_str(&format!(" ctx.append({}, {var});\n", p));
}
var
}
NodeType::Fragment(f) => {
let var = format!("n{}", c.n);
c.n += 1;
out.push_str(&format!(
" let {var} = ctx.create_node({});\n",
rust_str_literal("div")
));
if let Some(p) = parent {
out.push_str(&format!(" ctx.append({}, {var});\n", p));
}
let children: Vec<_> = f.children_mut().iter().cloned().collect();
for child in children.iter() {
let _ = gen_node(out, child.as_ref(), Some(&var), c);
}
var
}
}
}
fn collect_asserts(var: &str, attrs: &Attribute, c: &mut Counter) {
const MAP: &[(&str, &str, &str)] = &[
("expect_width", "data-expect-width", "width"),
("expect_height", "data-expect-height", "height"),
("expect_left", "data-expect-left", "left"),
("expect_top", "data-expect-top", "top"),
("expect_margin_top", "data-expect-margin-top", "margin_top"),
(
"expect_margin_right",
"data-expect-margin-right",
"margin_right",
),
(
"expect_margin_bottom",
"data-expect-margin-bottom",
"margin_bottom",
),
(
"expect_margin_left",
"data-expect-margin-left",
"margin_left",
),
];
for (alt, primary, getter) in MAP {
let v = attrs.get(primary).or_else(|| attrs.get(alt));
if let Some(v) = v {
c.asserts.push(format!(
" assert_eq!(ctx.{getter}({var}).round(), {});\n",
parse_value(&v)
));
}
}
}
fn parse_value(v: &str) -> String {
let v = v.trim();
if v.contains('.') {
v.to_string()
} else {
format!("{}.0", v)
}
}
fn is_measure_text_slot(tag: &str) -> bool {
tag == "text-slot"
}
fn rust_str_literal(s: &str) -> String {
for hashes in 1..=5 {
let pat = format!("\"{}", "#".repeat(hashes));
if !s.contains(&pat) {
let h = "#".repeat(hashes);
return format!("r{h}\"{s}\"{h}");
}
}
let mut out = String::from('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
other => out.push(other),
}
}
out.push('"');
out
}
fn walk(root: &PathBuf, dir: &PathBuf) -> Vec<(PathBuf, PathBuf)> {
let mut out = Vec::new();
if let Ok(rd) = fs::read_dir(dir) {
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
out.extend(walk(root, &p));
} else {
let rel = p.strip_prefix(root).unwrap().to_path_buf();
out.push((rel, p));
}
}
}
out
}