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);
}
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> {
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..];
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,
})
}