lsp-docs 0.1.1

Static structured documentation for the Axon language, embedded at compile time. Consumed by axon-lsp's hover and completion resolvers.
Documentation
//! Build-time embedder for `content/**/*.md`.
//!
//! Walks the `content/` tree under `CARGO_MANIFEST_DIR`, parses each
//! markdown file's YAML frontmatter (a small, fixed schema — no
//! external YAML crate needed), and emits `OUT_DIR/generated.rs`
//! containing a `pub static ENTRIES: &[DocEntry]` literal.
//!
//! At runtime, `src/lib.rs` does `include!(concat!(env!("OUT_DIR"),
//! "/generated.rs"))` and exposes `find_doc` / `find_any_doc`
//! lookups that hit pre-allocated string slices baked into the
//! binary — zero disk I/O, zero allocation per lookup, and `cargo
//! tree -p lsp-docs --edges normal` stays empty.
//!
//! The frontmatter schema is intentionally narrow:
//!
//! ```text
//! ---
//! name: String
//! kind: type
//! since: 0.1
//! stability: stable
//! ---
//! (markdown body…)
//! ```
//!
//! `name` is the lookup key. `kind` is one of `type`, `syntax`,
//! `handler` (mapped to the matching `DocKind` enum at runtime).
//! `since` and `stability` are surfaced in the rendered hover
//! header. Trailing whitespace and BOM are tolerated; everything
//! else is rejected with a build-time error so doc rot is caught
//! by `cargo build`, not by users.

use std::fs;
use std::io::Write as _;
use std::path::{Path, PathBuf};

fn main() {
    let manifest_dir = PathBuf::from(env_var("CARGO_MANIFEST_DIR"));
    let content_root = manifest_dir.join("content");
    let out_dir = PathBuf::from(env_var("OUT_DIR"));
    let out_path = out_dir.join("generated.rs");

    println!("cargo:rerun-if-changed=content");
    println!("cargo:rerun-if-changed=build.rs");

    let mut entries: Vec<Entry> = Vec::new();
    if content_root.is_dir() {
        collect_markdown(&content_root, &mut entries);
    }
    // Stable order matching the runtime `DocKind` enum's discriminant
    // (declared `Type, Syntax, Handler` — see `src/lib.rs`).
    entries.sort_by(|a, b| {
        kind_order(a.kind)
            .cmp(&kind_order(b.kind))
            .then(a.name.cmp(&b.name))
    });

    let mut out = fs::File::create(&out_path)
        .unwrap_or_else(|e| panic!("create {}: {e}", out_path.display()));
    writeln!(
        out,
        "// @generated by build.rs — do not edit by hand.\n\
         pub(crate) static ENTRIES: &[DocEntry] = &["
    )
    .unwrap();
    for e in &entries {
        writeln!(
            out,
            "    DocEntry {{\n        \
              name: {name:?},\n        \
              kind: DocKind::{kind},\n        \
              since: {since:?},\n        \
              stability: Stability::{stability},\n        \
              body: {body:?},\n    \
             }},",
            name = e.name,
            kind = e.kind,
            since = e.since,
            stability = e.stability,
            body = e.body,
        )
        .unwrap();
    }
    writeln!(out, "];").unwrap();
}

fn env_var(key: &str) -> String {
    std::env::var(key).unwrap_or_else(|_| panic!("env var {key} not set"))
}

const fn kind_order(kind: &'static str) -> u8 {
    match kind.as_bytes() {
        b"Type" => 0,
        b"Syntax" => 1,
        b"Handler" => 2,
        _ => u8::MAX,
    }
}

fn collect_markdown(dir: &Path, out: &mut Vec<Entry>) {
    let read = fs::read_dir(dir).unwrap_or_else(|e| panic!("read_dir {}: {e}", dir.display()));
    for entry in read {
        let entry = entry.unwrap_or_else(|e| panic!("dirent in {}: {e}", dir.display()));
        let path = entry.path();
        let ft = entry
            .file_type()
            .unwrap_or_else(|e| panic!("file_type {}: {e}", path.display()));
        if ft.is_dir() {
            collect_markdown(&path, out);
        } else if path.extension().and_then(|s| s.to_str()) == Some("md") {
            let raw = fs::read_to_string(&path)
                .unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
            let parsed = parse_doc(&raw)
                .unwrap_or_else(|e| panic!("frontmatter error in {}: {e}", path.display()));
            out.push(parsed);
        }
    }
}

struct Entry {
    name: String,
    kind: &'static str,
    since: String,
    stability: &'static str,
    body: String,
}

fn parse_doc(raw: &str) -> Result<Entry, String> {
    // Strip optional UTF-8 BOM.
    let body = raw.strip_prefix('\u{feff}').unwrap_or(raw);
    let body = body.trim_start_matches('\n');
    let rest = body
        .strip_prefix("---\n")
        .or_else(|| body.strip_prefix("---\r\n"))
        .ok_or_else(|| "missing opening `---` line".to_string())?;
    let end = rest
        .find("\n---")
        .ok_or_else(|| "missing closing `---` line".to_string())?;
    let header = &rest[..end];
    let after_close = &rest[end..];
    // Skip past the closing `---` and its trailing newline.
    let after_marker = after_close
        .strip_prefix("\n---\n")
        .or_else(|| after_close.strip_prefix("\n---\r\n"))
        .or_else(|| after_close.strip_prefix("\n---"))
        .ok_or_else(|| "malformed closing `---`".to_string())?;
    let body_md = after_marker.trim_start_matches('\n').to_string();

    let mut name = None;
    let mut kind = None;
    let mut since = None;
    let mut stability = None;
    for line in header.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        let (key, value) = trimmed
            .split_once(':')
            .ok_or_else(|| format!("malformed key: line {line:?}"))?;
        let key = key.trim();
        let value = value.trim().trim_matches('"').to_string();
        match key {
            "name" => name = Some(value),
            "kind" => kind = Some(value),
            "since" => since = Some(value),
            "stability" => stability = Some(value),
            other => return Err(format!("unknown frontmatter key {other:?}")),
        }
    }

    let name = name.ok_or("missing `name`")?;
    let kind = match kind.ok_or("missing `kind`")?.as_str() {
        "type" => "Type",
        "syntax" => "Syntax",
        "handler" => "Handler",
        other => {
            return Err(format!(
                "unknown kind {other:?} (expected type|syntax|handler)"
            ));
        }
    };
    let since = since.ok_or("missing `since`")?;
    let stability = match stability.ok_or("missing `stability`")?.as_str() {
        "stable" => "Stable",
        "experimental" => "Experimental",
        other => {
            return Err(format!(
                "unknown stability {other:?} (expected stable|experimental)"
            ));
        }
    };

    Ok(Entry {
        name,
        kind,
        since,
        stability,
        body: body_md,
    })
}