pleme-doc-gen 0.1.9

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.
//! Hand-rolled minimal yaml parser for action.yml — extracts only
//! description / inputs / outputs without pulling the full
//! serde_yaml dependency-graph cost. The actions repo's yaml is
//! shape-constrained (composite-action only), so we can use
//! line-based parsing safely.

use crate::InputSpec;
use std::collections::BTreeMap;

#[derive(Debug, Default)]
pub struct Parsed {
    pub description: String,
    pub inputs: BTreeMap<String, InputSpec>,
    pub outputs: BTreeMap<String, String>,
}

pub fn parse(src: &str) -> Parsed {
    let mut out = Parsed::default();
    let mut section: Option<&str> = None;
    let mut current_key: Option<String> = None;

    for raw in src.lines() {
        let line = raw.trim_end();

        // Top-level description: line
        if line.starts_with("description:") {
            out.description = unquote(line.splitn(2, ':').nth(1).unwrap_or("").trim());
            continue;
        }

        // Section headers
        if line.starts_with("inputs:") {
            section = Some("inputs");
            current_key = None;
            continue;
        }
        if line.starts_with("outputs:") {
            section = Some("outputs");
            current_key = None;
            continue;
        }
        if line.starts_with("runs:") || line.starts_with("name:") || line.starts_with("branding:") {
            section = None;
            current_key = None;
            continue;
        }

        // 2-space-indented `  key:` opens a new entry inside the current section.
        if let Some(sec) = section {
            if let Some(rest) = line.strip_prefix("  ") {
                if !rest.starts_with(' ') && rest.ends_with(':') {
                    let key = rest.trim_end_matches(':').to_string();
                    current_key = Some(key.clone());
                    if sec == "inputs" {
                        out.inputs.insert(key, InputSpec::default());
                    } else {
                        out.outputs.insert(key, String::new());
                    }
                    continue;
                }
                // 4-space-indented attribute of the current entry
                if let Some(ck) = &current_key {
                    let attr = rest.trim();
                    if sec == "inputs" {
                        if let Some(spec) = out.inputs.get_mut(ck) {
                            if let Some(val) = attr.strip_prefix("required:") {
                                spec.required = val.trim().contains("true");
                            } else if let Some(val) = attr.strip_prefix("default:") {
                                spec.default = Some(unquote(val.trim()));
                            } else if let Some(val) = attr.strip_prefix("description:") {
                                spec.description = Some(unquote(val.trim()));
                            }
                        }
                    } else if sec == "outputs" {
                        if let Some(val) = attr.strip_prefix("description:") {
                            out.outputs.insert(ck.clone(), unquote(val.trim()));
                        }
                    }
                }
            }
        }
    }
    out
}

fn unquote(s: &str) -> String {
    let s = s.trim();
    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
        s[1..s.len() - 1].to_string()
    } else {
        s.to_string()
    }
}