use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Expr {
String(String),
Int(i64),
Bool(bool),
Array(Vec<Expr>),
Call { name: String, args: Vec<(Option<String>, Expr)> },
}
impl Expr {
pub fn s(v: impl Into<String>) -> Self { Self::String(v.into()) }
pub fn i(v: i64) -> Self { Self::Int(v) }
pub fn b(v: bool) -> Self { Self::Bool(v) }
pub fn arr(items: impl IntoIterator<Item = Expr>) -> Self {
Self::Array(items.into_iter().collect())
}
pub fn call(name: impl Into<String>, args: Vec<(Option<String>, Expr)>) -> Self {
Self::Call { name: name.into(), args }
}
pub fn pos(e: Expr) -> (Option<String>, Expr) { (None, e) }
pub fn lbl(label: &str, e: Expr) -> (Option<String>, Expr) {
(Some(label.to_string()), e)
}
fn render(&self, indent: usize) -> String {
match self {
Self::String(s) => render_meson_string(s),
Self::Int(n) => n.to_string(),
Self::Bool(true) => "true".into(),
Self::Bool(false) => "false".into(),
Self::Array(items) if items.is_empty() => "[]".into(),
Self::Array(items) => {
let parts: Vec<String> = items.iter().map(|v| v.render(indent + 1)).collect();
let mut s = String::from("[");
s.push_str(&parts.join(", "));
s.push(']');
s
}
Self::Call { name, args } if args.is_empty() => {
let mut s = String::from(name); s.push_str("()"); s
}
Self::Call { name, args } => {
let parts: Vec<String> = args.iter().map(|(lbl, v)| {
let mut p = String::new();
if let Some(l) = lbl { p.push_str(l); p.push_str(": "); }
p.push_str(&v.render(indent + 1));
p
}).collect();
let inline = {
let mut s = String::from(name); s.push('(');
s.push_str(&parts.join(", "));
s.push(')'); s
};
if inline.len() <= 80 && !inline.contains('\n') {
inline
} else {
let pad_inner = " ".repeat(indent + 1);
let mut out = String::from(name);
out.push_str("(\n");
for (i, p) in parts.iter().enumerate() {
let _ = write!(out, "{pad_inner}{p}");
if i + 1 < parts.len() { out.push(','); }
out.push('\n');
}
let pad_outer = " ".repeat(indent);
let _ = write!(out, "{pad_outer})");
out
}
}
}
}
}
fn render_meson_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for c in s.chars() {
match c {
'\\' => out.push_str(r"\\"),
'\'' => out.push_str(r"\'"),
'\n' => out.push_str(r"\n"),
c => out.push(c),
}
}
out.push('\'');
out
}
#[derive(Debug, Clone)]
pub enum Stmt {
Expr(Expr),
Blank,
Comment(String),
}
#[derive(Debug, Clone, Default)]
pub struct File { pub stmts: Vec<Stmt> }
impl File {
pub fn new() -> Self { Self::default() }
pub fn push(&mut self, s: Stmt) -> &mut Self { self.stmts.push(s); self }
pub fn render(&self) -> String {
let mut out = String::new();
for s in &self.stmts {
match s {
Stmt::Expr(e) => { let _ = writeln!(out, "{}", e.render(0)); }
Stmt::Blank => out.push('\n'),
Stmt::Comment(c) => { let _ = writeln!(out, "# {c}"); }
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_quoted_escapes_quote() {
assert_eq!(render_meson_string("it's"), r"'it\'s'");
}
#[test]
fn long_call_breaks_multiline() {
let e = Expr::call("project", vec![
Expr::pos(Expr::s("very-long-project-name-pushing-over-eighty-chars")),
Expr::lbl("version", Expr::s("1.0")),
Expr::lbl("license", Expr::s("MIT")),
]);
let out = e.render(0);
assert!(out.starts_with("project(\n"));
assert!(out.ends_with(')'));
}
#[test]
fn short_call_inline() {
let e = Expr::call("executable", vec![
Expr::pos(Expr::s("demo")),
Expr::pos(Expr::s("src/main.cpp")),
]);
assert_eq!(e.render(0), "executable('demo', 'src/main.cpp')");
}
}