pleme-doc-gen 0.1.45

Rust replacement for the M0 Python _gen-patterns.py + _gen-docs.py scripts in pleme-io/actions. Walks every action.yml + emits substrate's patterns-full.nix + per-action README.md + root catalog. Per the NO-SHELL prime directive.
//! Typed Meson AST + pretty-printer.
//!
//! Per the ★★★ NO-format!()-for-code prime directive. Covers the
//! `meson.build` flat-function-call DSL:
//!
//!   project('name', 'c', 'cpp',
//!     version: '0.1.0',
//!     license: 'MIT',
//!     default_options: ['cpp_std=c++20'])
//!
//!   executable('name', 'src/main.cpp')
//!
//! Meson is not Python — `project(...)` and `executable(...)` are typed
//! function calls with positional + named arguments. Single-quoted
//! strings are the convention.

use std::fmt::Write;

#[derive(Debug, Clone)]
pub enum Expr {
    /// `'text'` — single-quoted with Meson's escape rules.
    String(String),
    Int(i64),
    Bool(bool),
    /// `[a, b, c]` — Meson list literal.
    Array(Vec<Expr>),
    /// `func(pos, pos, named: val)` — Meson function call.
    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 {
    /// Bare call statement: `executable(...)`, `project(...)`.
    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')");
    }
}