const DECORATION: [char; 8] = ['-', '=', '*', '#', '~', '+', '_', '/'];
const CRATE_NAMES: [&str; 5] = ["alloc", "core", "proc_macro", "std", "test"];
const KEYWORDS: [&str; 51] = [
"abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "crate",
"do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if", "impl",
"in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
"return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof",
"unsafe", "unsized", "use", "virtual", "where", "while", "yield",
];
const TITLE_WORDS: usize = 3;
pub(crate) fn module_name(raw: &str) -> String {
let name = sanitize(raw);
if is_reserved(&name) {
format!("{name}_")
} else {
name
}
}
pub(crate) fn is_reserved(name: &str) -> bool {
CRATE_NAMES.contains(&name) || KEYWORDS.contains(&name)
}
pub(crate) fn banner_title(leading: &str) -> Option<String> {
let mut after_rule = false;
for line in leading.lines() {
let Some(text) = line.trim().strip_prefix("//") else {
after_rule = false;
continue;
};
let text = text.trim();
let core = text.trim_matches(|c: char| DECORATION.contains(&c) || c.is_whitespace());
if core.is_empty() {
after_rule = text.chars().filter(|c| DECORATION.contains(c)).count() >= 3;
continue;
}
let before = decoration_run(text.chars());
let after = decoration_run(text.chars().rev());
let decorated = before >= 3 || after >= 3 || (before >= 2 && after >= 2);
if decorated || after_rule {
let title = core
.split([':', '('])
.next()
.unwrap_or_default()
.split_whitespace()
.take(TITLE_WORDS)
.collect::<Vec<_>>()
.join(" ");
if title.chars().any(char::is_alphanumeric) {
return Some(title);
}
}
after_rule = false;
}
None
}
fn decoration_run(chars: impl Iterator<Item = char>) -> usize {
chars
.take_while(|c| DECORATION.contains(c) || c.is_whitespace())
.filter(|c| DECORATION.contains(c))
.count()
}
pub(crate) fn sanitize(name: &str) -> String {
let mut out: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'_'
}
})
.collect();
while out.contains("__") {
out = out.replace("__", "_");
}
let out = out.trim_matches('_').to_owned();
if out.is_empty() || out.chars().next().unwrap().is_ascii_digit() {
format!("m_{out}")
} else {
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decorated_and_boxed_banners_yield_their_titles() {
for (leading, title) in [
("// ---- Linking ----\n\n", "Linking"),
("// ==== Syntax helpers ====\n", "Syntax helpers"),
(" // --- Collection\n", "Collection"),
("// -- Dump --\n", "Dump"),
(
"// ------------\n// Parity checks\n// ------------\n",
"Parity checks",
),
(
"// ----- razel regression: re-exports, mod decls, and body mods\n",
"razel regression",
),
(
"// ---- Load the very large input files ----\n",
"Load the very",
),
("// ---- Linking (phase 2) ----\n", "Linking"),
] {
assert_eq!(banner_title(leading).as_deref(), Some(title), "{leading:?}");
}
}
#[test]
fn prose_and_bare_rules_are_not_banners() {
for leading in [
"",
"// Helper for the linker.\n",
"// - a bullet point\n",
"// ----------------\n\n",
"/* ---- block ---- */\n",
"// ---- ---- ----\n",
] {
assert_eq!(banner_title(leading), None, "{leading:?}");
}
}
#[test]
fn crate_names_and_keywords_get_a_suffix() {
for (raw, name) in [
("Core", "core_"),
("std", "std_"),
("alloc", "alloc_"),
("Type", "type_"),
("Self", "self_"),
("Linking", "linking"),
("impl Core", "impl_core"),
("Syntax helpers", "syntax_helpers"),
] {
assert_eq!(module_name(raw), name);
}
}
}