pleme-doc-gen 0.1.52

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 CMake AST + pretty-printer.
//!
//! Per the ★★★ NO-format!()-for-code prime directive. Covers the
//! CMakeLists.txt function-call DSL:
//!
//!   cmake_minimum_required(VERSION 3.20)
//!   project(demo VERSION 0.1.0 LANGUAGES CXX)
//!   set(CMAKE_CXX_STANDARD 20)
//!   add_executable(demo src/main.cpp)
//!
//! CMake commands are flat function calls; arguments are positional
//! but conventionally GROUPED by keyword (VERSION X, LANGUAGES CXX,
//! PUBLIC|PRIVATE deps…). The typed Arg variant captures this.
//!
//! Strings are unquoted when they're simple identifiers/numbers/paths
//! and double-quoted with escape when they contain spaces, special
//! chars, or need to be explicit literals.

use std::fmt::Write;

/// A typed CMake command argument.
#[derive(Debug, Clone)]
pub enum Arg {
    /// `IDENTIFIER` — bare token (variable name, keyword like VERSION).
    Ident(String),
    /// `42` or `3.20` — bare numeric literal.
    Num(String),
    /// `"string with spaces"` — quoted string literal (escape rules
    /// applied at render time).
    Str(String),
}

impl Arg {
    pub fn ident(v: impl Into<String>) -> Self { Self::Ident(v.into()) }
    pub fn num(v: impl Into<String>) -> Self { Self::Num(v.into()) }
    pub fn s(v: impl Into<String>) -> Self { Self::Str(v.into()) }

    fn render(&self) -> String {
        match self {
            Self::Ident(s) => s.clone(),
            Self::Num(s) => s.clone(),
            Self::Str(s) => render_cmake_string(s),
        }
    }
}

fn render_cmake_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("\\\""),
            '\n' => out.push_str(r"\n"),
            c => out.push(c),
        }
    }
    out.push('"');
    out
}

/// A typed CMake statement.
#[derive(Debug, Clone)]
pub enum Stmt {
    /// `command(arg1 arg2 ...)` — the only real CMake statement.
    Call { name: String, args: Vec<Arg> },
    /// Blank-line separator.
    Blank,
    /// `# comment` line.
    Comment(String),
}

/// A typed CMakeLists.txt file.
#[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 }
    /// Convenience: `name(args…)` call as a statement.
    pub fn call(&mut self, name: impl Into<String>, args: impl IntoIterator<Item = Arg>) -> &mut Self {
        self.stmts.push(Stmt::Call {
            name: name.into(),
            args: args.into_iter().collect(),
        });
        self
    }
    pub fn render(&self) -> String {
        let mut out = String::new();
        for s in &self.stmts {
            match s {
                Stmt::Call { name, args } => {
                    let _ = write!(out, "{name}(");
                    let parts: Vec<String> = args.iter().map(Arg::render).collect();
                    out.push_str(&parts.join(" "));
                    out.push_str(")\n");
                }
                Stmt::Blank => out.push('\n'),
                Stmt::Comment(c) => { let _ = writeln!(out, "# {c}"); }
            }
        }
        out
    }
}

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

    #[test]
    fn ident_args_render_unquoted() {
        let mut f = File::new();
        f.call("project", [Arg::ident("demo")]);
        assert_eq!(f.render(), "project(demo)\n");
    }

    #[test]
    fn string_with_spaces_gets_quoted() {
        let mut f = File::new();
        f.call("set", [Arg::ident("DESC"), Arg::s("a description with spaces")]);
        let out = f.render();
        assert!(out.contains("\"a description with spaces\""));
    }

    #[test]
    fn keyword_grouped_args() {
        // project(demo VERSION 0.1.0 LANGUAGES CXX)
        let mut f = File::new();
        f.call("project", [
            Arg::ident("demo"),
            Arg::ident("VERSION"), Arg::num("0.1.0"),
            Arg::ident("LANGUAGES"), Arg::ident("CXX"),
        ]);
        assert_eq!(f.render(), "project(demo VERSION 0.1.0 LANGUAGES CXX)\n");
    }

    #[test]
    fn quoted_string_escapes_quote_and_backslash() {
        assert_eq!(render_cmake_string(r#"a"b\c"#), r#""a\"b\\c""#);
    }

    #[test]
    fn full_cmakelists_shape() {
        let mut f = File::new();
        f.call("cmake_minimum_required", [Arg::ident("VERSION"), Arg::num("3.20")]);
        f.call("project", [
            Arg::ident("demo"),
            Arg::ident("VERSION"), Arg::num("0.1.0"),
            Arg::ident("LANGUAGES"), Arg::ident("CXX"),
        ]);
        f.call("set", [Arg::ident("CMAKE_CXX_STANDARD"), Arg::num("20")]);
        f.push(Stmt::Blank);
        f.call("add_executable", [Arg::ident("demo"), Arg::ident("src/main.cpp")]);
        let out = f.render();
        assert!(out.starts_with("cmake_minimum_required(VERSION 3.20)\n"));
        assert!(out.contains("project(demo VERSION 0.1.0 LANGUAGES CXX)\n"));
        assert!(out.contains("set(CMAKE_CXX_STANDARD 20)\n"));
        assert!(out.contains("add_executable(demo src/main.cpp)\n"));
    }
}