use penmanship::categories;
use std::collections::BTreeMap;
use std::fs;
use std::io::Write;
fn generate_docs() -> String {
let mut output = String::new();
output.push_str("# Character Mappings\n\n");
output.push_str(
"This document lists all Unicode character patterns supported by penmanship.\n\n",
);
let mut categories_data: BTreeMap<&str, Vec<(String, String, String)>> = BTreeMap::new();
{
let mut patterns = Vec::new();
for (pattern, (character, description)) in categories::punctuation::PUNCTUATION.entries() {
patterns.push((
pattern.to_string(),
character.to_string(),
description.to_string(),
));
}
patterns.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
categories_data.insert("Punctuation", patterns);
}
{
let mut patterns = Vec::new();
for (pattern, (character, description)) in categories::math::MATH.entries() {
patterns.push((
pattern.to_string(),
character.to_string(),
description.to_string(),
));
}
patterns.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
categories_data.insert("Math", patterns);
}
{
let mut patterns = Vec::new();
for (pattern, (character, description)) in categories::greek::GREEK.entries() {
patterns.push((
pattern.to_string(),
character.to_string(),
description.to_string(),
));
}
patterns.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
categories_data.insert("Greek", patterns);
}
{
let mut patterns = Vec::new();
for (pattern, (character, description)) in categories::fractions::FRACTIONS.entries() {
patterns.push((
pattern.to_string(),
character.to_string(),
description.to_string(),
));
}
patterns.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
categories_data.insert("Fractions", patterns);
}
{
let mut patterns = Vec::new();
for (pattern, (character, description)) in categories::currency::CURRENCY.entries() {
patterns.push((
pattern.to_string(),
character.to_string(),
description.to_string(),
));
}
patterns.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
categories_data.insert("Currency", patterns);
}
{
let mut patterns = Vec::new();
for (pattern, (character, description)) in categories::symbols::SYMBOLS.entries() {
patterns.push((
pattern.to_string(),
character.to_string(),
description.to_string(),
));
}
patterns.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
categories_data.insert("Symbols", patterns);
}
{
let mut patterns = Vec::new();
for (pattern, (character, description)) in categories::superscripts::SUPERSCRIPTS.entries()
{
patterns.push((
pattern.to_string(),
character.to_string(),
description.to_string(),
));
}
patterns.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
categories_data.insert("Superscripts", patterns);
}
{
let mut patterns = Vec::new();
for (pattern, (character, description)) in categories::subscripts::SUBSCRIPTS.entries() {
patterns.push((
pattern.to_string(),
character.to_string(),
description.to_string(),
));
}
patterns.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
categories_data.insert("Subscripts", patterns);
}
{
let mut patterns = Vec::new();
let mut count = 0;
for (pattern, (character, _description)) in categories::html::PART1.entries() {
if pattern.contains("Invisible") || pattern.contains("ZeroWidth") {
continue;
}
if count < 10 {
patterns.push((
pattern.to_string(),
character.to_string(),
"html entity".to_string(), ));
count += 1;
} else {
break;
}
}
patterns.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
categories_data.insert("HTML (sample)", patterns);
}
{
let patterns = vec![
(
":smile:".to_string(),
"😄".to_string(),
"grinning face with smiling eyes".to_string(),
),
(
":heart:".to_string(),
"❤️".to_string(),
"red heart".to_string(),
),
(
":thumbsup:".to_string(),
"👍".to_string(),
"thumbs up".to_string(),
),
];
categories_data.insert("Emoji (sample)", patterns);
}
for (category_name, patterns) in categories_data {
output.push_str(&format!("## {category_name}\n\n"));
if category_name.contains("sample") {
if category_name == "HTML (sample)" {
output.push_str("2200+ total HTML named character references are supported.\n\n");
} else if category_name == "Emoji (sample)" {
output.push_str("1800+ emoji shortcodes via emojis crate are supported.\n\n");
}
}
output.push_str("| Pattern | Character | Description |\n");
output.push_str("|---------|-----------|-------------|\n");
for (pattern, character, description) in patterns {
let escaped_pattern = pattern.replace('|', "\\|");
output.push_str(&format!(
"| `{escaped_pattern}` | {character} | {description} |\n"
));
}
output.push('\n');
}
output.push_str("---\n\n");
output.push_str("*Generated by `scripts/generate_mapping_docs.rs`*\n");
output
}
fn main() -> std::io::Result<()> {
println!("Generating mapping documentation...");
let output = generate_docs();
fs::create_dir_all("docs")?;
let mut file = fs::File::create("docs/mappings.md")?;
file.write_all(output.as_bytes())?;
println!("✓ Generated docs/mappings.md");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_docs() {
let output = generate_docs();
assert!(output.contains("# Character Mappings"));
assert!(output.contains("## Punctuation"));
assert!(output.contains("## Math"));
assert!(output.contains("## Greek"));
assert!(output.len() > 1000); }
}