use std::collections::{BTreeMap, HashSet}; #[derive(Clone, Copy)] /// Flag indicating whether special effects variants should be used. pub enum SfxMode { Off, On, } struct Translation<'a> { find: &'a str, replacement: &'a str, } #[derive(Clone)] /// Holds the set of delimiters that should be removed from the MML to select a variant. pub struct Variant { keep_delim: char, remove_delims: HashSet, } impl Variant { /// Get the set of character delimiters to remove from MML for this variant. pub fn ignores(&self) -> &HashSet { &self.remove_delims } } /// The list of all variants defined in an MML file. pub struct VariantList<'v> { all_delims: HashSet, variants: BTreeMap<&'v str, Variant>, } impl<'v> VariantList<'v> { fn new() -> Self { VariantList { all_delims: HashSet::new(), variants: BTreeMap::new(), } } fn finish(mut self) -> Self { for (_, v) in self.variants.iter_mut() { v.remove_delims = self.all_delims.clone(); v.remove_delims.remove(&v.keep_delim); } if self.variants.is_empty() { self.variants.insert( "_default_", Variant { // Not that we expect to find NUL bytes, but effectively we will remove all // delimiters with this variant. keep_delim: '\0', remove_delims: self.all_delims.clone(), }, ); } self } /// Get a specific variant by name, which exists in the MML file. pub fn get(&self, name: &str) -> Option<&Variant> { self.variants.get(name) } } /// Processes "#REPLACE" macros in MML. /// /// Macro syntax: /// #REPLACE c1 c2 /// /// "c1" and "c2" are meant to be single characters, but `replace_chars` finds any two strings of /// non-whitespace. These strings are substituted in-place, so the longer string is truncated to /// the length of the shorter. pub fn replace_chars(input: &str) -> String { let trs: Vec> = input .lines() .filter_map(|line| { line.strip_prefix("#REPLACE") .and_then(|l| l.trim().rsplit_once(char::is_whitespace)) .and_then(|(first, second)| { let length = std::cmp::min(first.len(), second.len()); Some(Translation { find: &first[0..length], replacement: &second[0..length], }) }) }) .collect(); let mut new = input.to_string(); for tr in trs { new = new.replace(tr.find, tr.replacement); } new } /// Processes "#SFXV" and "#VARIANT" macros in MML. /// /// Macro syntax: /// #SFXV o i /// #VARIANT c name /// /// "o", "i", and "c" are single characters that act as markup delimiters around the variant codes. /// For #SFXV, if special effects mode is off, the "o" variants are used, otherwise "i". The "name" /// for one #VARIANT is optional, and its absence indicates the default variant. If all variants in /// the input are named, the first variant will be the default. pub fn get_variants<'a>(input: &'a str, sfx_mode: SfxMode) -> VariantList<'a> { input .lines() .fold(VariantList::new(), |mut state, line| { if let Some(l) = line.strip_prefix("#SFXV") { if let Some((first, second)) = l.trim().rsplit_once(char::is_whitespace) { use SfxMode::*; match sfx_mode { Off => state.all_delims.insert(first_char(first)), On => state.all_delims.insert(first_char(second)), }; } } else if let Some(l) = line.strip_prefix("#VARIANT") { let (first, second) = { let rest = l.trim(); if let Some((f, s)) = rest.rsplit_once(' ') { (first_char(f), s) } else { (first_char(rest), "_default_") } }; state.all_delims.insert(first); if state.variants.len() == 0 && second != "_default_" { state.variants.insert( "_default_", Variant { keep_delim: first, remove_delims: HashSet::new(), }, ); } state.variants.insert( second, Variant { keep_delim: first, remove_delims: HashSet::new(), }, ); } state }) .finish() } fn first_char(s: &str) -> char { s.chars().next().unwrap_or('\0') } pub fn select_variant<'i>(input: &'i str, var: &'i Variant) -> String { let mut output = input.to_string(); // Workspace for converting char to &str in the loop. let mut buf = [0; 4]; // Set of chars to find in `input`. let charlist: Vec = var .remove_delims .iter() .copied() .chain([var.keep_delim]) .collect(); let mut needles = input.rmatch_indices(&charlist[..]).peekable(); while let Some((idx, c)) = needles.next() { match c { // Remove the char if it is the `keep_delim`. d if var.keep_delim.encode_utf8(&mut buf) == d => { // `replace_range` doesn't make a full copy of the string the way `remove` does. output.replace_range(idx..=idx, ""); } // Anything else will be in `remove_delim`, so find a matching pair and remove // them and everything between them. d => match needles.peek() { Some(&(idx_1, d2)) if d == d2 => { needles.next(); output.replace_range(idx_1..=idx, ""); } _ => {} }, } } output }