#![allow(clippy::needless_return)]
use std::path::PathBuf;
use std::collections::HashMap;
use std::cell::{RefCell, RefMut};
use sxd_document::dom::{ChildOfElement, Document, Element};
use sxd_document::{Package, QName};
use sxd_xpath::context::Evaluation;
use sxd_xpath::{Context, Factory, Value, XPath};
use sxd_xpath::nodeset::Node;
use std::fmt;
use crate::errors::*;
use crate::prefs::*;
use yaml_rust::{YamlLoader, Yaml, yaml::Hash};
use crate::tts::*;
use crate::pretty_print::{mml_to_string, yaml_to_string};
use std::path::Path;
use std::rc::Rc;
use crate::shim_filesystem::read_to_string_shim;
use crate::canonicalize::{as_element, create_mathml_element, set_mathml_name, name};
pub fn intent_from_mathml<'a, 'm>(mathml: Element<'a>, doc: Document<'m>) -> Result<Element<'m>> {
let intent_tree = intent_rules(&INTENT_RULES, doc, mathml)?;
doc.root().append_child(intent_tree);
return Ok(intent_tree);
}
pub fn speak_intent(mathml: Element) -> Result<String> {
return speak_rules(&SPEECH_RULES, mathml);
}
pub fn overview_mathml(mathml: Element) -> Result<String> {
return speak_rules(&OVERVIEW_RULES, mathml);
}
fn intent_rules<'c, 'm>(rules: &'static std::thread::LocalKey<RefCell<SpeechRules>>, doc: Document<'m>, mathml: Element<'c>) -> Result<Element<'m>> {
SpeechRules::update();
rules.with(|rules| {
rules.borrow_mut().read_files()?;
let rules = rules.borrow();
let mut rules_with_context = SpeechRulesWithContext::new(&rules, doc, "".to_string());
let intent = rules_with_context.match_pattern::<Element<'m>>(mathml)
.chain_err(|| "Pattern match/replacement failure!")?;
if name(&intent) == "TEMP_NAME" { assert_eq!(intent.children().len(), 1);
return Ok( as_element(intent.children()[0]) );
} else {
return Ok(intent);
}
})
}
fn speak_rules(rules: &'static std::thread::LocalKey<RefCell<SpeechRules>>, mathml: Element) -> Result<String> {
SpeechRules::update();
rules.with(|rules| {
rules.borrow_mut().read_files()?;
let rules = rules.borrow();
let new_package = Package::new();
let mut rules_with_context = SpeechRulesWithContext::new(&rules, new_package.as_document(), "".to_string());
let speech_string = rules_with_context.match_pattern::<String>(mathml)
.chain_err(|| "Pattern match/replacement failure!")?;
return Ok( rules.pref_manager.borrow().get_tts()
.merge_pauses(remove_optional_indicators(
&speech_string.replace(CONCAT_STRING, "")
.replace(CONCAT_INDICATOR, "")
)
.trim()) );
})
}
pub fn yaml_to_type(yaml: &Yaml) -> String {
return match yaml {
Yaml::Real(v)=> format!("real='{:#}'", v),
Yaml::Integer(v)=> format!("integer='{:#}'", v),
Yaml::String(v)=> format!("string='{:#}'", v),
Yaml::Boolean(v)=> format!("boolean='{:#}'", v),
Yaml::Array(v)=> match v.len() {
0 => "array with no entries".to_string(),
1 => format!("array with the entry: {}", yaml_to_type(&v[0])),
_ => format!("array with {} entries. First entry: {}", v.len(), yaml_to_type(&v[0])),
}
Yaml::Hash(h)=> {
let first_pair =
if h.is_empty() {
"no pairs".to_string()
} else {
let (key, val) = h.iter().next().unwrap();
format!("({}, {})", yaml_to_type(key), yaml_to_type(val))
};
format!("dictionary with {} pair{}. A pair: {}", h.len(), if h.len()==1 {""} else {"s"}, first_pair)
}
Yaml::Alias(_)=> "Alias".to_string(),
Yaml::Null=> "Null".to_string(),
Yaml::BadValue=> "BadValue".to_string(),
}
}
fn yaml_type_err(yaml: &Yaml, str: &str) -> String {
return format!("Expected {}, found {}", str, yaml_to_type(yaml));
}
fn find_str<'a>(dict: &'a Yaml, key: &'a str) -> Option<&'a str> {
return dict[key].as_str();
}
pub fn as_hash_checked(value: &Yaml) -> Result<&Hash> {
let result = value.as_hash();
let result = result.ok_or_else(|| yaml_type_err(value, "hashmap"))?;
return Ok( result );
}
pub fn as_vec_checked(value: &Yaml) -> Result<&Vec<Yaml>> {
let result = value.as_vec();
let result = result.ok_or_else(|| yaml_type_err(value, "array"))?;
return Ok( result );
}
pub fn as_str_checked(yaml: &Yaml) -> Result<&str> {
return Ok( yaml.as_str().ok_or_else(|| yaml_type_err(yaml, "string"))? );
}
pub const CONCAT_INDICATOR: &str = "\u{F8FE}";
pub const CONCAT_STRING: &str = " \u{F8FE}";
const OPTIONAL_INDICATOR: &str = "\u{F8FD}";
const OPTIONAL_INDICATOR_LEN: usize = OPTIONAL_INDICATOR.len();
pub fn remove_optional_indicators(str: &str) -> String {
return str.replace(OPTIONAL_INDICATOR, "");
}
pub fn compile_rule<F>(str: &str, mut build_fn: F) -> Result<()> where
F: FnMut(&Yaml) -> Result<()> {
let docs = YamlLoader::load_from_str(str);
match docs {
Err(e) => {
bail!("Parse error!!: {}", e);
},
Ok(docs) => {
if docs.len() != 1 {
bail!("Didn't find rules!");
}
return build_fn(&docs[0]);
}
}
}
fn process_include<F>(current_file: &Path, new_file_name: &str, mut read_new_file: F) -> Result<()>
where F: FnMut(&Path) -> Result<()> {
let parent_path = current_file.parent();
if parent_path.is_none() {
bail!("Internal error: {:?} is not a valid file name", current_file);
}
let mut new_file = parent_path.unwrap().to_path_buf();
new_file.push(new_file_name);
info!("...processing include: {}...", new_file_name);
let new_file = match crate::shim_filesystem::canonicalize_shim(new_file.as_path()) {
Ok(buf) => buf,
Err(msg) => bail!("-include: constructed file name '{}' causes error '{}'",
new_file.to_str().unwrap(), msg),
};
return read_new_file(new_file.as_path());
}
pub trait TreeOrString<'c, 'm:'c, T> {
fn from_element(e: Element<'m>) -> Result<T>;
fn from_string(s: String, doc: Document<'m>) -> Result<T>;
fn replace_tts<'s:'c, 'r>(tts: &TTS, command: &TTSCommandRule, prefs: &PreferenceManager, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T>;
fn replace<'s:'c, 'r>(ra: &ReplacementArray, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T>;
fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<T>;
fn highlight_braille(braille: T, highlight_style: String) -> T;
}
impl<'c, 'm:'c> TreeOrString<'c, 'm, String> for String {
fn from_element(_e: Element<'m>) -> Result<String> {
bail!("from_element not allowed for strings");
}
fn from_string(s: String, _doc: Document<'m>) -> Result<String> {
return Ok(s);
}
fn replace_tts<'s:'c, 'r>(tts: &TTS, command: &TTSCommandRule, prefs: &PreferenceManager, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<String> {
return tts.replace_string(command, prefs, rules_with_context, mathml);
}
fn replace<'s:'c, 'r>(ra: &ReplacementArray, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<String> {
return ra.replace_array_string(rules_with_context, mathml);
}
fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<String> {
return rules.replace_nodes_string(nodes, mathml);
}
fn highlight_braille(braille: String, highlight_style: String) -> String {
return SpeechRulesWithContext::highlight_braille_string(braille, highlight_style);
}
}
impl<'c, 'm:'c> TreeOrString<'c, 'm, Element<'m>> for Element<'m> {
fn from_element(e: Element<'m>) -> Result<Element<'m>> {
return Ok(e);
}
fn from_string(s: String, doc: Document<'m>) -> Result<Element<'m>> {
let leaf = create_mathml_element(&doc, "mi");
leaf.set_text(&s);
return Ok(leaf);
}
fn replace_tts<'s:'c, 'r>(_tts: &TTS, _command: &TTSCommandRule, _prefs: &PreferenceManager, _rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, _mathml: Element<'c>) -> Result<Element<'m>> {
bail!("Internal error: applying a TTS rule to a tree");
}
fn replace<'s:'c, 'r>(ra: &ReplacementArray, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<Element<'m>> {
return ra.replace_array_tree(rules_with_context, mathml);
}
fn replace_nodes<'s:'c, 'r>(rules: &'r mut SpeechRulesWithContext<'c, 's,'m>, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<Element<'m>> {
return rules.replace_nodes_tree(nodes, mathml);
}
fn highlight_braille(_braille: Element<'c>, _highlight_style: String) -> Element<'m> {
panic!("Internal error: highlight_braille called on a tree");
}
}
#[derive(Debug, Clone)]
#[allow(clippy::upper_case_acronyms)]
enum Replacement {
Text(String),
XPath(MyXPath),
Intent(Box<Intent>),
Test(Box<TestArray>),
TTS(Box<TTSCommandRule>),
With(Box<With>),
SetVariables(Box<SetVariables>),
Insert(Box<InsertChildren>),
Translate(TranslateExpression),
}
impl fmt::Display for Replacement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "{}",
match self {
Replacement::Test(c) => c.to_string(),
Replacement::Text(t) => format!("t: \"{}\"", t),
Replacement::XPath(x) => x.to_string(),
Replacement::Intent(i) => i.to_string(),
Replacement::TTS(t) => t.to_string(),
Replacement::With(w) => w.to_string(),
Replacement::SetVariables(v) => v.to_string(),
Replacement::Insert(ic) => ic.to_string(),
Replacement::Translate(x) => x.to_string(),
}
);
}
}
impl Replacement {
fn build(replacement: &Yaml) -> Result<Replacement> {
let dictionary = replacement.as_hash();
if dictionary.is_none() {
bail!(" expected a key/value pair. Found {}.", yaml_to_string(replacement, 0));
};
let dictionary = dictionary.unwrap();
if dictionary.is_empty() {
bail!("No key/value pairs found for key 'replace'.\n\
Suggestion: are the following lines indented properly?");
}
if dictionary.len() > 1 {
bail!("Should only be one key/value pair for the replacement.\n \
Suggestion: are the following lines indented properly?\n \
The key/value pairs found are\n{}", yaml_to_string(replacement, 2));
}
let (key, value) = dictionary.iter().next().unwrap();
let key = key.as_str().ok_or("replacement key(e.g, 't') is not a string")?;
match key {
"t" | "T" => {
return Ok( Replacement::Text( as_str_checked(value)?.to_string() ) );
},
"ct" | "CT" => {
return Ok( Replacement::Text( CONCAT_INDICATOR.to_string() + as_str_checked(value)? ) );
},
"ot" | "OT" => {
return Ok( Replacement::Text( OPTIONAL_INDICATOR.to_string() + as_str_checked(value)? + OPTIONAL_INDICATOR ) );
},
"x" => {
return Ok( Replacement::XPath( MyXPath::build(value)
.chain_err(|| "while trying to evaluate value of 'x:'")? ) );
},
"pause" | "rate" | "pitch" | "volume" | "gender" | "voice" | "spell" | "SPELL" | "bookmark" | "pronounce" | "PRONOUNCE" => {
return Ok( Replacement::TTS( TTS::build(&key.to_ascii_lowercase(), value)? ) );
},
"intent" => {
return Ok( Replacement::Intent( Intent::build(value)? ) );
},
"test" => {
return Ok( Replacement::Test( Box::new( TestArray::build(value)? ) ) );
},
"with" => {
return Ok( Replacement::With( With::build(value)? ) );
},
"set_variables" => {
return Ok( Replacement::SetVariables( SetVariables::build(value)? ) );
},
"insert" => {
return Ok( Replacement::Insert( InsertChildren::build(value)? ) );
},
"translate" => {
return Ok( Replacement::Translate( TranslateExpression::build(value)
.chain_err(|| "while trying to evaluate value of 'speak:'")? ) );
},
_ => {
bail!("Unknown 'replace' command ({}) with value: {}", key, yaml_to_string(value, 0));
}
}
}
}
#[derive(Debug, Clone)]
struct InsertChildren {
xpath: MyXPath, replacements: ReplacementArray, }
impl fmt::Display for InsertChildren {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "InsertChildren:\n nodes {}\n replacements {}", self.xpath, &self.replacements);
}
}
impl<'r> InsertChildren {
fn build(insert: &Yaml) -> Result<Box<InsertChildren>> {
if insert.as_hash().is_none() {
bail!("")
}
let nodes = &insert["nodes"];
if nodes.is_badvalue() {
bail!("Missing 'nodes' as part of 'insert'.\n \
Suggestion: add 'nodes:' or if present, indent so it is contained in 'insert'");
}
let nodes = as_str_checked(nodes)?;
let replace = &insert["replace"];
if replace.is_badvalue() {
bail!("Missing 'replace' as part of 'insert'.\n \
Suggestion: add 'replace:' or if present, indent so it is contained in 'insert'");
}
return Ok( Box::new( InsertChildren {
xpath: MyXPath::new(nodes.to_string())?,
replacements: ReplacementArray::build(replace).chain_err(|| "'replace:'")?,
} ) );
}
fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
let result = self.xpath.evaluate(&rules_with_context.context_stack.base, mathml)
.chain_err(||format!("in '{}' replacing after pattern match", &self.xpath.rc.string) )?;
match result {
Value::Nodeset(nodes) => {
if nodes.size() == 0 {
bail!("During replacement, no matching element found");
};
let n_nodes = nodes.size();
let mut expanded_result = Vec::with_capacity(n_nodes + (n_nodes+1)*self.replacements.replacements.len());
expanded_result.push(
Replacement::XPath(
MyXPath::new(format!("{}[{}]", self.xpath.rc.string , 1))?
)
);
for i in 2..n_nodes+1 {
expanded_result.extend_from_slice(&self.replacements.replacements);
expanded_result.push(
Replacement::XPath(
MyXPath::new(format!("{}[{}]", self.xpath.rc.string , i))?
)
);
}
let replacements = ReplacementArray{ replacements: expanded_result };
return replacements.replace(rules_with_context, mathml);
},
Value::String(t) => { return T::from_string(rules_with_context.replace_chars(&t, mathml)?, rules_with_context.doc); },
Value::Number(num) => { return T::from_string( num.to_string(), rules_with_context.doc ); },
Value::Boolean(b) => { return T::from_string( b.to_string(), rules_with_context.doc ); }, }
}
}
#[derive(Debug, Clone)]
struct Intent {
name: Option<String>, xpath: Option<MyXPath>, children: ReplacementArray, }
impl fmt::Display for Intent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = if self.name.is_some() {
self.name.as_ref().unwrap().to_string()
} else {
self.xpath.as_ref().unwrap().to_string()
};
return write!(f, "intent: {}: {}\n children: {}",
if self.name.is_some() {"name"} else {"xpath-name"}, name, &self.children);
}
}
impl<'r> Intent {
fn build(yaml_dict: &Yaml) -> Result<Box<Intent>> {
if yaml_dict.as_hash().is_none() {
bail!("Array found for contents of 'intent' -- should be dictionary with keys 'name' and 'children'")
}
let name = &yaml_dict["name"];
let xpath_name = &yaml_dict["xpath-name"];
if name.is_badvalue() && xpath_name.is_badvalue(){
bail!("Missing 'name' or 'xpath-name' as part of 'intent'.\n \
Suggestion: add 'name:' or if present, indent so it is contained in 'intent'");
}
let replace = &yaml_dict["children"];
if replace.is_badvalue() {
bail!("Missing 'children' as part of 'intent'.\n \
Suggestion: add 'children:' or if present, indent so it is contained in 'intent'");
}
return Ok( Box::new( Intent {
name: if name.is_badvalue() {None} else {Some(as_str_checked(name).chain_err(|| "'name'")?.to_string())},
xpath: if xpath_name.is_badvalue() {None} else {Some(MyXPath::build(xpath_name).chain_err(|| "'intent'")?)},
children: ReplacementArray::build(replace).chain_err(|| "'children:'")?,
} ) );
}
fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
let result = self.children.replace::<Element<'m>>(rules_with_context, mathml)
.chain_err(||"replacing inside 'intent'")?;
let result = lift_children(result);
if let Some(name) = &self.name {
set_mathml_name(result, name.as_str());
} else if let Some(my_xpath) = &self.xpath{ let xpath_value = my_xpath.evaluate(rules_with_context.get_context(), mathml)?;
match xpath_value {
Value::String(name) => {
set_mathml_name(result, name.as_str())
},
_ => bail!("'xpath-name' value '{}' was not a string", &my_xpath),
}
} else {
panic!("Intent::replace: internal error -- neither 'name' nor 'xpath' is set");
};
for attr in mathml.attributes() {
result.set_attribute_value(attr.name(), attr.value());
}
return T::from_element(result);
fn lift_children(result: Element) -> Element {
result.replace_children(
result.children().iter()
.map(|&child_of_element| {
match child_of_element {
ChildOfElement::Element(child) => {
if name(&child) == "TEMP_NAME" {
assert_eq!(child.children().len(), 1);
child.children()[0]
} else {
child_of_element
}
},
_ => child_of_element, }
})
.collect::<Vec<ChildOfElement>>()
);
return result;
}
}
}
#[derive(Debug, Clone)]
struct With {
variables: VariableDefinitions, replacements: ReplacementArray, }
impl fmt::Display for With {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "with:\n variables: {}\n replace: {}", &self.variables, &self.replacements);
}
}
impl<'r> With {
fn build(vars_replacements: &Yaml) -> Result<Box<With>> {
if vars_replacements.as_hash().is_none() {
bail!("Array found for contents of 'with' -- should be dictionary with keys 'variables' and 'replace'")
}
let var_defs = &vars_replacements["variables"];
if var_defs.is_badvalue() {
bail!("Missing 'variables' as part of 'with'.\n \
Suggestion: add 'variables:' or if present, indent so it is contained in 'with'");
}
let replace = &vars_replacements["replace"];
if replace.is_badvalue() {
bail!("Missing 'replace' as part of 'with'.\n \
Suggestion: add 'replace:' or if present, indent so it is contained in 'with'");
}
return Ok( Box::new( With {
variables: VariableDefinitions::build(var_defs).chain_err(|| "'variables'")?,
replacements: ReplacementArray::build(replace).chain_err(|| "'replace:'")?,
} ) );
}
fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
rules_with_context.context_stack.push(self.variables.clone(), mathml)?;
let result = self.replacements.replace(rules_with_context, mathml)
.chain_err(||"replacing inside 'with'")?;
rules_with_context.context_stack.pop();
return Ok( result );
}
}
#[derive(Debug, Clone)]
struct SetVariables {
variables: VariableDefinitions, }
impl fmt::Display for SetVariables {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "SetVariables: variables {}", &self.variables);
}
}
impl<'r> SetVariables {
fn build(vars: &Yaml) -> Result<Box<SetVariables>> {
if vars.as_vec().is_none() {
bail!("'set_variables' -- should be an of variable name, xpath value");
}
return Ok( Box::new( SetVariables {
variables: VariableDefinitions::build(vars).chain_err(|| "'set_variables'")?
} ) );
}
fn replace<'c, 's:'c, 'm: 'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
rules_with_context.context_stack.set_globals(self.variables.clone(), mathml)?;
return T::from_string( "".to_string(), rules_with_context.doc );
}
}
#[derive(Debug, Clone)]
struct TranslateExpression {
id: MyXPath, }
impl fmt::Display for TranslateExpression {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "speak: {}", &self.id);
}
}
impl<'r> TranslateExpression {
fn build(vars: &Yaml) -> Result<TranslateExpression> {
return Ok( TranslateExpression { id: MyXPath::build(vars).chain_err(|| "'set_variables'")? } );
}
fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
if self.id.rc.string.contains('@') {
let xpath_value = self.id.evaluate(rules_with_context.get_context(), mathml)?;
let id = match xpath_value {
Value::String(s) => Some(s),
Value::Nodeset(nodes) => {
if nodes.size() == 1 {
nodes.document_order_first().unwrap().attribute().map(|attr| attr.value().to_string())
} else {
None
}
},
_ => None,
};
let speech = match id {
None => bail!("'translate' value '{}' is not a string or an attribute value (correct by using '@id'??):\n", self.id),
Some(id) => {
match crate::navigate::get_node_by_id(mathml, &id) { None => bail!("'translate' value '{}' was not an 'id' found in {}", &id, mml_to_string(&mathml)),
Some(element) => speak_intent(element)?,
}
}
};
return T::from_string(speech, rules_with_context.doc);
} else {
return T::from_string(
self.id.replace(rules_with_context, mathml).chain_err(||"'translate'")?,
rules_with_context.doc
);
}
}
}
#[derive(Debug, Clone)]
pub struct ReplacementArray {
replacements: Vec<Replacement>
}
impl fmt::Display for ReplacementArray {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "{}", self.pretty_print_replacements());
}
}
impl<'r> ReplacementArray {
pub fn build_empty() -> ReplacementArray {
return ReplacementArray {
replacements: vec![]
}
}
pub fn build(replacements: &Yaml) -> Result<ReplacementArray> {
let result= if replacements.is_array() {
let replacements = replacements.as_vec().unwrap();
replacements
.iter()
.enumerate() .map(|(i, r)| Replacement::build(r)
.chain_err(|| format!("replacement #{} of {}", i+1, replacements.len())))
.collect::<Result<Vec<Replacement>>>()?
} else {
vec![ Replacement::build(replacements)?]
};
return Ok( ReplacementArray{ replacements: result } );
}
pub fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
return T::replace(self, rules_with_context, mathml);
}
pub fn replace_array_string<'c, 's:'c, 'm:'c>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<String> {
let mut replacement_strings = Vec::with_capacity(self.replacements.len()); for replacement in self.replacements.iter() {
let string: String = rules_with_context.replace(replacement, mathml)?;
if !string.is_empty() {
replacement_strings.push(string);
}
}
if replacement_strings.is_empty() {
return Ok( "".to_string() );
}
for i in 1..replacement_strings.len()-1 {
if let Some(bytes) = is_repetitive(&replacement_strings[i-1], &replacement_strings[i]) {
replacement_strings[i] = bytes.to_string();
}
}
for i in 0..replacement_strings.len() {
if replacement_strings[i].contains(PAUSE_AUTO_STR) {
let before = if i == 0 {""} else {&replacement_strings[i-1]};
let after = if i+1 == replacement_strings.len() {""} else {&replacement_strings[i+1]};
replacement_strings[i] = replacement_strings[i].replace(
PAUSE_AUTO_STR,
&rules_with_context.speech_rules.pref_manager.borrow().get_tts().compute_auto_pause(&rules_with_context.speech_rules.pref_manager.borrow(), before, after));
}
}
return Ok( replacement_strings.join(" ") );
fn is_repetitive<'a>(prev: &str, optional: &'a str) -> Option<&'a str> {
if optional.len() <= 2 * OPTIONAL_INDICATOR_LEN {
return None;
}
match optional.find(OPTIONAL_INDICATOR) {
None => return None,
Some(start_index) => {
let optional_word_start_slice = &optional[start_index + OPTIONAL_INDICATOR_LEN..];
match optional_word_start_slice.find(OPTIONAL_INDICATOR) {
None => panic!("Internal error: missing end optional char -- text handling is corrupted!"),
Some(end_index) => {
let optional_word = &optional_word_start_slice[..end_index];
let prev = prev.trim_end().as_bytes();
if prev.len() > optional_word.len() &&
&prev[prev.len()-optional_word.len()..] == optional_word.as_bytes() {
return Some( optional_word_start_slice[optional_word.len() + OPTIONAL_INDICATOR_LEN..].trim_start() );
} else {
return None;
}
}
}
}
}
}
}
pub fn replace_array_tree<'c, 's:'c, 'm:'c>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<Element<'m>> {
if self.replacements.len() == 1 {
return rules_with_context.replace::<Element<'m>>(&self.replacements[0], mathml);
}
let new_element = create_mathml_element(&rules_with_context.doc, "Unknown"); let mut new_children = Vec::with_capacity(self.replacements.len());
for child in self.replacements.iter() {
let child = rules_with_context.replace::<Element<'m>>(child, mathml)?;
new_children.push(ChildOfElement::Element(child));
};
new_element.append_children(new_children);
return Ok(new_element);
}
pub fn is_empty(&self) -> bool {
return self.replacements.is_empty();
}
fn pretty_print_replacements(&self) -> String {
let mut group_string = String::with_capacity(128);
if self.replacements.len() == 1 {
group_string += &format!("[{}]", self.replacements[0]);
} else {
group_string += &self.replacements.iter()
.map(|replacement| format!("\n - {}", replacement))
.collect::<Vec<String>>()
.join("");
group_string += "\n";
}
return group_string;
}
}
#[derive(Debug)]
struct RCMyXPath {
xpath: XPath,
string: String, }
#[derive(Debug, Clone)]
pub struct MyXPath {
rc: Rc<RCMyXPath> }
impl fmt::Display for MyXPath {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "x: \"{}\"", self.rc.string);
}
}
thread_local!{
static XPATH_CACHE: RefCell<HashMap<String, MyXPath>> = RefCell::new( HashMap::with_capacity(2047) );
}
impl<'r> MyXPath {
fn new(xpath: String) -> Result<MyXPath> {
return XPATH_CACHE.with( |cache| {
let mut cache = cache.borrow_mut();
return Ok(
match cache.get(&xpath) {
Some(compiled_xpath) => {
compiled_xpath.clone()
},
None => {
let new_xpath = MyXPath {
rc: Rc::new( RCMyXPath {
xpath: MyXPath::compile_xpath(&xpath)?,
string: xpath.clone()
})};
cache.insert(xpath.clone(), new_xpath.clone());
new_xpath
},
}
)
});
}
pub fn build(xpath: &Yaml) -> Result<MyXPath> {
let xpath = match xpath {
Yaml::String(s) => s.to_string(),
Yaml::Integer(i) => i.to_string(),
Yaml::Real(s) => s.to_string(),
Yaml::Boolean(s) => s.to_string(),
Yaml::Array(v) =>
v.iter()
.map(as_str_checked)
.collect::<Result<Vec<&str>>>()?
.join(" "),
_ => bail!("Bad value when trying to create an xpath: {}", yaml_to_string(xpath, 1)),
};
return MyXPath::new(xpath);
}
fn compile_xpath(xpath: &str) -> Result<XPath> {
let factory = Factory::new();
let xpath_with_debug_info = MyXPath::add_debug_string_arg(xpath)?;
let compiled_xpath = factory.build(&xpath_with_debug_info)
.chain_err(|| format!(
"Could not compile XPath for pattern:\n{}{}",
&xpath, more_details(xpath)))?;
return match compiled_xpath {
Some(xpath) => Ok(xpath),
None => bail!("Problem compiling Xpath for pattern:\n{}{}",
&xpath, more_details(xpath)),
};
fn more_details(xpath: &str) -> String {
if xpath.is_empty() {
return "xpath is empty string".to_string();
}
let as_bytes = xpath.trim().as_bytes();
if as_bytes[0] == b'\'' && as_bytes[as_bytes.len()-1] != b'\'' {
return "\nmissing \"'\"".to_string();
}
if (as_bytes[0] == b'"' && as_bytes[as_bytes.len()-1] != b'"') ||
(as_bytes[0] != b'"' && as_bytes[as_bytes.len()-1] == b'"'){
return "\nmissing '\"'".to_string();
}
let mut i_bytes = 0; let mut paren_count = 0; let mut i_paren = 0; let mut bracket_count = 0;
let mut i_bracket = 0;
for ch in xpath.chars() {
if ch == '(' {
if paren_count == 0 {
i_paren = i_bytes;
}
paren_count += 1;
} else if ch == '[' {
if bracket_count == 0 {
i_bracket = i_bytes;
}
bracket_count += 1;
} else if ch == ')' {
if paren_count == 0 {
return format!("\nExtra ')' found after '{}'", &xpath[i_paren..i_bytes]);
}
paren_count -= 1;
if paren_count == 0 && bracket_count > 0 && i_bracket > i_paren {
return format!("\nUnclosed brackets found at '{}'", &xpath[i_paren..i_bytes]);
}
} else if ch == ']' {
if bracket_count == 0 {
return format!("\nExtra ']' found after '{}'", &xpath[i_bracket..i_bytes]);
}
bracket_count -= 1;
if bracket_count == 0 && paren_count > 0 && i_paren > i_bracket {
return format!("\nUnclosed parens found at '{}'", &xpath[i_bracket..i_bytes]);
}
}
i_bytes += ch.len_utf8();
}
return "".to_string();
}
}
fn add_debug_string_arg(xpath: &str) -> Result<String> {
let debug_start = xpath.find("DEBUG(");
if debug_start.is_none() {
return Ok( xpath.to_string() );
}
let debug_start = debug_start.unwrap();
let string_start = xpath[..debug_start+6].to_string(); let mut count = 1; let mut remainder: &str = &xpath[debug_start+6..];
loop {
let next = remainder.find(|c| c=='(' || c==')');
match next {
None => bail!("Did not find closing paren for DEBUG in\n{}", xpath),
Some(i_paren) => {
let remainder_as_bytes = remainder.as_bytes();
if i_paren == 0 || remainder_as_bytes[i_paren-1] != b'\'' ||
i_paren+1 >= remainder.len() || remainder_as_bytes[i_paren+1] != b'\'' {
if remainder_as_bytes[i_paren] == b'(' {
count += 1;
} else { count -= 1;
if count == 0 {
let i_end = xpath.len() - remainder.len() + i_paren;
let escaped_arg = &xpath[debug_start+6..i_end].to_string().replace('"', "\\\"");
let contents = MyXPath::add_debug_string_arg(&xpath[debug_start+6..i_end])?;
return Ok( string_start + &contents + ", \"" + escaped_arg + "\" "
+ &MyXPath::add_debug_string_arg(&xpath[i_end..])? );
}
}
}
remainder = &remainder[i_paren+1..];
}
}
}
}
fn is_true(&self, context: &Context, mathml: Element) -> Result<bool> {
return Ok(
match self.evaluate(context, mathml)? {
Value::Boolean(b) => b,
Value::Nodeset(nodes) => nodes.size() > 0,
_ => false,
}
)
}
pub fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
if self.rc.string == "process-intent(.)" {
return T::from_element( crate::infer_intent::infer_intent(rules_with_context, mathml)? );
}
let result = self.evaluate(&rules_with_context.context_stack.base, mathml)
.chain_err(|| format!("in '{}' replacing after pattern match", &self.rc.string) )?;
return match result {
Value::Nodeset(nodes) => {
if nodes.size() == 0 {
bail!("During replacement, no matching element found");
}
rules_with_context.replace_nodes(nodes.document_order(), mathml)
},
Value::String(t) => T::from_string(t, rules_with_context.doc),
Value::Number(num) => T::from_string(num.to_string(), rules_with_context.doc ),
Value::Boolean(b) => T::from_string(b.to_string(), rules_with_context.doc ), };
}
pub fn evaluate<'a, 'c>(&'a self, context: &'r Context<'c>, mathml: Element<'c>) -> Result<Value<'c>> {
let result = self.rc.xpath.evaluate(context, mathml);
return match result {
Ok(val) => Ok( val ),
Err(e) => {
bail!( "{}\n\n",
e.to_string() .replace("OwnedPrefixedName { prefix: None, local_part:", "")
.replace(" }", "") );
}
};
}
}
#[derive(Debug)]
struct SpeechPattern {
pattern_name: String,
tag_name: String,
file_name: String,
pattern: MyXPath, match_uses_var_defs: bool, var_defs: VariableDefinitions, replacements: ReplacementArray, }
impl fmt::Display for SpeechPattern {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "[name: {}, tag: {},\n variables: {:?}, pattern: {},\n replacement: {}]",
self.pattern_name, self.tag_name, self.var_defs, self.pattern,
self.replacements.pretty_print_replacements());
}
}
impl SpeechPattern {
fn build(dict: &Yaml, file: &Path, rules: &mut SpeechRules) -> Result<()> {
if let Some(include_file_name) = find_str(dict, "include") {
let do_include_fn = |new_file: &Path| {
rules.read_patterns(&[Some(new_file.to_path_buf()), None, None])
};
return process_include(file, include_file_name, do_include_fn);
}
let pattern_name = find_str(dict, "name");
let mut tag_names: Vec<&str> = Vec::new();
match find_str(dict, "tag") {
Some(str) => tag_names.push(str),
None => {
let tag_array = &dict["tag"];
tag_names = vec![];
if tag_array.is_array() {
for (i, name) in tag_array.as_vec().unwrap().iter().enumerate() {
match as_str_checked(name) {
Err(e) => return Err(
e.chain_err(||
format!("tag name '{}' is not a string in:\n{}",
&yaml_to_string(&tag_array.as_vec().unwrap()[i], 0),
&yaml_to_string(dict, 1)))
),
Ok(str) => tag_names.push(str),
};
}
} else {
bail!("Errors trying to find 'tag' in:\n{}", &yaml_to_string(dict, 1));
}
}
}
if pattern_name.is_none() {
if dict.is_null() {
bail!("Error trying to find 'name': empty value (two consecutive '-'s?");
} else {
bail!("Errors trying to find 'name' in:\n{}", &yaml_to_string(dict, 1));
};
};
let pattern_name = pattern_name.unwrap().to_string();
if dict["match"].is_badvalue() {
bail!("Did not find 'match' in\n{}", yaml_to_string(dict, 1));
}
if dict["replace"].is_badvalue() {
bail!("Did not find 'replace' in\n{}", yaml_to_string(dict, 1));
}
for tag_name in tag_names {
let tag_name = tag_name.to_string();
let pattern_xpath = MyXPath::build(&dict["match"])
.chain_err(|| {
format!("value for 'match' in rule ({}: {}):\n{}",
tag_name, pattern_name, yaml_to_string(dict, 1))
})?;
let speech_pattern =
Box::new( SpeechPattern{
pattern_name: pattern_name.clone(),
tag_name: tag_name.clone(),
file_name: file.to_str().unwrap().to_string(),
match_uses_var_defs: dict["variables"].is_array() && pattern_xpath.rc.string.contains('$'), pattern: pattern_xpath,
var_defs: VariableDefinitions::build(&dict["variables"])
.chain_err(|| {
format!("value for 'variables' in rule ({}: {}):\n{}",
tag_name, pattern_name, yaml_to_string(dict, 1))
})?,
replacements: ReplacementArray::build(&dict["replace"])
.chain_err(|| {
format!("value for 'replace' in rule ({}: {}). Replacements:\n{}",
tag_name, pattern_name, yaml_to_string(&dict["replace"], 1))
})?
} );
let rule_value = rules.rules.entry(tag_name).or_default();
match rule_value.iter().enumerate().find(|&pattern| pattern.1.pattern_name == speech_pattern.pattern_name) {
None => rule_value.push(speech_pattern),
Some((i, _old_pattern)) => {
let old_rule = &rule_value[i];
info!("Warning: replacing {}/'{}' in {} with rule from {}",
old_rule.tag_name, old_rule.pattern_name, old_rule.file_name, speech_pattern.file_name);
rule_value[i] = speech_pattern;
},
}
}
return Ok( () );
}
fn is_match(&self, context: &Context, mathml: Element) -> Result<bool> {
if self.tag_name != mathml.name().local_part() && self.tag_name != "*" && self.tag_name != "!*" {
return Ok( false );
}
return Ok(
match self.pattern.evaluate(context, mathml)? {
Value::Boolean(b) => b,
Value::Nodeset(nodes) => nodes.size() > 0,
_ => false,
}
);
}
}
#[derive(Debug, Clone)]
struct TestArray {
tests: Vec<Test>
}
impl fmt::Display for TestArray {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for test in &self.tests {
writeln!(f, "{}", test)?;
}
return Ok( () );
}
}
impl<'r> TestArray {
fn build(test: &Yaml) -> Result<TestArray> {
let tests = if test.as_hash().is_some() {
vec![test]
} else if let Some(vec) = test.as_vec() {
vec.iter().collect()
} else {
bail!("Value for 'test:' is neither a dictionary or an array.")
};
let mut test_array = vec![];
for test in tests {
if test.as_hash().is_none() {
bail!("Value for array entry in 'test:' must be a dictionary/contain keys");
}
let if_part = &test[if test_array.is_empty() {"if"} else {"else_if"}];
if !if_part.is_badvalue() {
let condition = Some( MyXPath::build(if_part)? );
let then_part = TestOrReplacements::build(test, "then", "then_test", true)?;
let else_part = TestOrReplacements::build(test, "else", "else_test", false)?;
let n_keys = if else_part.is_none() {2} else {3};
if test.as_hash().unwrap().len() > n_keys {
bail!("A key other than 'if', 'else_if', 'then', 'then_test', 'else', or 'else_test' was found in the 'then' clause of 'test'");
};
test_array.push(
Test { condition, then_part, else_part }
);
} else {
let else_part = TestOrReplacements::build(test, "else", "else_test", true)?;
if test.as_hash().unwrap().len() > 1 {
bail!("A key other than 'if', 'else_if', 'then', 'then_test', 'else', or 'else_test' was found the 'else' clause of 'test'");
};
test_array.push(
Test { condition: None, then_part: None, else_part }
);
if test_array.len() < test.as_hash().unwrap().len() {
bail!("'else'/'else_test' key is not last key in 'test:'");
}
}
};
if test_array.is_empty() {
bail!("No entries for 'test:'");
}
return Ok( TestArray { tests: test_array } );
}
fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
for test in &self.tests {
if test.is_true(&rules_with_context.context_stack.base, mathml)? {
assert!(test.then_part.is_some());
return test.then_part.as_ref().unwrap().replace(rules_with_context, mathml);
} else if let Some(else_part) = test.else_part.as_ref() {
return else_part.replace(rules_with_context, mathml);
}
}
return T::from_string("".to_string(), rules_with_context.doc);
}
}
#[derive(Debug, Clone)]
enum TestOrReplacements {
Replacements(ReplacementArray), Test(TestArray), }
impl fmt::Display for TestOrReplacements {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let TestOrReplacements::Test(_) = self {
write!(f, " _test")?;
}
write!(f, ":")?;
return match self {
TestOrReplacements::Test(t) => write!(f, "{}", t),
TestOrReplacements::Replacements(r) => write!(f, "{}", r),
};
}
}
impl<'r> TestOrReplacements {
fn build(test: &Yaml, replace_key: &str, test_key: &str, key_required: bool) -> Result<Option<TestOrReplacements>> {
let part = &test[replace_key];
let test_part = &test[test_key];
if !part.is_badvalue() && !test_part.is_badvalue() {
bail!(format!("Only one of '{}' or '{}' is allowed as part of 'test'.\n{}\n \
Suggestion: delete one or adjust indentation",
replace_key, test_key, yaml_to_string(test, 2)));
}
if part.is_badvalue() && test_part.is_badvalue() {
if key_required {
bail!(format!("Missing one of '{}'/'{}:' as part of 'test:'\n{}\n \
Suggestion: add the missing key or indent so it is contained in 'test'",
replace_key, test_key, yaml_to_string(test, 2)));
} else {
return Ok( None );
}
}
if test_part.is_badvalue() {
return Ok( Some( TestOrReplacements::Replacements( ReplacementArray::build(part)? ) ) );
} else {
return Ok( Some( TestOrReplacements::Test( TestArray::build(test_part)? ) ) );
}
}
fn replace<'c, 's:'c, 'm:'c, T:TreeOrString<'c, 'm, T>>(&self, rules_with_context: &'r mut SpeechRulesWithContext<'c, 's,'m>, mathml: Element<'c>) -> Result<T> {
return match self {
TestOrReplacements::Replacements(r) => r.replace(rules_with_context, mathml),
TestOrReplacements::Test(t) => t.replace(rules_with_context, mathml),
}
}
}
#[derive(Debug, Clone)]
struct Test {
condition: Option<MyXPath>,
then_part: Option<TestOrReplacements>,
else_part: Option<TestOrReplacements>,
}
impl fmt::Display for Test {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "test: [ ")?;
if let Some(if_part) = &self.condition {
write!(f, " if: '{}'", if_part)?;
}
if let Some(then_part) = &self.then_part {
write!(f, " then{}", then_part)?;
}
if let Some(else_part) = &self.else_part {
write!(f, " else{}", else_part)?;
}
return write!(f, "]");
}
}
impl Test {
fn is_true(&self, context: &Context, mathml: Element) -> Result<bool> {
return match self.condition.as_ref() {
None => Ok( false ), Some(condition) => condition.is_true(context, mathml)
.chain_err(|| "Failure in conditional test"),
}
}
}
#[derive(Debug, Clone)]
struct VariableDefinition {
name: String, value: MyXPath, }
impl fmt::Display for VariableDefinition {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "[name: {}={}]", self.name, self.value);
}
}
#[derive(Debug)]
struct VariableValue<'v> {
name: String, value: Option<Value<'v>>, }
impl<'v> fmt::Display for VariableValue<'v> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let value = match &self.value {
None => "unset".to_string(),
Some(val) => format!("{:?}", val)
};
return write!(f, "[name: {}, value: {}]", self.name, value);
}
}
impl VariableDefinition {
fn build(name_value_def: &Yaml) -> Result<VariableDefinition> {
match name_value_def.as_hash() {
Some(map) => {
if map.len() != 1 {
bail!("definition is not a key/value pair. Found {}",
yaml_to_string(name_value_def, 1) );
}
let (name, value) = map.iter().next().unwrap();
let name = as_str_checked( name)
.chain_err(|| format!( "definition name is not a string: {}",
yaml_to_string(name, 1) ))?.to_string();
match value {
Yaml::Boolean(_) | Yaml::String(_) | Yaml::Integer(_) | Yaml::Real(_) => (),
_ => bail!("definition value is not a string, boolean, or number. Found {}",
yaml_to_string(value, 1) )
};
return Ok(
VariableDefinition{
name,
value: MyXPath::build(value)?
}
);
},
None => bail!("definition is not a key/value pair. Found {}",
yaml_to_string(name_value_def, 1) )
}
}
}
#[derive(Debug, Clone)]
struct VariableDefinitions {
defs: Vec<VariableDefinition>
}
impl fmt::Display for VariableDefinitions {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for def in &self.defs {
write!(f, "{},", def)?;
}
return Ok( () );
}
}
#[derive(Debug)]
struct VariableValues<'v> {
defs: Vec<VariableValue<'v>>
}
impl VariableDefinitions {
fn new(len: usize) -> VariableDefinitions {
return VariableDefinitions{ defs: Vec::with_capacity(len) };
}
fn build(defs: &Yaml) -> Result<VariableDefinitions> {
if defs.is_badvalue() {
return Ok( VariableDefinitions::new(0) );
};
if defs.is_array() {
let defs = defs.as_vec().unwrap();
let mut definitions = VariableDefinitions::new(defs.len());
for def in defs {
let variable_def = VariableDefinition::build(def)
.chain_err(|| "definition of 'variables'")?;
definitions.push( variable_def);
};
return Ok (definitions );
}
bail!( "'variables' is not an array of {{name: xpath-value}} definitions. Found {}'",
yaml_to_string(defs, 1) );
}
fn push(&mut self, var_def: VariableDefinition) {
self.defs.push(var_def);
}
fn len(&self) -> usize {
return self.defs.len();
}
}
struct ContextStack<'c> {
old_values: Vec<VariableValues<'c>>, base: Context<'c> }
impl<'c> fmt::Display for ContextStack<'c> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return writeln!(f, " {} old_values", self.old_values.len());
}
}
impl<'c, 'r> ContextStack<'c> {
fn new<'a,>(pref_manager: &'a PreferenceManager) -> ContextStack<'c> {
let prefs = pref_manager.merge_prefs();
let context_stack = ContextStack {
base: ContextStack::base_context(prefs),
old_values: Vec::with_capacity(31) };
return context_stack;
}
fn base_context(var_defs: PreferenceHashMap) -> Context<'c> {
let mut context = Context::new();
context.set_namespace("m", "http://www.w3.org/1998/Math/MathML");
crate::xpath_functions::add_builtin_functions(&mut context);
for (key, value) in var_defs {
context.set_variable(key.as_str(), yaml_to_value(&value));
};
return context;
}
fn set_globals(&'r mut self, new_vars: VariableDefinitions, mathml: Element<'c>) -> Result<()> {
for def in &new_vars.defs {
let new_value = match def.value.evaluate(&self.base, mathml) {
Ok(val) => val,
Err(_) => bail!(format!("Can't evaluate variable def for {}", def)),
};
let qname = QName::new(def.name.as_str());
self.base.set_variable(qname, new_value);
}
return Ok( () );
}
fn push(&'r mut self, new_vars: VariableDefinitions, mathml: Element<'c>) -> Result<()> {
let mut old_values = VariableValues {defs: Vec::with_capacity(new_vars.defs.len()) };
let evaluation = Evaluation::new(&self.base, Node::Element(mathml));
for def in &new_vars.defs {
let qname = QName::new(def.name.as_str());
let old_value = evaluation.value_of(qname).cloned();
old_values.defs.push( VariableValue{ name: def.name.clone(), value: old_value} );
}
for def in &new_vars.defs {
let new_value = match def.value.evaluate(&self.base, mathml) {
Ok(val) => val,
Err(_) => bail!(format!("Can't evaluate variable def for {}", def)),
};
let qname = QName::new(def.name.as_str());
self.base.set_variable(qname, new_value);
}
self.old_values.push(old_values);
return Ok( () );
}
fn pop(&mut self) {
const MISSING_VALUE: &str = "-- unset value --"; let old_values = self.old_values.pop().unwrap();
for variable in old_values.defs {
let qname = QName::new(&variable.name);
let old_value = match variable.value {
None => Value::String(MISSING_VALUE.to_string()),
Some(val) => val,
};
self.base.set_variable(qname, old_value);
}
}
}
fn yaml_to_value<'a, 'b>(yaml: &'a Yaml) -> Value<'b> {
return match yaml {
Yaml::String(s) => Value::String(s.clone()),
Yaml::Boolean(b) => Value::Boolean(*b),
Yaml::Integer(i) => Value::Number(*i as f64),
Yaml::Real(s) => Value::Number(s.parse::<f64>().unwrap()),
_ => {
error!("yaml_to_value: illegal type found in Yaml value: {}", yaml_to_string(yaml, 1));
Value::String("".to_string())
},
}
}
struct UnicodeDef {
ch: u32,
speech: ReplacementArray
}
impl fmt::Display for UnicodeDef {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
return write!(f, "UnicodeDef{{ch: {}, speech: {:?}}}", self.ch, self.speech);
}
}
impl UnicodeDef {
fn build(unicode_def: &Yaml, file_name: &Path, speech_rules: &SpeechRules, use_short: bool) -> Result<()> {
if let Some(include_file_name) = find_str(unicode_def, "include") {
let do_include_fn = |new_file: &Path| {
speech_rules.read_unicode(Some(new_file.to_path_buf()), use_short)
};
return process_include(file_name, include_file_name, do_include_fn);
}
let dictionary = unicode_def.as_hash();
if dictionary.is_none() {
bail!("Expected a unicode definition (e.g, '+':[t: \"plus\"]'), found {}", yaml_to_string(unicode_def, 0));
}
let dictionary = dictionary.unwrap();
if dictionary.len() != 1 {
bail!("Expected a unicode definition (e.g, '+':[t: \"plus\"]'), found {}", yaml_to_string(unicode_def, 0));
}
let (ch, replacements) = dictionary.iter().next().ok_or_else(|| format!("Expected a unicode definition (e.g, '+':[t: \"plus\"]'), found {}", yaml_to_string(unicode_def, 0)))?;
let mut unicode_table = if use_short {
speech_rules.unicode_short.borrow_mut()
} else {
speech_rules.unicode_full.borrow_mut()
};
if let Some(str) = ch.as_str() {
if str.is_empty() {
bail!("Empty character definition. Replacement is {}", replacements.as_str().unwrap());
}
let mut chars = str.chars();
let first_ch = chars.next().unwrap(); if chars.next().is_some() { if str.contains('-') {
return process_range(str, replacements, unicode_table);
} else if first_ch != '0' { for ch in str.chars() { let ch_as_str = ch.to_string();
unicode_table.insert(ch as u32, ReplacementArray::build(&substitute_ch(replacements, &ch_as_str))
.chain_err(|| format!("In definition of char: '{}'", str))?.replacements);
}
return Ok( () );
}
}
}
let ch = UnicodeDef::get_unicode_char(ch)?;
unicode_table.insert(ch, ReplacementArray::build(replacements)
.chain_err(|| format!("In definition of char: '{}' (0x{})",
char::from_u32(ch).unwrap(), ch))?.replacements);
return Ok( () );
fn process_range(def_range: &str, replacements: &Yaml, mut unicode_table: RefMut<HashMap<u32,Vec<Replacement>>>) -> Result<()> {
let mut range = def_range.split('-');
let first = range.next().unwrap().chars().next().unwrap() as u32;
let last = range.next().unwrap().chars().next().unwrap() as u32;
if range.next().is_some() {
bail!("Character range definition has more than one '-': '{}'", def_range);
}
for ch in first..last+1 {
let ch_as_str = char::from_u32(ch).unwrap().to_string();
unicode_table.insert(ch, ReplacementArray::build(&substitute_ch(replacements, &ch_as_str))
.chain_err(|| format!("In definition of char: '{}'", def_range))?.replacements);
};
return Ok( () );
}
fn substitute_ch(yaml: &Yaml, ch: &str) -> Yaml {
return match yaml {
Yaml::Array(ref v) => {
Yaml::Array(
v.iter()
.map(|e| substitute_ch(e, ch))
.collect::<Vec<Yaml>>()
)
},
Yaml::Hash(ref h) => {
Yaml::Hash(
h.iter()
.map(|(key,val)| (key.clone(), substitute_ch(val, ch)) )
.collect::<Hash>()
)
},
Yaml::String(s) => Yaml::String( s.replace('.', ch) ),
_ => yaml.clone(),
}
}
}
fn get_unicode_char(ch: &Yaml) -> Result<u32> {
if let Some(ch) = ch.as_str() {
let mut ch_iter = ch.chars();
let unicode_ch = ch_iter.next();
if unicode_ch.is_none() || ch_iter.next().is_some() {
bail!("Wanted unicode char, found string '{}')", ch);
};
return Ok( unicode_ch.unwrap() as u32 );
}
if let Some(num) = ch.as_i64() {
return Ok( num as u32 );
}
bail!("Unicode character '{}' can't be converted to an code point", yaml_to_string(ch, 0));
}
}
type RuleTable = HashMap<String, Vec<Box<SpeechPattern>>>;
type UnicodeTable = Rc<RefCell<HashMap<u32,Vec<Replacement>>>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RulesFor {
Intent,
Speech,
OverView,
Navigation,
Braille,
}
impl fmt::Display for RulesFor {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
RulesFor::Intent => "Intent",
RulesFor::Speech => "Speech",
RulesFor::OverView => "OverView",
RulesFor::Navigation => "Navigation",
RulesFor::Braille => "Braille",
};
return write!(f, "{}", name);
}
}
pub struct SpeechRules {
error: String,
name: RulesFor,
pub pref_manager: Rc<RefCell<PreferenceManager>>,
rules: RuleTable, translate_single_chars_only: bool, unicode_short: UnicodeTable, unicode_full: UnicodeTable, }
impl fmt::Display for SpeechRules {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "SpeechRules '{}'\n{})", self.name, self.pref_manager.borrow())?;
let mut rules_vec: Vec<(&String, &Vec<Box<SpeechPattern>>)> = self.rules.iter().collect();
rules_vec.sort_by(|(tag_name1, _), (tag_name2, _)| tag_name1.cmp(tag_name2));
for (tag_name, rules) in rules_vec {
writeln!(f, " {}: #patterns {}", tag_name, rules.len())?;
};
return writeln!(f, " {}+{} unicode entries", &self.unicode_short.borrow().len(), &self.unicode_full.borrow().len());
}
}
pub struct SpeechRulesWithContext<'c, 's:'c, 'm:'c> {
speech_rules: &'s SpeechRules,
context_stack: ContextStack<'c>, doc: Document<'m>,
nav_node_id: String,
pub inside_spell: bool, }
impl<'c, 's:'c, 'm:'c> fmt::Display for SpeechRulesWithContext<'c, 's,'m> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "SpeechRulesWithContext \n{})", self.speech_rules)?;
return writeln!(f, " {} context entries, nav node id '{}'", &self.context_stack, &self.nav_node_id);
}
}
thread_local!{
static SPEECH_UNICODE_SHORT: UnicodeTable =
Rc::new( RefCell::new( HashMap::with_capacity(6997) ) );
static SPEECH_UNICODE_FULL: UnicodeTable =
Rc::new( RefCell::new( HashMap::with_capacity(497) ) );
pub static INTENT_RULES: RefCell<SpeechRules> =
RefCell::new( SpeechRules::new(RulesFor::Intent, true) );
pub static SPEECH_RULES: RefCell<SpeechRules> =
RefCell::new( SpeechRules::new(RulesFor::Speech, true) );
pub static OVERVIEW_RULES: RefCell<SpeechRules> =
RefCell::new( SpeechRules::new(RulesFor::OverView, true) );
pub static NAVIGATION_RULES: RefCell<SpeechRules> =
RefCell::new( SpeechRules::new(RulesFor::Navigation, true) );
pub static BRAILLE_RULES: RefCell<SpeechRules> =
RefCell::new( SpeechRules::new(RulesFor::Braille, false) );
}
impl SpeechRules {
pub fn new(name: RulesFor, translate_single_chars_only: bool) -> SpeechRules {
use crate::definitions::read_definitions_file;
let pref_manager = PreferenceManager::get();
let mut error = pref_manager.borrow().get_error().to_string();
if pref_manager.borrow().get_error().is_empty() {
let read_files = read_definitions_file(pref_manager.borrow().get_definitions_file());
match read_files {
Ok(_) => {
let unicode = if name == RulesFor::Braille {
(
Rc::new( RefCell::new (HashMap::with_capacity(497)) ),
Rc::new( RefCell::new (HashMap::with_capacity(2497)) )
)
} else {
(
SPEECH_UNICODE_SHORT.with( |unicode| Rc::clone( unicode) ),
SPEECH_UNICODE_FULL. with( |unicode| Rc::clone( unicode) )
)
};
let rules = SpeechRules {
error: Default::default(),
name,
rules: HashMap::with_capacity(if name == RulesFor::Intent {1023} else {31}), unicode_short: unicode.0, unicode_full: unicode.1, translate_single_chars_only,
pref_manager,
};
return rules;
},
Err(e) => error = crate::interface::errors_to_string(&e),
}
};
return SpeechRules {
error,
name,
rules: HashMap::with_capacity(1),
unicode_short: Rc::new( RefCell::new (HashMap::with_capacity(1)) ),
unicode_full: Rc::new( RefCell::new (HashMap::with_capacity(1)) ),
translate_single_chars_only: true,
pref_manager,
};
}
pub fn get_error(&self) -> Option<&str> {
return if self.error.is_empty() {
None
} else {
Some(&self.error)
}
}
pub fn initialize_all_rules() -> Result<()> {
INTENT_RULES.with(|speech_rules| -> Result<()> {
if let Some(e) = speech_rules.borrow().get_error() {bail!("{}", e)} else {Ok(())}
})?;
SPEECH_RULES.with(|speech_rules| -> Result<()> {
if let Some(e) = speech_rules.borrow().get_error() {bail!("{}", e)} else {Ok(())}
})?;
BRAILLE_RULES.with(|speech_rules| -> Result<()> {
if let Some(e) = speech_rules.borrow().get_error() {bail!("{}", e)} else {Ok(())}
})?;
NAVIGATION_RULES.with(|speech_rules| -> Result<()> {
if let Some(e) = speech_rules.borrow().get_error() {bail!("{}", e)} else {Ok(())}
})?;
OVERVIEW_RULES.with(|speech_rules| -> Result<()> {
if let Some(e) = speech_rules.borrow().get_error() {bail!("{}", e)} else {Ok(())}
})?;
return Ok( () );
}
pub fn read_files(&mut self) -> Result<()> {
if self.rules.is_empty() {
let rule_file = self.pref_manager.borrow().get_rule_file(&self.name).clone();
self.read_patterns(&rule_file)?;
}
if self.unicode_short.borrow().is_empty() {
self.read_unicode(None, true)?;
}
return Ok( () );
}
pub fn invalidate(&mut self, changes: FilesChanged) {
if self.name == RulesFor::Braille {
if changes.braille_rules {
self.rules.clear();
}
if changes.braille_unicode_short {
self.unicode_short.borrow_mut().clear();
}
if changes.braille_unicode_full {
self.unicode_full.borrow_mut().clear();
}
} else {
if changes.speech_rules {
self.rules.clear();
}
if changes.speech_unicode_short {
self.unicode_short.borrow_mut().clear();
}
if changes.speech_unicode_full {
self.unicode_full.borrow_mut().clear();
}
}
}
pub fn update() {
if let Some(files_changed) = PreferenceManager::get().borrow_mut().is_up_to_date() {
SPEECH_RULES.with(|rules| {
let mut rules = rules.borrow_mut();
if files_changed.speech_rules {
rules.rules.clear();
}
if files_changed.speech_unicode_short {
rules.unicode_short.borrow_mut().clear();
}
if files_changed.speech_unicode_full {
rules.unicode_full.borrow_mut().clear();
}
});
BRAILLE_RULES.with(|rules| {
let mut rules = rules.borrow_mut();
if files_changed.braille_rules {
rules.rules.clear();
}
if files_changed.braille_unicode_short {
rules.unicode_short.borrow_mut().clear();
}
if files_changed.braille_unicode_full {
rules.unicode_full.borrow_mut().clear();
}
});
INTENT_RULES.with(|rules| {
let mut rules = rules.borrow_mut();
if files_changed.intent {
rules.rules.clear();
}
});
}
}
fn read_patterns(&mut self, path: &Locations) -> Result<()> {
if let Some(p) = &path[0] {
let rule_file_contents = read_to_string_shim(p.as_path()).expect("cannot read file");
let rules_build_fn = |pattern: &Yaml| {
self.build_speech_patterns(pattern, p)
.chain_err(||format!("in file {:?}", p.to_str().unwrap()))
};
return compile_rule(&rule_file_contents, rules_build_fn)
.chain_err(||format!("in file {:?}", p.to_str().unwrap()));
}
return Ok(());
}
fn build_speech_patterns(&mut self, patterns: &Yaml, file_name: &Path) -> Result<()> {
let patterns_vec = patterns.as_vec();
if patterns_vec.is_none() {
bail!(yaml_type_err(patterns, "array"));
}
let patterns_vec = patterns.as_vec().unwrap();
for entry in patterns_vec.iter() {
SpeechPattern::build(entry, file_name, self)?;
}
return Ok( () );
}
fn read_unicode(&self, path: Option<PathBuf>, use_short: bool) -> Result<()> {
let path = match path {
Some(p) => p,
None => {
let pref_manager = self.pref_manager.borrow();
let unicode_files = if self.name == RulesFor::Braille {
pref_manager.get_braille_unicode_file()
} else {
pref_manager.get_speech_unicode_file()
};
if use_short {unicode_files.0} else {unicode_files.1}
}
};
info!("Reading unicode file {}", path.to_str().unwrap());
let unicode_file_contents = read_to_string_shim(&path)?;
let unicode_build_fn = |unicode_def_list: &Yaml| {
let unicode_defs = unicode_def_list.as_vec();
if unicode_defs.is_none() {
bail!("File '{}' does not begin with an array", yaml_to_type(unicode_def_list));
};
for unicode_def in unicode_defs.unwrap() {
UnicodeDef::build(unicode_def, &path, self, use_short)
.chain_err(|| {format!("In file {:?}", path.to_str())})?;
};
return Ok(());
};
return compile_rule(&unicode_file_contents, unicode_build_fn)
.chain_err(||format!("in file {:?}", path.to_str().unwrap()));
}
}
use crate::prefs::FilesChanged;
impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> {
pub fn new(speech_rules: &'s SpeechRules, doc: Document<'m>, nav_node_id: String) -> SpeechRulesWithContext<'c, 's, 'm> {
return SpeechRulesWithContext {
speech_rules,
context_stack: ContextStack::new(&speech_rules.pref_manager.borrow()),
doc,
nav_node_id,
inside_spell: false,
}
}
pub fn get_rules(&mut self) -> &SpeechRules {
return self.speech_rules;
}
pub fn get_context(&mut self) -> &mut Context<'c> {
return &mut self.context_stack.base;
}
pub fn get_document(&mut self) -> Document<'m> {
return self.doc;
}
pub fn match_pattern<T:TreeOrString<'c, 'm, T>>(&'r mut self, mathml: Element<'c>) -> Result<T> {
let tag_name = mathml.name().local_part();
let rules = &self.speech_rules.rules;
if let Some(rule_vector) = rules.get("!*") {
if let Some(result) = self.find_match(rule_vector, mathml)? {
return Ok(result); }
}
if let Some(rule_vector) = rules.get(tag_name) {
if let Some(result) = self.find_match(rule_vector, mathml)? {
return Ok(result); }
}
if let Some(rule_vector) = rules.get("*") {
if let Some(result) = self.find_match(rule_vector, mathml)? {
return Ok(result); }
}
let mut file_name = "unknown";
let speech_manager = self.speech_rules.pref_manager.borrow();
if let Some(path) = &speech_manager.get_rule_file(&self.speech_rules.name)[0] {
file_name= path.to_str().unwrap();
}
bail!("\nNo match found!\nMissing patterns in {} for MathML.\n{}", file_name, mml_to_string(&mathml));
}
fn find_match<T:TreeOrString<'c, 'm, T>>(&'r mut self, rule_vector: &[Box<SpeechPattern>], mathml: Element<'c>) -> Result<Option<T>> {
for pattern in rule_vector {
if pattern.match_uses_var_defs {
self.context_stack.push(pattern.var_defs.clone(), mathml)?;
}
if pattern.is_match(&self.context_stack.base, mathml)
.chain_err(|| error_string(pattern, mathml) )? {
if !pattern.match_uses_var_defs && pattern.var_defs.len() > 0 { self.context_stack.push(pattern.var_defs.clone(), mathml)?;
}
let result: Result<T> = pattern.replacements.replace(self, mathml);
if pattern.var_defs.len() > 0 {
self.context_stack.pop();
}
return match result {
Ok(s) => {
if !self.nav_node_id.is_empty() {
match mathml.attribute_value("id") {
None => {},
Some(id) => {
if self.nav_node_id == id {
let highlight_style = self.speech_rules.pref_manager.borrow().get_user_prefs().to_string("BrailleNavHighlight");
return Ok( Some( T::highlight_braille(s, highlight_style) ) );
}
}
}
}
Ok( Some(s) )
},
Err(e) => Err( e.chain_err(||
format!(
"attempting replacement pattern: \"{}\" for \"{}\".\n\
Replacement\n{}\n...due to matching the following MathML with the pattern\n{}\n\
{}\
The patterns are in {}.\n",
pattern.pattern_name, pattern.tag_name,
pattern.replacements.pretty_print_replacements(),pattern.pattern,
mml_to_string(&mathml),
pattern.file_name
)
))
}
} else if pattern.match_uses_var_defs {
self.context_stack.pop();
}
};
return Ok(None);
fn error_string(pattern: &SpeechPattern, mathml: Element) -> String {
return format!(
"error during pattern match using: \"{}\" for \"{}\".\n\
Pattern is \n{}\nMathML for the match:\n\
{}\
The patterns are in {}.\n",
pattern.pattern_name, pattern.tag_name,
pattern.pattern,
mml_to_string(&mathml),
pattern.file_name
);
}
}
fn highlight_braille_string(braille: String, highlight_style: String) -> String {
if &highlight_style == "Off" || braille.is_empty() {
return braille;
}
let mut result = String::with_capacity(braille.len());
let mut i_bytes = 0;
let mut chars = braille.chars();
for ch in chars.by_ref() {
let modified_ch = add_dots_to_braille_char(ch);
i_bytes += ch.len_utf8();
result.push(modified_ch);
if ch != modified_ch {
break;
};
};
let mut i_end = braille.len();
if &highlight_style != "FirstChar" {
let rev_chars = braille.chars().rev();
for ch in rev_chars {
let modified_ch = add_dots_to_braille_char(ch);
i_end -= ch.len_utf8();
if ch != modified_ch {
break;
}
}
}
for ch in chars {
result.push( if i_bytes == i_end {add_dots_to_braille_char(ch)} else {ch} );
i_bytes += ch.len_utf8();
};
return result;
fn add_dots_to_braille_char(ch: char) -> char {
let as_u32 = ch as u32;
if (0x2800..0x28FF).contains(&as_u32) {
return unsafe {char::from_u32_unchecked(as_u32 | 0xC0)};
} else {
return ch;
}
}
}
fn replace<T:TreeOrString<'c, 'm, T>>(&'r mut self, replacement: &Replacement, mathml: Element<'c>) -> Result<T> {
return Ok(
match replacement {
Replacement::Text(t) => T::from_string(t.clone(), self.doc)?,
Replacement::XPath(xpath) => xpath.replace(self, mathml)?,
Replacement::TTS(tts) => {
T::from_string(
self.speech_rules.pref_manager.borrow().get_tts().replace(tts, &self.speech_rules.pref_manager.borrow(), self, mathml)?,
self.doc
)?
},
Replacement::Intent(intent) => {
intent.replace(self, mathml)?
},
Replacement::Test(test) => {
test.replace(self, mathml)?
},
Replacement::With(with) => {
with.replace(self, mathml)?
},
Replacement::SetVariables(vars) => {
vars.replace(self, mathml)?
},
Replacement::Insert(ic) => {
ic.replace(self, mathml)?
},
Replacement::Translate(id) => {
id.replace(self, mathml)?
},
}
)
}
fn replace_nodes<T:TreeOrString<'c, 'm, T>>(&'r mut self, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<T> {
return T::replace_nodes(self, nodes, mathml);
}
fn replace_nodes_tree(&'r mut self, nodes: Vec<Node<'c>>, _mathml: Element<'c>) -> Result<Element<'m>> {
let mut children = Vec::with_capacity(3*nodes.len()); for node in nodes {
let matched = match node {
Node::Element(n) => self.match_pattern::<Element<'m>>(n)?,
Node::Text(t) => {
let leaf = create_mathml_element(&self.doc, "TEMP_NAME");
leaf.set_text(t.text());
leaf
},
Node::Attribute(attr) => {
let leaf = create_mathml_element(&self.doc, "TEMP_NAME");
leaf.set_text(attr.value());
leaf
},
_ => {
bail!("replace_nodes: found unexpected node type!!!");
},
};
children.push(matched);
}
let result = create_mathml_element(&self.doc, "TEMP_NAME"); result.append_children(children);
return Ok( result );
}
fn replace_nodes_string(&'r mut self, nodes: Vec<Node<'c>>, mathml: Element<'c>) -> Result<String> {
let mut result = String::with_capacity(3*nodes.len()); let mut first_time = true;
for node in nodes {
if first_time {
first_time = false;
} else {
result.push(' ');
};
let matched = match node {
Node::Element(n) => self.match_pattern::<String>(n)?,
Node::Text(t) => self.replace_chars(t.text(), mathml)?,
Node::Attribute(attr) => self.replace_chars(attr.value(), mathml)?,
_ => bail!("replace_nodes: found unexpected node type!!!"),
};
result += &matched;
}
return Ok( result );
}
pub fn replace_chars(&'r mut self, str: &str, mathml: Element<'c>) -> Result<String> {
let rules = self.speech_rules;
let mut chars = str.chars();
if rules.translate_single_chars_only {
let ch = chars.next().unwrap_or(' ');
if chars.next().is_none() {
return replace_single_char(self, ch, mathml)
} else {
return Ok(str.replace('\u{00A0}', " "));
}
};
let result = chars
.map(|ch| replace_single_char(self, ch, mathml))
.collect::<Result<Vec<String>>>()?
.join("");
return Ok( result );
fn replace_single_char<'c, 's:'c, 'm, 'r>(rules_with_context: &'r mut SpeechRulesWithContext<'c,'s,'m>, ch: char, mathml: Element<'c>) -> Result<String> {
let ch_as_u32 = ch as u32;
let mut unicode = rules_with_context.speech_rules.unicode_short.borrow();
let mut replacements = unicode.get( &ch_as_u32 );
if replacements.is_none() {
if rules_with_context.speech_rules.unicode_full.borrow().is_empty() {
info!("*** Loading full unicode {} for char '{}'/{:#06x}", rules_with_context.speech_rules.name, ch, ch_as_u32);
rules_with_context.speech_rules.read_unicode(None, false)?;
info!("# Unicode defs = {}/{}", rules_with_context.speech_rules.unicode_short.borrow().len(), rules_with_context.speech_rules.unicode_full.borrow().len());
}
unicode = rules_with_context.speech_rules.unicode_full.borrow();
replacements = unicode.get( &ch_as_u32 );
if replacements.is_none() {
return Ok(String::from(ch)); }
};
return Ok(
replacements.unwrap()
.iter()
.map(|replacement|
rules_with_context.replace(replacement, mathml)
.chain_err(|| format!("Unicode replacement error: {}", replacement)) )
.collect::<Result<Vec<String>>>()?
.join(" ")
);
}
}
}
pub fn braille_replace_chars(str: &str, mathml: Element) -> Result<String> {
return BRAILLE_RULES.with(|rules| {
let rules = rules.borrow();
let new_package = Package::new();
let mut rules_with_context = SpeechRulesWithContext::new(&rules, new_package.as_document(), "".to_string());
return rules_with_context.replace_chars(str, mathml);
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_statement() {
let str = r#"---
{name: default, tag: math, match: ".", replace: [x: "./*"] }"#;
let doc = YamlLoader::load_from_str(str).unwrap();
assert_eq!(doc.len(), 1);
let mut rules = SpeechRules::new(RulesFor::Speech, true);
SpeechPattern::build(&doc[0], Path::new("testing"), &mut rules).unwrap();
assert_eq!(rules.rules["math"].len(), 1, "\nshould only be one rule");
let speech_pattern = &rules.rules["math"][0];
assert_eq!(speech_pattern.pattern_name, "default", "\npattern name failure");
assert_eq!(speech_pattern.tag_name, "math", "\ntag name failure");
assert_eq!(speech_pattern.pattern.rc.string, ".", "\npattern failure");
assert_eq!(speech_pattern.replacements.replacements.len(), 1, "\nreplacement failure");
assert_eq!(speech_pattern.replacements.replacements[0].to_string(), r#"x: "./*""#, "\nreplacement failure");
}
#[test]
fn test_read_statements_with_replace() {
let str = r#"---
{name: default, tag: math, match: ".", replace: [x: "./*"] }"#;
let doc = YamlLoader::load_from_str(str).unwrap();
assert_eq!(doc.len(), 1);
let mut rules = SpeechRules::new(RulesFor::Speech, true);
SpeechPattern::build(&doc[0], Path::new("testing"), &mut rules).unwrap();
let str = r#"---
{name: default, tag: math, match: ".", replace: [t: "test", x: "./*"] }"#;
let doc2 = YamlLoader::load_from_str(str).unwrap();
assert_eq!(doc2.len(), 1);
SpeechPattern::build(&doc2[0], Path::new("testing"), &mut rules).unwrap();
assert_eq!(rules.rules["math"].len(), 1, "\nfirst rule not replaced");
let speech_pattern = &rules.rules["math"][0];
assert_eq!(speech_pattern.pattern_name, "default", "\npattern name failure");
assert_eq!(speech_pattern.tag_name, "math", "\ntag name failure");
assert_eq!(speech_pattern.pattern.rc.string, ".", "\npattern failure");
assert_eq!(speech_pattern.replacements.replacements.len(), 2, "\nreplacement failure");
}
#[test]
fn test_read_statements_with_add() {
let str = r#"---
{name: default, tag: math, match: ".", replace: [x: "./*"] }"#;
let doc = YamlLoader::load_from_str(str).unwrap();
assert_eq!(doc.len(), 1);
let mut rules = SpeechRules::new(RulesFor::Speech, true);
SpeechPattern::build(&doc[0], Path::new("testing"), &mut rules).unwrap();
let str = r#"---
{name: another-rule, tag: math, match: ".", replace: [t: "test", x: "./*"] }"#;
let doc2 = YamlLoader::load_from_str(str).unwrap();
assert_eq!(doc2.len(), 1);
SpeechPattern::build(&doc2[0], Path::new("testing"), &mut rules).unwrap();
assert_eq!(rules.rules["math"].len(), 2, "\nsecond rule not added");
let speech_pattern = &rules.rules["math"][0];
assert_eq!(speech_pattern.pattern_name, "default", "\npattern name failure");
assert_eq!(speech_pattern.tag_name, "math", "\ntag name failure");
assert_eq!(speech_pattern.pattern.rc.string, ".", "\npattern failure");
assert_eq!(speech_pattern.replacements.replacements.len(), 1, "\nreplacement failure");
}
#[test]
fn test_debug_no_debug() {
let str = r#"*[2]/*[3][text()='3']"#;
let result = MyXPath::add_debug_string_arg(str);
assert!(result.is_ok());
assert_eq!(result.unwrap(), str);
}
#[test]
fn test_debug_no_debug_with_quote() {
let str = r#"*[2]/*[3][text()='(']"#;
let result = MyXPath::add_debug_string_arg(str);
assert!(result.is_ok());
assert_eq!(result.unwrap(), str);
}
#[test]
fn test_debug_no_quoted_paren() {
let str = r#"DEBUG(*[2]/*[3][text()='3'])"#;
let result = MyXPath::add_debug_string_arg(str);
assert!(result.is_ok());
assert_eq!(result.unwrap(), r#"DEBUG(*[2]/*[3][text()='3'], "*[2]/*[3][text()='3']" )"#);
}
#[test]
fn test_debug_quoted_paren() {
let str = r#"DEBUG(*[2]/*[3][text()='('])"#;
let result = MyXPath::add_debug_string_arg(str);
assert!(result.is_ok());
assert_eq!(result.unwrap(), r#"DEBUG(*[2]/*[3][text()='('], "*[2]/*[3][text()='(']" )"#);
}
#[test]
fn test_debug_quoted_paren_before_paren() {
let str = r#"DEBUG(ClearSpeak_Matrix = 'Combinatorics') and IsBracketed(., '(', ')')"#;
let result = MyXPath::add_debug_string_arg(str);
assert!(result.is_ok());
assert_eq!(result.unwrap(), r#"DEBUG(ClearSpeak_Matrix = 'Combinatorics', "ClearSpeak_Matrix = 'Combinatorics'" ) and IsBracketed(., '(', ')')"#);
}
}