pleme-doc-gen 0.1.40

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 line-directives AST + pretty-printer.
//!
//! Per the ★★★ NO-format!()-for-code prime directive. Used by the
//! line-directive ecosystems (go.mod / nimble / .gitignore-shape /
//! anything whose grammar is "one directive per line ± optional
//! parenthesized block of items"). Distinct from sexp_ast (Lisp-shape)
//! and ini_ast (section-key-value) — these formats are flat sequences
//! of `keyword args…` with optional `(…)` grouping.

use std::fmt::Write;

#[derive(Debug, Clone)]
pub enum Line {
    /// A bare comment — emitted as-is with a leading `// ` prefix
    /// added by the renderer (callers pass the comment body only).
    Comment(String),
    /// A blank line — used to separate stanza groups.
    Blank,
    /// A single-line directive: `keyword arg arg arg`.
    Directive { keyword: String, args: Vec<String> },
    /// A grouped directive: `keyword (\n\t<inner>\n)`. Inner lines get
    /// indented one tab in the canonical Go convention.
    Group { keyword: String, inner: Vec<Line> },
}

pub fn render(lines: &[Line]) -> String {
    let mut out = String::new();
    render_at(lines, 0, &mut out);
    out
}

/// Typed wrapper around a Vec<Line> so the unified `ast::Render` trait
/// has a target type (the free function `render` predates the trait
/// + remains the implementation backend).
#[derive(Debug, Clone, Default)]
pub struct Lines(pub Vec<Line>);

impl Lines {
    pub fn new() -> Self { Self::default() }
    pub fn push(&mut self, line: Line) -> &mut Self { self.0.push(line); self }
    pub fn extend(&mut self, lines: impl IntoIterator<Item = Line>) -> &mut Self {
        self.0.extend(lines);
        self
    }
    pub fn from(items: impl IntoIterator<Item = Line>) -> Self {
        Self(items.into_iter().collect())
    }
}

fn render_at(lines: &[Line], depth: usize, out: &mut String) {
    let indent = "\t".repeat(depth);
    for line in lines {
        match line {
            Line::Comment(c) => {
                let _ = writeln!(out, "{indent}// {c}");
            }
            Line::Blank => out.push('\n'),
            Line::Directive { keyword, args } => {
                let _ = write!(out, "{indent}{keyword}");
                for a in args {
                    out.push(' ');
                    out.push_str(a);
                }
                out.push('\n');
            }
            Line::Group { keyword, inner } => {
                let _ = writeln!(out, "{indent}{keyword} (");
                render_at(inner, depth + 1, out);
                let _ = writeln!(out, "{indent})");
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn simple_directive() {
        let out = render(&[Line::Directive {
            keyword: "module".to_string(),
            args: vec!["github.com/pleme-io/demo".to_string()],
        }]);
        assert_eq!(out, "module github.com/pleme-io/demo\n");
    }
    #[test]
    fn group_renders_with_tab_indent() {
        let out = render(&[Line::Group {
            keyword: "require".to_string(),
            inner: vec![
                Line::Directive {
                    keyword: "github.com/x/y".to_string(),
                    args: vec!["v1.2.3".to_string()],
                },
                Line::Directive {
                    keyword: "github.com/a/b".to_string(),
                    args: vec!["v0.1.0".to_string()],
                },
            ],
        }]);
        assert!(out.contains("require (\n"));
        assert!(out.contains("\tgithub.com/x/y v1.2.3\n"));
        assert!(out.ends_with(")\n"));
    }
    #[test]
    fn blank_separates_stanzas() {
        let out = render(&[
            Line::Directive { keyword: "module".to_string(), args: vec!["m".to_string()] },
            Line::Blank,
            Line::Directive { keyword: "go".to_string(), args: vec!["1.22".to_string()] },
        ]);
        assert_eq!(out, "module m\n\ngo 1.22\n");
    }
}