#![allow(clippy::needless_return)]
use yaml_rust::{Yaml, YamlLoader};
use crate::pretty_print::yaml_to_string;
use crate::tts::TTS;
extern crate dirs;
use std::cell::RefCell;
use std::rc::Rc;
use std::path::{Path, PathBuf};
use crate::speech::{as_str_checked, RulesFor, FileAndTime};
use std::collections::{HashMap, HashSet};
use phf::phf_set;
use crate::shim_filesystem::*;
use crate::errors::*;
pub static NO_PREFERENCE: &str = "\u{FFFF}";
lazy_static! {
static ref DEFAULT_LANG: Yaml = Yaml::String("en".to_string());
}
pub type PreferenceHashMap = HashMap<String, Yaml>;
#[derive(Debug, Clone, Default)]
pub struct Preferences {
prefs: PreferenceHashMap }
use std::fmt;
impl fmt::Display for Preferences {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut pref_vec: Vec<(&String, &Yaml)> = self.prefs.iter().collect();
pref_vec.sort();
for (name, value) in pref_vec {
writeln!(f, " {}: {}", name, yaml_to_string(value, 0))?;
}
return Ok(());
}
}
impl Preferences{
fn user_defaults() -> Preferences {
let mut prefs = PreferenceHashMap::with_capacity(39);
prefs.insert("Language".to_string(), Yaml::String("en".to_string()));
prefs.insert("LanguageAuto".to_string(), Yaml::String("".to_string())); prefs.insert("SpeechStyle".to_string(), Yaml::String("ClearSpeak".to_string()));
prefs.insert("Verbosity".to_string(), Yaml::String("Medium".to_string()));
prefs.insert("SpeechOverrides_CapitalLetters".to_string(), Yaml::String("".to_string())); prefs.insert("Blind".to_string(), Yaml::Boolean(true));
prefs.insert("MathRate".to_string(), Yaml::Real("100.0".to_string()));
prefs.insert("PauseFactor".to_string(), Yaml::Real("100.0".to_string()));
prefs.insert("NavMode".to_string(), Yaml::String("Enhanced".to_string()));
prefs.insert("Overview".to_string(), Yaml::Boolean(false));
prefs.insert("ResetOverView".to_string(), Yaml::Boolean(true));
prefs.insert("NavVerbosity".to_string(), Yaml::String("Verbose".to_string()));
prefs.insert("AutoZoomOut".to_string(), Yaml::Boolean(true));
prefs.insert("BrailleCode".to_string(), Yaml::String("Nemeth".to_string()));
prefs.insert("BrailleNavHighlight".to_string(), Yaml::String("EndPoints".to_string()));
prefs.insert("UEB_START_MODE".to_string(), Yaml::String("Grade2".to_string()));
prefs.insert("DecimalSeparators".to_string(), Yaml::String(".".to_string()));
prefs.insert("BlockSeparators".to_string(), Yaml::String(", \u{00A0}\u{202F}".to_string()));
return Preferences{ prefs };
}
fn api_defaults() -> Preferences {
let mut prefs = PreferenceHashMap::with_capacity(19);
prefs.insert("TTS".to_string(), Yaml::String("none".to_string()));
prefs.insert("Pitch".to_string(), Yaml::Real("0.0".to_string()));
prefs.insert("Rate".to_string(), Yaml::Real("180.0".to_string()));
prefs.insert("Volume".to_string(), Yaml::Real("100.0".to_string()));
prefs.insert("Voice".to_string(), Yaml::String("none".to_string()));
prefs.insert("Gender".to_string(), Yaml::String("none".to_string()));
prefs.insert("Bookmark".to_string(), Yaml::Boolean(false));
prefs.insert("CapitalLetters_UseWord".to_string(), Yaml::Boolean(true));
prefs.insert("CapitalLetters_Pitch".to_string(), Yaml::Real("0.0".to_string()));
prefs.insert("CapitalLetters_Beep".to_string(), Yaml::Boolean(false));
prefs.insert("IntentErrorRecovery".to_string(), Yaml::String("IgnoreIntent".to_string())); prefs.insert("CheckRuleFiles".to_string(), Yaml::String(
(if cfg!(target_family = "wasm") {"None"} else {"Prefs"}).to_string())); return Preferences{ prefs };
}
fn read_prefs_file(file: &Path, mut base_prefs: Preferences) -> Result<Preferences> {
let file_name = file.to_str().unwrap();
let docs;
match read_to_string_shim(file) {
Err(e) => {
bail!("Couldn't read file {}\n{}", file_name, e);
},
Ok( file_contents) => {
match YamlLoader::load_from_str(&file_contents) {
Err(e) => {
bail!("Yaml parse error ('{}') in preference file {}.", e, file_name);
},
Ok(d) => docs = d,
}
}
}
if docs.len() != 1 {
bail!("MathCAT: error in prefs file '{}'.\nFound {} 'documents' -- should only be 1.", file_name, docs.len());
}
let doc = &docs[0];
if cfg!(debug_assertions) {
verify_keys(doc, "Speech", file_name)?;
verify_keys(doc, "Navigation", file_name)?;
verify_keys(doc, "Braille", file_name)?;
verify_keys(doc, "Other", file_name)?;
}
let prefs = &mut base_prefs.prefs;
add_prefs(prefs, &doc["Speech"], "", file_name);
add_prefs(prefs, &doc["Navigation"], "", file_name);
add_prefs(prefs, &doc["Braille"], "", file_name);
add_prefs(prefs, &doc["Other"], "", file_name);
return Ok( Preferences{ prefs: prefs.to_owned() } );
fn verify_keys(dict: &Yaml, key: &str, file_name: &str) -> Result<()> {
let prefs = &dict[key];
if prefs.is_badvalue() {
bail!("Yaml error in file {}.\nDidn't find '{}' key.", file_name, key);
}
if prefs.as_hash().is_none() {
bail!("Yaml error in file {}.\n'{}' key is not a dictionary. Value found is {}.",
file_name, key, yaml_to_string(dict, 1));
}
return Ok(());
}
fn add_prefs(map: &mut PreferenceHashMap, new_prefs: &Yaml, name_prefix: &str, file_name: &str) {
if new_prefs.is_badvalue() || new_prefs.is_null() || new_prefs.as_hash().is_none() {
return;
}
let new_prefs = new_prefs.as_hash().unwrap();
for (yaml_name, yaml_value) in new_prefs {
let name = as_str_checked(yaml_name);
if let Err(e) = name {
error!("{}", (&e.chain_err(||
format!("name '{}' is not a string in file {}", yaml_to_string(yaml_name, 0), file_name))));
} else {
match yaml_value {
Yaml::Hash(_) => add_prefs(map, yaml_value, &(name.unwrap().to_string() + "_"), file_name),
Yaml::Array(_) => error!("name '{}' has illegal array value {} in file '{}'",
yaml_to_string(yaml_name, 0), yaml_to_string(yaml_value, 0), file_name),
Yaml::String(_) | Yaml::Boolean(_) | Yaml::Integer(_) | Yaml::Real(_) => {
let trimmed_name = name_prefix.to_string() + name.unwrap().trim();
let mut yaml_value = yaml_value.to_owned();
if let Some(value) = yaml_value.as_str() {
yaml_value = Yaml::String(value.to_string());
}
map.insert(trimmed_name, yaml_value);
},
_ => error!("name '{}' has illegal {:#?} value {} in file '{}'",
yaml_to_string(yaml_name, 0), yaml_value, yaml_to_string(yaml_value, 0), file_name),
}
}
}
}
}
#[allow(dead_code)] fn set_string_value(&mut self, name: &str, value: &str) {
self.prefs.insert(name.to_string(), Yaml::String(value.trim().to_string()));
}
#[allow(dead_code)] fn set_bool_value(&mut self, name: &str, value: bool) {
self.prefs.insert(name.to_string(), Yaml::Boolean(value));
}
}
thread_local!{
static DEFAULT_USER_PREFERENCES: Preferences = Preferences::user_defaults();
static DEFAULT_API_PREFERENCES: Preferences = Preferences::api_defaults();
static PREF_MANAGER: Rc<RefCell<PreferenceManager>> =
Rc::new( RefCell::new( PreferenceManager::default() ) );
}
#[derive(Debug, Default)]
pub struct PreferenceManager {
rules_dir: PathBuf, error: String, user_prefs: Preferences, api_prefs: Preferences, sys_prefs_file: Option<FileAndTime>, user_prefs_file: Option<FileAndTime>, intent: PathBuf, speech: PathBuf, overview: PathBuf, navigation: PathBuf, speech_unicode: PathBuf, speech_unicode_full: PathBuf, speech_defs: PathBuf, braille: PathBuf, braille_unicode: PathBuf, braille_unicode_full: PathBuf, braille_defs: PathBuf, }
impl fmt::Display for PreferenceManager {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "PreferenceManager:")?;
if self.error.is_empty() {
writeln!(f, " not initialized!!! Error is {}", &self.error)?;
} else {
writeln!(f, " user prefs:\n{}", self.user_prefs)?;
writeln!(f, " api prefs:\n{}", self.api_prefs)?;
writeln!(f, " style files: {:?}", self.speech.as_path())?;
writeln!(f, " unicode files: {:?}", self.speech_unicode.as_path())?;
writeln!(f, " intent files: {:?}", self.intent.as_path())?;
writeln!(f, " speech definition files: {:?}", self.speech_defs)?;
writeln!(f, " braille definition files: {:?}", self.braille_defs)?;
}
return Ok(());
}
}
impl PreferenceManager {
pub fn initialize(&mut self, rules_dir: PathBuf) -> Result<()> {
#[cfg(not(feature = "include-zip"))]
let rules_dir = match rules_dir.canonicalize() {
Err(e) => bail!("set_rules_dir: could not canonicalize path {}: {}", rules_dir.display(), e.to_string()),
Ok(rules_dir) => rules_dir,
};
self.set_rules_dir(&rules_dir)?;
self.set_preference_files()?;
self.set_all_files(&rules_dir)?;
return Ok( () );
}
pub fn get() -> Rc<RefCell<PreferenceManager>> {
return PREF_MANAGER.with( |pm| pm.clone() );
}
pub fn get_error(&self) -> &str {
return &self.error;
}
pub fn merge_prefs(&self) -> PreferenceHashMap {
let mut merged_prefs = self.user_prefs.prefs.clone();
merged_prefs.extend(self.api_prefs.prefs.clone());
return merged_prefs;
}
fn set_rules_dir(&mut self, rules_dir: &Path) -> Result<()> {
if !is_dir_shim(rules_dir) {
bail!("Unable to find MathCAT Rules directory '{}'", rules_dir.to_string_lossy())
}
self.rules_dir = rules_dir.to_path_buf();
return Ok( () );
}
pub fn get_rules_dir(&self) -> PathBuf {
return self.rules_dir.clone();
}
pub fn set_preference_files(&mut self) -> Result<()> {
if self.api_prefs.prefs.is_empty() {
self.api_prefs = Preferences{ prefs: DEFAULT_API_PREFERENCES.with(|defaults| defaults.prefs.clone()) };
}
let should_update_system_prefs = self.sys_prefs_file.is_none() || !self.sys_prefs_file.as_ref().unwrap().is_up_to_date();
let should_update_user_prefs = self.user_prefs_file.is_none() || !self.user_prefs_file.as_ref().unwrap().is_up_to_date();
if !(should_update_system_prefs || should_update_user_prefs) {
return Ok( () ); }
let mut prefs = Preferences::default();
let mut system_prefs_file = self.rules_dir.to_path_buf();
system_prefs_file.push("prefs.yaml");
if is_file_shim(&system_prefs_file) {
let defaults = DEFAULT_USER_PREFERENCES.with(|defaults| defaults.clone());
prefs = Preferences::read_prefs_file(&system_prefs_file, defaults)?;
self.sys_prefs_file = Some( FileAndTime::new_with_time(system_prefs_file.clone()) );
} else {
error!("MathCAT couldn't open file system preference file '{}'.\nUsing fallback defaults which may be inappropriate.",
system_prefs_file.to_str().unwrap());
};
let mut user_prefs_file = dirs::config_dir();
if let Some(mut user_prefs_file_path_buf) = user_prefs_file {
user_prefs_file_path_buf.push("MathCAT/prefs.yaml");
if is_file_shim(&user_prefs_file_path_buf) {
prefs = Preferences::read_prefs_file(&user_prefs_file_path_buf, prefs)?;
}
self.user_prefs_file = Some( FileAndTime::new_with_time(user_prefs_file_path_buf.clone()) );
user_prefs_file = Some(user_prefs_file_path_buf);
}
if prefs.prefs.is_empty() {
let user_prefs_file_name = match user_prefs_file {
None => "No user config directory".to_string(),
Some(file) => file.to_string_lossy().to_string(),
};
bail!("Didn't find preferences in rule directory ('{}') or user directory ('{}')", &system_prefs_file.to_string_lossy(), user_prefs_file_name);
}
self.set_files_based_on_changes(&prefs)?;
self.user_prefs = prefs;
let language = self.user_prefs.prefs.get("Language").unwrap_or(&DEFAULT_LANG).clone();
let language = language.as_str().unwrap();
self.set_separators(language)?;
return Ok( () );
}
fn set_all_files(&mut self, rules_dir: &Path) -> Result<()> {
let language = self.pref_to_string("Language");
let language = if language.as_str() == "Auto" {"en"} else {language.as_str()}; let language_dir = rules_dir.to_path_buf().join("Languages");
self.set_speech_files(&language_dir, language, None)?;
let braille_code = self.pref_to_string("BrailleCode");
let braille_dir = rules_dir.to_path_buf().join("Braille");
self.set_braille_files(&braille_dir, &braille_code)?;
return Ok(());
}
fn set_speech_files(&mut self, language_dir: &Path, language: &str, new_speech_style: Option<&str>) -> Result<()> {
PreferenceManager::unzip_files(language_dir, language, Some("en"))?;
self.intent = PreferenceManager::find_file(language_dir, language, Some("en"), "intent.yaml")?;
self.overview = PreferenceManager::find_file(language_dir, language, Some("en"), "overview.yaml")?;
self.navigation = PreferenceManager::find_file(language_dir, language, Some("en"), "navigate.yaml")?;
self.speech_unicode = PreferenceManager::find_file(language_dir, language, Some("en"), "unicode.yaml")?;
self.speech_unicode_full = PreferenceManager::find_file(language_dir, language, Some("en"), "unicode-full.yaml")?;
self.speech_defs = PreferenceManager::find_file(language_dir, language, Some("en"), "definitions.yaml")?;
match new_speech_style {
Some(style_name) => self.set_style_file(language_dir, language, style_name)?,
None => self.set_style_file(language_dir, language, &self.pref_to_string("SpeechStyle"))?,
}
return Ok( () );
}
fn set_style_file(&mut self, language_dir: &Path, language: &str, style_file_name: &str) -> Result<()> {
let style_file_name = style_file_name.to_string() + "_Rules.yaml";
self.speech = PreferenceManager::find_file(language_dir, language, Some("en"), &style_file_name)?;
return Ok( () );
}
fn set_braille_files(&mut self, braille_rules_dir: &Path, braille_code_name: &str) -> Result<()> {
PreferenceManager::unzip_files(braille_rules_dir, braille_code_name, Some("UEB"))?;
let braille_file = braille_code_name.to_string() + "_Rules.yaml";
self.braille = PreferenceManager::find_file(braille_rules_dir, braille_code_name, Some("UEB"), &(braille_file))?;
self.braille_unicode = PreferenceManager::find_file(braille_rules_dir, braille_code_name, Some("UEB"), "unicode.yaml")?;
self.braille_unicode_full = PreferenceManager::find_file(braille_rules_dir, braille_code_name, Some("UEB"), "unicode-full.yaml")?;
self.braille_defs = PreferenceManager::find_file(braille_rules_dir, braille_code_name, Some("UEB"), "definitions.yaml")?;
return Ok( () );
}
fn set_files_based_on_changes(&mut self, new_prefs: &Preferences) -> Result<()> {
let old_language = self.user_prefs.prefs.get("Language"); if old_language.is_none() {
return Ok( () ); }
let old_language = old_language.unwrap();
let new_language = new_prefs.prefs.get("Language").unwrap();
debug!("set_files_based_on_changes: old_language={old_language:?}, new_language={new_language:?}");
if old_language != new_language {
let language_dir = self.rules_dir.to_path_buf().join("Languages");
self.set_speech_files(&language_dir, new_language.as_str().unwrap(), None)?; } else {
let old_speech_style = self.user_prefs.prefs.get("SpeechStyle").unwrap();
let new_speech_style = new_prefs.prefs.get("SpeechStyle").unwrap();
let language_dir = self.rules_dir.to_path_buf().join("Languages");
if old_speech_style != new_speech_style {
self.set_speech_files(&language_dir, new_language.as_str().unwrap(), new_speech_style.as_str())?;
}
}
let old_braille_code = self.user_prefs.prefs.get("BrailleCode").unwrap();
let new_braille_code = new_prefs.prefs.get("BrailleCode").unwrap();
if old_braille_code != new_braille_code {
let braille_code_dir = self.rules_dir.to_path_buf().join("Braille");
self.set_braille_files(&braille_code_dir, new_braille_code.as_str().unwrap())?; }
return Ok( () );
}
pub fn unzip_files(path: &Path, lang: &str, default_lang: Option<&str>) -> Result<bool> {
thread_local!{
static UNZIPPED_FILES: RefCell<HashSet<String>> = RefCell::new( HashSet::with_capacity(31));
}
let dir = PreferenceManager::get_language_dir(path, lang, default_lang)?;
let language = if dir.ends_with(lang) {lang} else {dir.file_name().unwrap().to_str().unwrap()};
let zip_file_name = language.to_string() + ".zip";
let zip_file_path = dir.join(&zip_file_name);
let zip_file_string = zip_file_path.to_string_lossy().to_string();
if UNZIPPED_FILES.with( |unzipped_files| unzipped_files.borrow().contains(&zip_file_string)) {
return Ok(false);
}
let result = match zip_extract_shim(&dir, &zip_file_name) {
Err(e) => {
if lang.contains('-') {
let language = lang.split_once('-').unwrap_or((lang, "")).0; PreferenceManager::unzip_files(path, language, default_lang)
.chain_err(|| format!("Couldn't open zip file {zip_file_string} in parent {language}: {e}."))?
} else {
let mut regional_dirs = Vec::new();
find_all_dirs_shim(&dir, &mut regional_dirs);
for dir in regional_dirs {
let language = format!("{}-{}", lang, dir.file_name().unwrap().to_str().unwrap());
if let Ok(result) =PreferenceManager::unzip_files(path, &language, default_lang) {
return Ok(result);
}
}
bail!("Couldn't open zip file {}: {}.", zip_file_string, e)
}
},
Ok(result) => {
result
},
};
UNZIPPED_FILES.with( |unzipped_files| unzipped_files.borrow_mut().insert(zip_file_string.clone()) );
return Ok(result);
}
fn set_separators(&mut self, language_country: &str) -> Result<()> {
static USE_DECIMAL_SEPARATOR: phf::Set<&str> = phf_set! {
"en", "bn", "km", "el-cy", "tr-cy", "zh", "es-do", "ar", "es-sv", "es-gt", "es-hn", "hi", "as", "gu", "kn", "ks",
"ml", "mr", "ne", "or", "pa", "sa", "sd", "ta", "te", "ur", "he", "ja", "sw", "ko", "de-li", "ms", "dv", "mt", "es-mx", "my",
"af-na", "es-ni", "es-pa", "fil", "ms-sg", "si", "th",
"es-419", };
let decimal_separator = self.pref_to_string("DecimalSeparator");
if !["Auto", ",", "."].contains(&decimal_separator.as_str()) {
return Ok( () );
}
if language_country == "Auto" && decimal_separator == "Auto" {
return Ok( () ); }
let language_country = language_country.to_ascii_lowercase();
let language_country = &language_country;
let mut lang_country_split = language_country.split('-');
let language = lang_country_split.next().unwrap_or("");
let country = lang_country_split.next().unwrap_or("");
let mut use_period = decimal_separator == ".";
if decimal_separator == "Auto" {
use_period = USE_DECIMAL_SEPARATOR.contains(language_country) || USE_DECIMAL_SEPARATOR.contains(language);
}
self.user_prefs.prefs.insert("DecimalSeparators".to_string(), Yaml::String((if use_period {"."} else {","}).to_string()));
let mut block_separators = (if use_period {", \u{00A0}\u{202F}"} else {". \u{00A0}\u{202F}"}).to_string();
if country == "ch" || country == "li" { block_separators.push('\'');
}
self.user_prefs.prefs.insert("BlockSeparators".to_string(), Yaml::String(block_separators));
return Ok( () );
}
fn find_file(rules_dir: &Path, lang: &str, default_lang: Option<&str>, file_name: &str) -> Result<PathBuf> {
let lang_dir = PreferenceManager::get_language_dir(rules_dir, lang, default_lang)?;
let mut alternative_style_file = None; let looking_for_style_file = file_name.ends_with("_Rules.yaml");
for os_path in lang_dir.ancestors() { let path = PathBuf::from(os_path).join(file_name);
if is_file_shim(&path) {
if !(file_name == "definitions.yaml" && os_path.ends_with("Rules")) {
return Ok(path);
}
};
if looking_for_style_file && alternative_style_file.is_none() {
if let Ok(alt_file_path) = find_any_style_file(os_path) {
alternative_style_file = Some(alt_file_path);
}
}
if os_path.ends_with("Rules") {
break;
}
}
if let Some(result) = alternative_style_file {
return Ok(result); }
let mut regional_dirs = Vec::new();
find_all_dirs_shim(&lang_dir, &mut regional_dirs);
for dir in regional_dirs {
if find_files_in_dir_that_ends_with_shim(&dir, ".yaml").contains(&file_name.to_string()) {
let path = dir.join(file_name);
if is_file_shim(&path) {
return Ok(path);
}
}
}
if let Some(default_lang) = default_lang {
return PreferenceManager::find_file(rules_dir, default_lang, None, file_name);
}
bail!("Wasn't able to find/read MathCAT required file in directory: {}\n\
Initially looked in there for language specific directory: {}\n\
Looking for file: {}",
rules_dir.to_str().unwrap(), lang, file_name);
fn find_any_style_file(path: &Path) -> Result<PathBuf> {
let rule_files = find_files_in_dir_that_ends_with_shim(path, "_Rules.yaml");
if rule_files.is_empty() {
bail!{"didn't find file"};
} else {
return Ok( path.join(rule_files[0].clone()) );
}
}
}
fn get_language_dir(rules_dir: &Path, lang: &str, default_lang: Option<&str>) -> Result<PathBuf> {
let mut full_path = rules_dir.to_path_buf();
full_path.push(lang.replace('-', std::path::MAIN_SEPARATOR_STR));
for parent in full_path.ancestors() {
if parent == rules_dir {
break;
} else if is_dir_shim(parent) {
return Ok(parent.to_path_buf());
}
}
match default_lang {
Some(default_lang) => {
warn!("Couldn't find rules for language {lang}, ");
return PreferenceManager::get_language_dir(rules_dir, default_lang, None);
},
None => {
bail!("Wasn't able to find/read directory for language {}\n
Wasn't able to find/read MathCAT default language directory: {}",
lang, rules_dir.join(default_lang.unwrap_or("")).as_os_str().to_str().unwrap());
}
}
}
pub fn get_rule_file(&self, name: &RulesFor) -> &Path {
if !self.error.is_empty() {
panic!("Internal error: get_rule_file called on invalid PreferenceManager -- error message\n{}", &self.error);
};
let files = match name {
RulesFor::Intent => &self.intent,
RulesFor::Speech => &self.speech,
RulesFor::OverView => &self.overview,
RulesFor::Navigation => &self.navigation,
RulesFor::Braille => &self.braille,
};
return files.as_path();
}
pub fn get_speech_unicode_file(&self) ->(&Path, &Path) {
if !self.error.is_empty() {
panic!("Internal error: get_speech_unicode_file called on invalid PreferenceManager -- error message\n{}", &self.error);
};
return (self.speech_unicode.as_path(), self.speech_unicode_full.as_path());
}
pub fn get_braille_unicode_file(&self) -> (&Path, &Path) {
if !self.error.is_empty() {
panic!("Internal error: get_braille_unicode_file called on invalid PreferenceManager -- error message\n{}", &self.error);
};
return (self.braille_unicode.as_path(), self.braille_unicode_full.as_path());
}
pub fn get_definitions_file(&self, use_speech_defs: bool) -> &Path {
if !self.error.is_empty() {
panic!("Internal error: get_definitions_file called on invalid PreferenceManager -- error message\n{}", &self.error);
};
let defs_file = if use_speech_defs {&self.speech_defs} else {&self.braille_defs};
return defs_file;
}
pub fn get_tts(&self) -> TTS {
if !self.error.is_empty() {
panic!("Internal error: get_tts called on invalid PreferenceManager -- error message\n{}", &self.error);
};
return match self.pref_to_string("TTS").as_str().to_ascii_lowercase().as_str() {
"none" => TTS::None,
"ssml" => TTS::SSML,
"sapi5" => TTS::SAPI5,
_ => {
warn!("found unknown value for TTS: '{}'", self.pref_to_string("TTS").as_str());
TTS::None
}
}
}
pub fn set_string_pref(&mut self, key: &str, value: &str) -> Result<()> {
if !self.error.is_empty() {
panic!("Internal error: set_string_pref called on invalid PreferenceManager -- error message\n{}", &self.error);
};
let mut is_user_pref = true;
if let Some(pref_value) = self.api_prefs.prefs.get(key) {
if pref_value.as_str().unwrap() != value {
is_user_pref = false;
self.reset_files_from_preference_change(key, value)?;
}
} else if let Some(pref_value) = self.user_prefs.prefs.get(key) {
if pref_value.as_str().unwrap() != value {
self.reset_files_from_preference_change(key, value)?;
}
} else {
bail!("{} is an unknown MathCAT preference!", key);
}
if is_user_pref {
let current_decimal_separator = self.user_prefs.prefs.get("DecimalSeparator").unwrap().clone();
let current_decimal_separator = current_decimal_separator.as_str().unwrap();
let is_decimal_separators_changed = key == "DecimalSeparator" && current_decimal_separator != value;
let is_language_changed = key == "Language" && self.user_prefs.prefs.get("Language").unwrap().as_str().unwrap() != value;
self.user_prefs.prefs.insert(key.to_string(), Yaml::String(value.to_string()));
if is_decimal_separators_changed || (current_decimal_separator == "Auto" && is_language_changed) {
let language = self.user_prefs.prefs.get("Language").unwrap_or(&DEFAULT_LANG).clone();
let language = language.as_str().unwrap();
self.set_separators(language)?;
}
} else {
self.api_prefs.prefs.insert(key.to_string(), Yaml::String(value.to_string()));
}
return Ok( () );
}
fn reset_files_from_preference_change(&mut self, changed_pref: &str, changed_value: &str) -> Result<()> {
if changed_pref == "Language" && changed_value == "Auto" {
self.api_prefs.prefs.insert("LanguageAuto".to_string(),
self.api_prefs.prefs.get("Language").unwrap_or(&DEFAULT_LANG).clone() );
return Ok( () );
}
let changed_pref = if changed_pref == "LanguageAuto" {"Language"} else {changed_pref};
let language_dir = self.rules_dir.to_path_buf().join("Languages");
match changed_pref {
"Language" => {
self.set_speech_files(&language_dir, changed_value, None)?
},
"SpeechStyle" => {
let language = self.pref_to_string("Language");
let language = if language.as_str() == "Auto" {"en"} else {language.as_str()}; self.set_style_file(&language_dir, language, changed_value)?
},
"BrailleCode" => {
let braille_dir = self.rules_dir.to_path_buf().join("Braille");
self.set_braille_files(&braille_dir, changed_value)?
},
_ => (),
}
return Ok( () );
}
pub fn set_api_float_pref(&mut self, key: &str, value: f64) {
if !self.error.is_empty() {
panic!("Internal error: set_api_float_pref called on invalid PreferenceManager -- error message\n{}", &self.error);
};
self.api_prefs.prefs.insert(key.to_string(), Yaml::Real(value.to_string()));
}
pub fn set_api_boolean_pref(&mut self, key: &str, value: bool) {
if !self.error.is_empty() {
panic!("Internal error: set_api_boolean_pref called on invalid PreferenceManager -- error message\n{}", &self.error);
};
self.api_prefs.prefs.insert(key.to_string(), Yaml::Boolean(value));
}
pub fn get_rate(&self) -> f64 {
if !self.error.is_empty() {
panic!("Internal error: get_rate called on invalid PreferenceManager -- error message\n{}", &self.error);
};
return match &self.pref_to_string("Rate").parse::<f64>() {
Ok(val) => *val,
Err(_) => {
warn!("Rate ('{}') can't be converted to a floating point number", &self.pref_to_string("Rate"));
DEFAULT_API_PREFERENCES.with(|defaults| defaults.prefs["Rate"].as_f64().unwrap())
}
};
}
pub fn get_api_prefs(&self) -> &Preferences {
return &self.api_prefs;
}
pub fn pref_to_string(&self, name: &str) -> String {
let mut value = self.api_prefs.prefs.get(name);
if value.is_none() {
value = self.user_prefs.prefs.get(name);
}
return match value {
None => NO_PREFERENCE.to_string(),
Some(v) => match v {
Yaml::String(s) => s.clone(),
Yaml::Boolean(b) => b.to_string(),
Yaml::Integer(i) => i.to_string(),
Yaml::Real(s) => s.clone(),
_ => NO_PREFERENCE.to_string(), }
}
}
pub fn set_user_prefs(&mut self, key: &str, value: &str) -> Result<()> {
if !self.error.is_empty() {
panic!("Internal error: set_user_prefs called on invalid PreferenceManager -- error message\n{}", &self.error);
};
self.reset_files_from_preference_change(key, value)?;
let is_decimal_separators_changed = key == "DecimalSeparator" && self.user_prefs.prefs.get("DecimalSeparator").unwrap().as_str().unwrap() != value;
let is_language_changed = key == "Language" && self.user_prefs.prefs.get("Language").unwrap().as_str().unwrap() != value;
self.user_prefs.prefs.insert(key.to_string(), Yaml::String(value.to_string()));
if is_decimal_separators_changed || is_language_changed {
let language = self.user_prefs.prefs.get("Language").unwrap_or(&DEFAULT_LANG).clone();
let language = language.as_str().unwrap();
self.set_separators(language)?;
}
return Ok(());
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use crate::init_logger;
use super::*;
fn abs_rules_dir_path() -> PathBuf {
return PathBuf::from(super::super::abs_rules_dir_path());
}
fn rel_path<'a>(rules_dir: &'a Path, path: &'a Path) -> &'a Path {
let stripped_path = path.strip_prefix(rules_dir).unwrap();
return stripped_path
}
#[test]
fn separators() {
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
pref_manager.set_user_prefs("Language", "en").unwrap();
pref_manager.set_user_prefs("DecimalSeparator", "Auto").unwrap();
assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ".");
assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ", \u{00A0}\u{202F}");
pref_manager.set_user_prefs("Language", "sv").unwrap();
assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ",");
assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ". \u{00A0}\u{202F}");
pref_manager.set_user_prefs("Language", "es").unwrap();
assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ",");
assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ". \u{00A0}\u{202F}");
pref_manager.set_user_prefs("Language", "es-mx").unwrap();
assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ".");
assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ", \u{00A0}\u{202F}");
pref_manager.set_user_prefs("DecimalSeparator", ",").unwrap();
assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ",");
assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ". \u{00A0}\u{202F}");
pref_manager.set_user_prefs("DecimalSeparator", ".").unwrap();
assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ".");
assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ", \u{00A0}\u{202F}");
pref_manager.set_user_prefs("DecimalSeparator", ";").unwrap();
assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ".");
assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), ", \u{00A0}\u{202F}");
pref_manager.set_user_prefs("DecimalSeparators", ",").unwrap();
pref_manager.set_user_prefs("BlockSeparators", " ").unwrap();
pref_manager.set_user_prefs("DecimalSeparator", "None").unwrap();
assert_eq!(&pref_manager.pref_to_string("DecimalSeparators"), ",");
assert_eq!(&pref_manager.pref_to_string("BlockSeparators"), " ");
});
}
#[test]
fn find_simple_style() {
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
pref_manager.set_user_prefs("Language", "en").unwrap();
pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
assert_eq!(&pref_manager.pref_to_string("Language"), "en");
assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "ClearSpeak");
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/en/ClearSpeak_Rules.yaml"));
});
}
cfg_if::cfg_if! {if #[cfg(not(feature = "include-zip"))] {
#[test]
fn find_style_other_language() {
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
pref_manager.set_user_prefs("Language", "en").unwrap();
pref_manager.set_user_prefs("SpeechStyle", "SimpleSpeak").unwrap();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/en/SimpleSpeak_Rules.yaml"));
pref_manager.set_user_prefs("Language", "zz").unwrap();
assert_eq!(&pref_manager.pref_to_string("Language"), "zz");
assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "SimpleSpeak");
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/SimpleSpeak_Rules.yaml"));
pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "ClearSpeak");
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
pref_manager.set_user_prefs("SpeechStyle", "SimpleSpeak").unwrap();
assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "SimpleSpeak");
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/SimpleSpeak_Rules.yaml"));
});
}
#[test]
fn find_regional_overrides() {
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
pref_manager.set_user_prefs("Language", "zz-aa").unwrap();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/aa/ClearSpeak_Rules.yaml"));
pref_manager.set_user_prefs("SpeechStyle", "SimpleSpeak").unwrap();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/SimpleSpeak_Rules.yaml"));
});
}
#[test]
fn find_style_no_sublanguage() {
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
pref_manager.set_user_prefs("Language", "zz-ab").unwrap();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
});
}
#[test]
fn found_all_files() {
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
pref_manager.set_user_prefs("Language", "zz-aa").unwrap();
pref_manager.set_user_prefs("BrailleCode", "UEB").unwrap();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.intent.as_path()), PathBuf::from("intent.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.overview.as_path()), PathBuf::from("Languages/zz/overview.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_defs.as_path()), PathBuf::from("Languages/zz/aa/definitions.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/aa/ClearSpeak_Rules.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_unicode.as_path()), PathBuf::from("Languages/zz/aa/unicode.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_unicode_full.as_path()), PathBuf::from("Languages/zz/unicode-full.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille.as_path()), PathBuf::from("Braille/UEB/UEB_Rules.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_unicode.as_path()), PathBuf::from("Braille/UEB/unicode.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_unicode_full.as_path()), PathBuf::from("Braille/UEB/unicode-full.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_defs.as_path()), PathBuf::from("Braille/UEB/definitions.yaml"));
pref_manager.set_user_prefs("Language", "zz-ab").unwrap();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.intent.as_path()), PathBuf::from("intent.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.overview.as_path()), PathBuf::from("Languages/zz/overview.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_defs.as_path()), PathBuf::from("Languages/zz/definitions.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_unicode.as_path()), PathBuf::from("Languages/zz/unicode.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech_unicode_full.as_path()), PathBuf::from("Languages/zz/unicode-full.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille.as_path()), PathBuf::from("Braille/UEB/UEB_Rules.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_unicode.as_path()), PathBuf::from("Braille/UEB/unicode.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_unicode_full.as_path()), PathBuf::from("Braille/UEB/unicode-full.yaml"));
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.braille_defs.as_path()), PathBuf::from("Braille/UEB/definitions.yaml"));
})
}
#[test]
fn test_prefs() {
PREF_MANAGER.with(|pref_manager| {
{
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
pref_manager.set_user_prefs("Language", "en").unwrap();
pref_manager.set_user_prefs("ClearSpeak_AbsoluteValue", "Determinant").unwrap();
pref_manager.set_user_prefs("ResetNavMode", "true").unwrap();
pref_manager.set_user_prefs("BrailleCode", "Nemeth").unwrap();
assert_eq!(pref_manager.pref_to_string("Language").as_str(), "en");
assert_eq!(pref_manager.pref_to_string("SubjectArea").as_str(), "General");
assert_eq!(pref_manager.pref_to_string("ClearSpeak_AbsoluteValue").as_str(), "Determinant");
assert_eq!(pref_manager.pref_to_string("ResetNavMode").as_str(), "true");
assert_eq!(pref_manager.pref_to_string("BrailleCode").as_str(), "Nemeth");
assert_eq!(pref_manager.pref_to_string("X_Y_Z").as_str(), NO_PREFERENCE);
}
{
use crate::interface::{set_preference, get_preference};
set_preference("Language".to_string(), "zz".to_string()).unwrap();
set_preference("ClearSpeak_AbsoluteValue".to_string(), "Cardinality".to_string()).unwrap();
set_preference("Overview".to_string(), "true".to_string()).unwrap();
set_preference("BrailleCode".to_string(), "UEB".to_string()).unwrap();
assert_eq!(&get_preference("Language".to_string()).unwrap(), "zz");
assert_eq!(&get_preference("ClearSpeak_AbsoluteValue".to_string()).unwrap(), "Cardinality");
assert_eq!(&get_preference("Overview".to_string()).unwrap(), "true");
assert_eq!(&get_preference("BrailleCode".to_string()).unwrap(), "UEB");
assert!(&get_preference("X_Y_Z".to_string()).is_err());
}
});
}
#[test]
fn test_language_change() {
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
});
crate::interface::set_preference("Language".to_string(), "en".to_string()).unwrap();
crate::interface::set_preference("SpeechStyle".to_string(), "ClearSpeak".to_string()).unwrap();
PREF_MANAGER.with(|pref_manager| {
let pref_manager = pref_manager.borrow_mut();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.get_rule_file(&RulesFor::Speech)), PathBuf::from("Languages/en/ClearSpeak_Rules.yaml"));
});
crate::interface::set_preference("Language".to_string(), "zz".to_string()).unwrap();
PREF_MANAGER.with(|pref_manager| {
let pref_manager = pref_manager.borrow_mut();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.get_rule_file(&RulesFor::Speech)), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
});
}
#[test]
fn test_speech_style_change() {
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
pref_manager.set_user_prefs("Language", "en").unwrap();
pref_manager.set_user_prefs("SpeechStyle", "ClearSpeak").unwrap();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.get_rule_file(&RulesFor::Speech)), PathBuf::from("Languages/en/ClearSpeak_Rules.yaml"));
pref_manager.set_user_prefs("SpeechStyle", "SimpleSpeak").unwrap();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.get_rule_file(&RulesFor::Speech)), PathBuf::from("Languages/en/SimpleSpeak_Rules.yaml"));
});
}
#[test]
fn test_some_changes() {
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
pref_manager.set_user_prefs("Verbosity", "Terse").unwrap();
assert_eq!(&pref_manager.pref_to_string("Verbosity"), "Terse");
pref_manager.set_user_prefs("BrailleCode", "UEB").unwrap();
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.get_rule_file(&RulesFor::Braille)), PathBuf::from("Braille/UEB/UEB_Rules.yaml"));
let merged_prefs = pref_manager.merge_prefs();
assert_eq!(merged_prefs.get("Verbosity").unwrap().as_str().unwrap(), "Terse");
});
crate::interface::set_preference("NavVerbosity".to_string(), "Terse".to_string()).unwrap();
PREF_MANAGER.with(|pref_manager| {
let pref_manager = pref_manager.borrow_mut();
let merged_prefs = pref_manager.merge_prefs();
assert_eq!(merged_prefs.get("NavVerbosity").unwrap().as_str().unwrap(), "Terse");
});
}
#[test]
#[ignore] fn test_up_to_date() {
use std::fs;
use std::thread::sleep;
use std::time::Duration;
use crate::interface;
PREF_MANAGER.with(|pref_manager| {
let mut pref_manager = pref_manager.borrow_mut();
pref_manager.initialize(abs_rules_dir_path()).unwrap();
assert_eq!(&pref_manager.pref_to_string("SpeechStyle"), "ClearSpeak");
assert_eq!(rel_path(&pref_manager.rules_dir, pref_manager.speech.as_path()), PathBuf::from("Languages/zz/ClearSpeak_Rules.yaml"));
});
interface::set_mathml("<math><mo>+</mo><mn>10</mn></math>".to_string()).unwrap();
assert_eq!(interface::get_spoken_text().unwrap(), "ClearSpeak positive from zz 10");
let mut file_path = PathBuf::default();
let mut contents = vec![];
PREF_MANAGER.with(|pref_manager| {
let pref_manager = pref_manager.borrow();
if let Some(file_name) = pref_manager.user_prefs_file.as_ref().unwrap().debug_get_file() {
file_path = PathBuf::from(file_name);
contents = fs::read(&file_path).expect(&format!("Failed to write file {} during test", file_name));
let changed_contents = String::from_utf8(contents.clone()).unwrap()
.replace("SpeechStyle: ClearSpeak", "SpeechStyle: SimpleSpeak");
fs::write(&file_path, changed_contents).unwrap();
sleep(Duration::from_millis(5)); }
});
assert_eq!(interface::get_spoken_text().unwrap(), "SimpleSpeak positive from zz 10");
fs::write(&file_path, contents).unwrap();
}
}}
}