#![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 std::time::SystemTime;
use crate::definitions::read_definitions_file;
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 const NAV_NODE_SPEECH_NOT_FOUND: &str = "NAV_NODE_NOT_FOUND";
const NO_EVAL_QUOTE_CHAR: char = '\u{e00A}'; const NO_EVAL_QUOTE_CHAR_AS_BYTES: [u8;3] = [0xee,0x80,0x8a];
const N_BYTES_NO_EVAL_QUOTE_CHAR: usize = NO_EVAL_QUOTE_CHAR.len_utf8();
pub fn make_quoted_string(mut string: String) -> String {
string.push(NO_EVAL_QUOTE_CHAR);
return string;
}
pub fn is_quoted_string(str: &str) -> bool {
if str.len() < N_BYTES_NO_EVAL_QUOTE_CHAR {
return false;
}
let bytes = str.as_bytes();
return bytes[bytes.len()-N_BYTES_NO_EVAL_QUOTE_CHAR..] == NO_EVAL_QUOTE_CHAR_AS_BYTES;
}
pub fn unquote_string(str: &str) -> &str {
return &str[..str.len()-N_BYTES_NO_EVAL_QUOTE_CHAR];
}
pub fn intent_from_mathml<'m>(mathml: Element, 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_mathml(mathml: Element, nav_node_id: &str) -> Result<String> {
return speak_rules(&SPEECH_RULES, mathml, nav_node_id);
}
pub fn overview_mathml(mathml: Element, nav_node_id: &str) -> Result<String> {
return speak_rules(&OVERVIEW_RULES, mathml, nav_node_id);
}
fn intent_rules<'m>(rules: &'static std::thread::LocalKey<RefCell<SpeechRules>>, doc: Document<'m>, mathml: Element, nav_node_id: &'m str) -> Result<Element<'m>> {
rules.with(|rules| {
rules.borrow_mut().read_files()?;
let rules = rules.borrow();
let mut rules_with_context = SpeechRulesWithContext::new(&rules, doc, nav_node_id);
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, nav_node_id: &str) -> Result<String> {
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(), nav_node_id);
let mut speech_string = rules_with_context.match_pattern::<String>(mathml)
.chain_err(|| "Pattern match/replacement failure!")?;
if !nav_node_id.is_empty() {
if let Some(start) = speech_string.find("[[") {
match speech_string[start+2..].find("]]") {
None => bail!("Internal error: looking for '[[...]]' during navigation -- only found '[[' in '{}'", speech_string),
Some(end) => speech_string = speech_string[start+2..start+2+end].to_string(),
}
} else {
bail!(NAV_NODE_SPEECH_NOT_FOUND);
}
}
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<Vec<PathBuf>> where
F: FnMut(&Yaml) -> Result<Vec<PathBuf>> {
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]);
}
}
}
pub fn process_include<F>(current_file: &Path, new_file_name: &str, mut read_new_file: F) -> Result<Vec<PathBuf>>
where F: FnMut(&Path) -> Result<Vec<PathBuf>> {
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),
};
let mut included_files = read_new_file(new_file.as_path())?;
let mut files_read = vec![new_file];
files_read.append(&mut included_files);
return Ok(files_read);
}
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;
fn mark_nav_speech(speech: T) -> 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);
}
fn mark_nav_speech(speech: String) -> String {
return SpeechRulesWithContext::mark_nav_speech(speech);
}
}
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");
}
fn mark_nav_speech(_speech: Element<'c>) -> Element<'m> {
panic!("Internal error: mark_nav_speech 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" | "audio" | "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>, id: 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()
};
let default_id = MyXPath::new("'Auto'".to_string()).unwrap();
let id = self.id.as_ref().unwrap_or(&default_id);
return write!(f, "intent: {}: {}, id='{}'>\n children: {}",
if self.name.is_some() {"name"} else {"xpath-name"}, name,
id,
&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 id = &yaml_dict["id"];
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'")?)},
id: if id.is_badvalue() {None} else {Some(MyXPath::build(id).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 mut result = lift_children(result);
if name(&result) != "TEMP_NAME" && name(&result) != "Unknown" {
let temp = create_mathml_element(&result.document(), "TEMP_NAME");
temp.append_child(result);
result = temp;
}
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());
}
if let Some(id) = &self.id {
result.set_attribute_value("id", id.evaluate(rules_with_context.get_context(), mathml)?.string().as_str());
}
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 array 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,
};
match id {
None => bail!("'translate' value '{}' is not a string or an attribute value (correct by using '@id'??):\n", self.id),
Some(id) => {
let speech = speak_mathml(intent_from_mathml(mathml, rules_with_context.get_document())?, &id)?;
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) )?;
let string = match result {
Value::Nodeset(nodes) => {
if nodes.size() == 0 {
bail!("During replacement, no matching element found");
}
return rules_with_context.replace_nodes(nodes.document_order(), mathml);
},
Value::String(s) => s,
Value::Number(num) => num.to_string(),
Value::Boolean(b) => b.to_string(), };
let result = if self.rc.string.starts_with('$') {string} else {rules_with_context.replace_chars(&string, mathml)?};
return T::from_string(result, 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(" }", "") );
}
};
}
pub fn test_input<F>(self, f: F) -> bool where F: Fn(&str) -> bool {
return f(self.rc.string.as_ref());
}
}
#[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<Option<Vec<PathBuf>>> {
if let Some(include_file_name) = find_str(dict, "include") {
let do_include_fn = |new_file: &Path| {
rules.read_patterns(new_file)
};
return Ok( Some(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!("\n***WARNING: replacing {}/'{}' in {} with rule from {}\n",
old_rule.tag_name, old_rule.pattern_name, old_rule.file_name, speech_pattern.file_name);
rule_value[i] = speech_pattern;
},
}
}
return Ok(None);
}
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( () );
}
}
struct VariableValues<'v> {
defs: Vec<VariableValue<'v>>
}
impl<'v> fmt::Display for VariableValues<'v> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for value in &self.defs {
write!(f, "{}", value)?;
}
return writeln!(f);
}
}
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 {
writeln!(f, " {} old_values", self.old_values.len())?;
for values in &self.old_values {
writeln!(f, " {}", values)?;
}
return writeln!(f);
}
}
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 {} with ContextStack {}", def, self)),
};
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<'b>(yaml: &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<Option<Vec<PathBuf>>> {
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 Ok( Some(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();
if unicode_table.insert(ch as u32, ReplacementArray::build(&substitute_ch(replacements, &ch_as_str))
.chain_err(|| format!("In definition of char: '{}'", str))?.replacements).is_some() {
error!("*** Character '{}' (0x{:X}) is repeated", ch, ch as u32);
}
}
return Ok(None);
}
}
}
let ch = UnicodeDef::get_unicode_char(ch)?;
if unicode_table.insert(ch, ReplacementArray::build(replacements)
.chain_err(|| format!("In definition of char: '{}' (0x{})",
char::from_u32(ch).unwrap(), ch))?.replacements).is_some() {
error!("*** Character '{}' (0x{:X}) is repeated", char::from_u32(ch).unwrap(), ch);
}
return Ok(None);
fn process_range(def_range: &str, replacements: &Yaml, mut unicode_table: RefMut<HashMap<u32,Vec<Replacement>>>) -> Result<Option<Vec<PathBuf>>> {
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(None);
}
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>>>>;
type FilesAndTimesShared = Rc<RefCell<FilesAndTimes>>;
#[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);
}
}
#[derive(Debug, Clone)]
pub struct FileAndTime {
file: PathBuf,
time: SystemTime,
}
impl FileAndTime {
fn new(file: PathBuf) -> FileAndTime {
return FileAndTime {
file,
time: SystemTime::UNIX_EPOCH,
}
}
pub fn new_with_time(file: PathBuf) -> FileAndTime {
return FileAndTime {
time: FileAndTime::get_metadata(&file),
file,
}
}
pub fn is_up_to_date(&self) -> bool {
let file_mod_time = FileAndTime::get_metadata(&self.file);
return self.time >= file_mod_time;
}
fn get_metadata(path: &Path) -> SystemTime {
use std::fs;
if !cfg!(target_family = "wasm") {
let metadata = fs::metadata(path);
if let Ok(metadata) = metadata {
if let Ok(mod_time) = metadata.modified() {
return mod_time;
}
}
}
return SystemTime::UNIX_EPOCH
}
}
#[derive(Debug, Default)]
pub struct FilesAndTimes {
ft: Vec<FileAndTime>
}
impl FilesAndTimes {
pub fn new(start_path: PathBuf) -> FilesAndTimes {
let mut ft = Vec::with_capacity(8);
ft.push( FileAndTime::new(start_path) );
return FilesAndTimes{ ft };
}
pub fn is_file_up_to_date(&self, pref_path: &Path, should_ignore_file_time: bool) -> bool {
if self.ft.is_empty() || self.ft[0].time == SystemTime::UNIX_EPOCH || self.as_path() != pref_path {
return false;
}
if should_ignore_file_time || cfg!(target_family = "wasm") {
return !self.ft.is_empty();
}
for file in &self.ft {
if !file.is_up_to_date() {
return false;
}
}
return true;
}
fn set_files_and_times(&mut self, new_files: Vec<PathBuf>) {
self.ft.clear();
for path in new_files {
let time = FileAndTime::get_metadata(&path); self.ft.push( FileAndTime{ file: path, time })
}
}
pub fn as_path(&self) -> &Path {
assert!(!self.ft.is_empty());
return &self.ft[0].file;
}
pub fn paths(&self) -> Vec<PathBuf> {
return self.ft.iter().map(|ft| ft.file.clone()).collect::<Vec<PathBuf>>();
}
}
pub struct SpeechRules {
error: String,
name: RulesFor,
pub pref_manager: Rc<RefCell<PreferenceManager>>,
rules: RuleTable, rule_files: FilesAndTimes, translate_single_chars_only: bool, unicode_short: UnicodeTable, unicode_short_files: FilesAndTimesShared, unicode_full: UnicodeTable, unicode_full_files: FilesAndTimesShared, definitions_files: FilesAndTimesShared, }
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: &'m str,
pub inside_spell: bool, pub translate_count: usize, }
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(497) ) );
static SPEECH_UNICODE_FULL: UnicodeTable =
Rc::new( RefCell::new( HashMap::with_capacity(6997) ) );
static BRAILLE_UNICODE_SHORT: UnicodeTable =
Rc::new( RefCell::new( HashMap::with_capacity(497) ) );
static BRAILLE_UNICODE_FULL: UnicodeTable =
Rc::new( RefCell::new( HashMap::with_capacity(6997) ) );
static SPEECH_DEFINITION_FILES_AND_TIMES: FilesAndTimesShared =
Rc::new( RefCell::new(FilesAndTimes::default()) );
static BRAILLE_DEFINITION_FILES_AND_TIMES: FilesAndTimesShared =
Rc::new( RefCell::new(FilesAndTimes::default()) );
static SPEECH_UNICODE_SHORT_FILES_AND_TIMES: FilesAndTimesShared =
Rc::new( RefCell::new(FilesAndTimes::default()) );
static SPEECH_UNICODE_FULL_FILES_AND_TIMES: FilesAndTimesShared =
Rc::new( RefCell::new(FilesAndTimes::default()) );
static BRAILLE_UNICODE_SHORT_FILES_AND_TIMES: FilesAndTimesShared =
Rc::new( RefCell::new(FilesAndTimes::default()) );
static BRAILLE_UNICODE_FULL_FILES_AND_TIMES: FilesAndTimesShared =
Rc::new( RefCell::new(FilesAndTimes::default()) );
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 {
let globals = if name == RulesFor::Braille {
(
(BRAILLE_UNICODE_SHORT.with(Rc::clone), BRAILLE_UNICODE_SHORT_FILES_AND_TIMES.with(Rc::clone)),
(BRAILLE_UNICODE_FULL. with(Rc::clone), BRAILLE_UNICODE_FULL_FILES_AND_TIMES.with(Rc::clone)),
BRAILLE_DEFINITION_FILES_AND_TIMES.with(Rc::clone),
)
} else {
(
(SPEECH_UNICODE_SHORT.with(Rc::clone), SPEECH_UNICODE_SHORT_FILES_AND_TIMES.with(Rc::clone)),
(SPEECH_UNICODE_FULL. with(Rc::clone), SPEECH_UNICODE_FULL_FILES_AND_TIMES.with(Rc::clone)),
SPEECH_DEFINITION_FILES_AND_TIMES.with(Rc::clone),
)
};
return SpeechRules {
error: Default::default(),
name,
rules: HashMap::with_capacity(if name == RulesFor::Intent {1023} else {31}), rule_files: FilesAndTimes::default(),
unicode_short: globals.0.0, unicode_short_files: globals.0.1,
unicode_full: globals.1.0, unicode_full_files: globals.1.1,
definitions_files: globals.2,
translate_single_chars_only,
pref_manager: PreferenceManager::get(),
};
}
pub fn get_error(&self) -> Option<&str> {
return if self.error.is_empty() {
None
} else {
Some(&self.error)
}
}
pub fn read_files(&mut self) -> Result<()> {
let check_rule_files = self.pref_manager.borrow().pref_to_string("CheckRuleFiles");
if check_rule_files != "None" { self.pref_manager.borrow_mut().set_preference_files()?;
}
let should_ignore_file_time = self.pref_manager.borrow().pref_to_string("CheckRuleFiles") != "All"; let rule_file = self.pref_manager.borrow().get_rule_file(&self.name).to_path_buf(); if self.rules.is_empty() || !self.rule_files.is_file_up_to_date(&rule_file, should_ignore_file_time) {
self.rules.clear();
let files_read = self.read_patterns(&rule_file)?;
self.rule_files.set_files_and_times(files_read);
}
let pref_manager = self.pref_manager.borrow();
let unicode_pref_files = if self.name == RulesFor::Braille {pref_manager.get_braille_unicode_file()} else {pref_manager.get_speech_unicode_file()};
if !self.unicode_short_files.borrow().is_file_up_to_date(unicode_pref_files.0, should_ignore_file_time) {
self.unicode_short.borrow_mut().clear();
self.unicode_short_files.borrow_mut().set_files_and_times(self.read_unicode(None, true)?);
}
if self.definitions_files.borrow().ft.is_empty() || !self.definitions_files.borrow().is_file_up_to_date(
pref_manager.get_definitions_file(self.name != RulesFor::Braille),
should_ignore_file_time
) {
self.definitions_files.borrow_mut().set_files_and_times(read_definitions_file(self.name != RulesFor::Braille)?);
}
return Ok( () );
}
fn read_patterns(&mut self, path: &Path) -> Result<Vec<PathBuf>> {
let rule_file_contents = read_to_string_shim(path).chain_err(|| format!("cannot read file '{}'", path.to_str().unwrap()))?;
let rules_build_fn = |pattern: &Yaml| {
self.build_speech_patterns(pattern, path)
.chain_err(||format!("in file {:?}", path.to_str().unwrap()))
};
return compile_rule(&rule_file_contents, rules_build_fn)
.chain_err(||format!("in file {:?}", path.to_str().unwrap()));
}
fn build_speech_patterns(&mut self, patterns: &Yaml, file_name: &Path) -> Result<Vec<PathBuf>> {
let patterns_vec = patterns.as_vec();
if patterns_vec.is_none() {
bail!(yaml_type_err(patterns, "array"));
}
let patterns_vec = patterns.as_vec().unwrap();
let mut files_read = vec![file_name.to_path_buf()];
for entry in patterns_vec.iter() {
if let Some(mut added_files) = SpeechPattern::build(entry, file_name, self)? {
files_read.append(&mut added_files);
}
}
return Ok(files_read);
}
fn read_unicode(&self, path: Option<PathBuf>, use_short: bool) -> Result<Vec<PathBuf>> {
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()
};
let unicode_files = if use_short {unicode_files.0} else {unicode_files.1};
unicode_files.to_path_buf()
}
};
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));
};
let mut files_read = vec![path.to_path_buf()];
for unicode_def in unicode_defs.unwrap() {
if let Some(mut added_files) = UnicodeDef::build(unicode_def, &path, self, use_short)
.chain_err(|| {format!("In file {:?}", path.to_str())})? {
files_read.append(&mut added_files);
}
};
return Ok(files_read);
};
return compile_rule(&unicode_file_contents, unicode_build_fn)
.chain_err(||format!("in file {:?}", path.to_str().unwrap()));
}
}
cfg_if! {
if #[cfg(target_family = "wasm")] {
pub fn invalidate_all() {
SPEECH_RULES.with( |rules| {
let mut rules = rules.borrow_mut();
rules.rule_files.invalidate();
rules.unicode_short_files.borrow_mut().invalidate();
rules.unicode_full_files.borrow_mut().invalidate();
rules.definitions_files.borrow_mut().invalidate();
});
BRAILLE_RULES.with( |rules| {
let mut rules = rules.borrow_mut();
rules.rule_files.invalidate();
rules.unicode_short_files.borrow_mut().invalidate();
rules.unicode_full_files.borrow_mut().invalidate();
rules.definitions_files.borrow_mut().invalidate();
});
NAVIGATION_RULES.with( |rules| {
rules.borrow_mut().rule_files.invalidate();
});
OVERVIEW_RULES.with( |rules| {
rules.borrow_mut().rule_files.invalidate();
});
INTENT_RULES.with( |rules| {
rules.borrow_mut().rule_files.invalidate();
});
}
}
}
impl<'c, 's:'c, 'r, 'm:'c> SpeechRulesWithContext<'c, 's,'m> {
pub fn new(speech_rules: &'s SpeechRules, doc: Document<'m>, nav_node_id: &'m str) -> SpeechRulesWithContext<'c, 's, 'm> {
return SpeechRulesWithContext {
speech_rules,
context_stack: ContextStack::new(&speech_rules.pref_manager.borrow()),
doc,
nav_node_id,
inside_spell: false,
translate_count: 0,
}
}
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 speech_manager = self.speech_rules.pref_manager.borrow();
let file_name = speech_manager.get_rule_file(&self.speech_rules.name);
bail!("\nNo match found!\nMissing patterns in {} for MathML.\n{}", file_name.to_string_lossy(), 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() {
Ok( Some(s) )
} else {
Ok ( Some(self.nav_node_adjust(s, mathml)) )
}
},
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 nav_node_adjust<T:TreeOrString<'c, 'm, T>>(&self, speech: T, mathml: Element<'c>) -> T {
if let Some(id) = mathml.attribute_value("id") {
if self.nav_node_id == id {
if self.speech_rules.name == RulesFor::Braille {
let highlight_style = self.speech_rules.pref_manager.borrow().pref_to_string("BrailleNavHighlight");
return T::highlight_braille(speech, highlight_style);
} else {
return T::mark_nav_speech(speech)
}
}
}
return speech;
}
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();
let baseline_indicator_hack = PreferenceManager::get().borrow().pref_to_string("BrailleCode") == "Nemeth";
for ch in chars.by_ref() {
let modified_ch = add_dots_to_braille_char(ch, baseline_indicator_hack);
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, baseline_indicator_hack);
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, baseline_indicator_hack)} else {ch} );
i_bytes += ch.len_utf8();
};
return result;
fn add_dots_to_braille_char(ch: char, baseline_indicator_hack: bool) -> char {
let as_u32 = ch as u32;
if (0x2800..0x28FF).contains(&as_u32) {
return unsafe {char::from_u32_unchecked(as_u32 | 0xC0)};
} else if baseline_indicator_hack && ch == 'b' {
return '𝑏'
} else {
return ch;
}
}
}
fn mark_nav_speech(speech: String) -> String {
return "[[".to_string() + &speech + "]]";
}
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> {
if is_quoted_string(str) {
return Ok(unquote_string(str).to_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}', " ").replace(['\u{2061}', '\u{2062}', '\u{2063}', '\u{2064}'], ""));
}
};
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 rules = rules_with_context.speech_rules;
let mut unicode = rules.unicode_short.borrow();
let mut replacements = unicode.get( &ch_as_u32 );
if replacements.is_none() {
let pref_manager = rules.pref_manager.borrow();
let unicode_pref_files = if rules.name == RulesFor::Braille {pref_manager.get_braille_unicode_file()} else {pref_manager.get_speech_unicode_file()};
let should_ignore_file_time = pref_manager.pref_to_string("CheckRuleFiles") == "All";
if rules.unicode_full.borrow().is_empty() || !rules.unicode_full_files.borrow().is_file_up_to_date(unicode_pref_files.1, should_ignore_file_time) {
info!("*** Loading full unicode {} for char '{}'/{:#06x}", rules.name, ch, ch_as_u32);
rules.unicode_full.borrow_mut().clear();
rules.unicode_full_files.borrow_mut().set_files_and_times(rules.read_unicode(None, false)?);
info!("# Unicode defs = {}/{}", rules.unicode_short.borrow().len(), rules.unicode_full.borrow().len());
}
unicode = rules.unicode_full.borrow();
replacements = unicode.get( &ch_as_u32 );
if replacements.is_none() {
rules_with_context.translate_count = 0; return Ok(String::from(ch)); }
};
let result = replacements.unwrap()
.iter()
.map(|replacement|
rules_with_context.replace(replacement, mathml)
.chain_err(|| format!("Unicode replacement error: {}", replacement)) )
.collect::<Result<Vec<String>>>()?
.join(" ");
rules_with_context.translate_count = 0; return Ok(result);
}
}
}
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(), "");
return rules_with_context.replace_chars(str, mathml);
})
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use crate::init_logger;
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(., '(', ')')"#);
}
#[test]
fn test_up_to_date() {
use crate::interface::*;
set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
set_preference("Language".to_string(), "zz-aa".to_string()).unwrap();
if let Err(e) = set_mathml("<math><mi>x</mi></math>".to_string()) {
error!("{}", crate::errors_to_string(&e));
panic!("Should not be an error in setting MathML")
}
set_preference("CheckRuleFiles".to_string(), "All".to_string()).unwrap();
assert!(!is_file_time_same(), "file's time did not get updated");
set_preference("CheckRuleFiles".to_string(), "None".to_string()).unwrap();
assert!(is_file_time_same(), "file's time was wrongly updated (preference 'CheckRuleFiles' should have prevented updating)");
fn is_file_time_same() -> bool {
use std::time::Duration;
return SPEECH_RULES.with(|rules| {
let start_main_file = rules.borrow().unicode_short_files.borrow().ft[0].clone();
let contents = std::fs::read(&start_main_file.file).expect(&format!("Failed to read file {} during test", &start_main_file.file.to_string_lossy()));
std::fs::write(start_main_file.file, contents).unwrap();
std::thread::sleep(Duration::from_millis(5)); if let Err(e) = get_spoken_text() {
error!("{}", crate::errors_to_string(&e));
panic!("Should not be an error in speech")
}
return rules.borrow().unicode_short_files.borrow().ft[0].time == start_main_file.time;
});
}
}
}