#![allow(non_snake_case)]
#![allow(clippy::needless_return)]
use std::cell::RefCell;
use sxd_document::parser;
use sxd_document::Package;
use sxd_document::dom::*;
use crate::errors::*;
use regex::{Regex, Captures};
use phf::phf_map;
use crate::canonicalize::{name, as_element};
use crate::navigate::*;
use crate::pretty_print::mml_to_string;
use crate::xpath_functions::is_leaf;
fn cleanup_mathml(mathml: Element) -> Result<Element> {
trim_element(&mathml);
let mathml = crate::canonicalize::canonicalize(mathml)?;
let mathml = add_ids(mathml);
return Ok(mathml);
}
thread_local!{
pub static MATHML_INSTANCE: RefCell<Package> = init_mathml_instance();
}
fn init_mathml_instance() -> RefCell<Package> {
let package = parser::parse("<math></math>")
.expect("Internal error in 'init_mathml_instance;: didn't parse initializer string");
return RefCell::new( package );
}
pub fn set_rules_dir(dir: String) -> Result<()> {
use std::path::PathBuf;
let pref_manager = crate::prefs::PreferenceManager::get();
return pref_manager.borrow_mut().initialize(PathBuf::from(dir));
}
pub fn get_version() -> String {
const VERSION: &str = env!("CARGO_PKG_VERSION");
return VERSION.to_string();
}
pub fn set_mathml(mathml_str: String) -> Result<String> {
lazy_static! {
static ref MATHJAX_V2: Regex = Regex::new(r#"class *= *['"]MJX-.*?['"]"#).unwrap();
static ref MATHJAX_V3: Regex = Regex::new(r#"class *= *['"]data-mjx-.*?['"]"#).unwrap();
static ref NAMESPACE_DECL: Regex = Regex::new(r#"xmlns:[[:alpha:]]+"#).unwrap(); static ref PREFIX: Regex = Regex::new(r#"(</?)[[:alpha:]]+:"#).unwrap(); static ref HTML_ENTITIES: Regex = Regex::new(r#"&([a-zA-Z]+?);"#).unwrap();
}
NAVIGATION_STATE.with(|nav_stack| {
nav_stack.borrow_mut().reset();
});
return MATHML_INSTANCE.with(|old_package| {
static HTML_ENTITIES_MAPPING: phf::Map<&str, &str> = include!("entities.in");
let mut error_message = "".to_string(); let mathml_str = HTML_ENTITIES.replace_all(&mathml_str, |cap: &Captures| {
match HTML_ENTITIES_MAPPING.get(&cap[1]) {
None => {
error_message = format!("No entity named '{}'", &cap[0]);
cap[0].to_string()
},
Some(&ch) => ch.to_string(),
}
});
if !error_message.is_empty() {
bail!(error_message);
}
let mathml_str = MATHJAX_V2.replace_all(&mathml_str, "");
let mathml_str = MATHJAX_V3.replace_all(&mathml_str, "");
let mathml_str = NAMESPACE_DECL.replace(&mathml_str, "xmlns"); let mathml_str = PREFIX.replace_all(&mathml_str, "$1");
let new_package = parser::parse(&mathml_str);
if let Err(e) = new_package {
bail!("Invalid MathML input:\n{}\nError is: {}", &mathml_str, &e.to_string());
}
crate::speech::SpeechRules::initialize_all_rules()?;
let new_package = new_package.unwrap();
let mathml = get_element(&new_package);
let mathml = cleanup_mathml(mathml)?;
let mathml_string = mml_to_string(&mathml);
old_package.replace(new_package);
return Ok( mathml_string );
})
}
pub fn get_spoken_text() -> Result<String> {
return MATHML_INSTANCE.with(|package_instance| {
let package_instance = package_instance.borrow();
let mathml = get_element(&package_instance);
let new_package = Package::new();
let intent = crate::speech::intent_from_mathml(mathml, new_package.as_document())?;
debug!("Intent tree:\n{}", mml_to_string(&intent));
let speech = crate::speech::speak_intent(intent)?;
return Ok( speech );
});
}
pub fn get_overview_text() -> Result<String> {
return MATHML_INSTANCE.with(|package_instance| {
let package_instance = package_instance.borrow();
let mathml = get_element(&package_instance);
let speech = crate::speech::overview_mathml(mathml)?;
return Ok( speech );
});
}
pub fn get_preference(name: String) -> Result<String> {
use yaml_rust::Yaml;
return crate::speech::SPEECH_RULES.with(|rules| {
let rules = rules.borrow();
let pref_manager = rules.pref_manager.borrow();
let prefs = pref_manager.merge_prefs();
return match prefs.get(&name) {
None => bail!("No preference named '{}'", &name),
Some(yaml) => match yaml {
Yaml::String(s) => Ok(s.clone()),
Yaml::Boolean(b) => Ok( (if *b {"true"} else {"false"}).to_string() ),
Yaml::Integer(i) => Ok( format!("{}", *i)),
Yaml::Real(s) => Ok(s.clone()),
_ => bail!("Internal error in get_preference -- unknown YAML type"),
},
}
});
}
pub fn set_preference(name: String, value: String) -> Result<()> {
return crate::speech::SPEECH_RULES.with(|rules| {
let mut rules = rules.borrow_mut();
if let Some(error_string) = rules.get_error() {
bail!("{}", error_string);
}
let files_changed;
{
use crate::prefs::NO_PREFERENCE;
let mut pref_manager = rules.pref_manager.borrow_mut();
if pref_manager.get_api_prefs().to_string(&name) != NO_PREFERENCE {
match name.as_str() {
"Pitch" | "Rate" | "Volume" | "CapitalLetters_Pitch"=> {
pref_manager.set_api_float_pref(&name, to_float(&name, &value)?);
},
"Bookmark" | "CapitalLetters_UseWord" => {
pref_manager.set_api_boolean_pref(&name, value.to_lowercase()=="true");
},
_ => {
pref_manager.set_api_string_pref(&name, &value);
}
}
files_changed = None;
} else if pref_manager.get_user_prefs().to_string(name.as_str()) == NO_PREFERENCE {
bail!("set_preference: {} is not a known preference", &name);
} else {
files_changed = pref_manager.set_user_prefs(&name, &value); }
pref_manager.merge_prefs();
}
match name.as_str() {
"SpeechStyle" => {
if let Some(files_changed) = files_changed {
rules.invalidate(files_changed);
}
},
"Language" => {
if !( value.len() == 2 ||
(value.len() == 5 && value.as_bytes()[2] == b'-') ) {
bail!("Improper format for 'Language' preference '{}'. Should be of form 'en' or 'en-gb'", value);
}
if let Some(files_changed) = files_changed {
rules.invalidate(files_changed);
}
},
"BrailleCode" => {
crate::speech::BRAILLE_RULES.with(|braille_rules| {
if let Some(files_changed) = files_changed {
braille_rules.borrow_mut().invalidate(files_changed);
}
})
},
_ => {
}
}
return Ok( () );
});
fn to_float(name: &str, value: &str) -> Result<f64> {
match value.parse::<f64>() {
Ok(val) => return Ok(val),
Err(_) => bail!("SetPreference: preference'{}'s value '{}' must be a float", name, value),
};
}
}
pub fn get_braille(nav_node_id: String) -> Result<String> {
return MATHML_INSTANCE.with(|package_instance| {
let package_instance = package_instance.borrow();
let mathml = get_element(&package_instance);
let braille = crate::braille::braille_mathml(mathml, nav_node_id)?;
return Ok( braille );
});
}
pub fn do_navigate_keypress(key: usize, shift_key: bool, control_key: bool, alt_key: bool, meta_key: bool) -> Result<String> {
return MATHML_INSTANCE.with(|package_instance| {
let package_instance = package_instance.borrow();
let mathml = get_element(&package_instance);
return do_mathml_navigate_key_press(mathml, key, shift_key, control_key, alt_key, meta_key);
});
}
pub fn do_navigate_command(command: String) -> Result<String> {
let command = NAV_COMMANDS.get_key(&command); if command.is_none() {
bail!("Unknown command in call to DoNavigateCommand()");
};
let command = *command.unwrap();
return MATHML_INSTANCE.with(|package_instance| {
let package_instance = package_instance.borrow();
let mathml = get_element(&package_instance);
return do_navigate_command_string(mathml, command);
});
}
pub fn get_navigation_mathml() -> Result<(String, usize)> {
return MATHML_INSTANCE.with(|package_instance| {
let package_instance = package_instance.borrow();
let mathml = get_element(&package_instance);
return NAVIGATION_STATE.with(|nav_stack| {
return match nav_stack.borrow_mut().get_navigation_mathml(mathml) {
Err(e) => Err(e),
Ok( (found, offset) ) => Ok( (mml_to_string(&found), offset) ),
}
} )
});
}
pub fn get_navigation_mathml_id() -> Result<(String, usize)> {
return MATHML_INSTANCE.with(|package_instance| {
let package_instance = package_instance.borrow();
let mathml = get_element(&package_instance);
return Ok( NAVIGATION_STATE.with(|nav_stack| {
return nav_stack.borrow().get_navigation_mathml_id(mathml);
}) )
});
}
pub fn errors_to_string(e:&Error) -> String {
let mut result = String::default();
let mut first_time = true;
for e in e.iter() {
if first_time {
result = format!("{}\n", e);
first_time = false;
} else {
result += &format!("caused by: {}\n", e);
}
}
return result;
}
fn add_ids(mathml: Element) -> Element {
use std::time::SystemTime;
let time = if cfg!(target_family = "wasm") {
rand::random::<usize>()
} else {
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as usize
};
let time_part = radix_fmt::radix(time, 36).to_string();
let random_part = radix_fmt::radix(rand::random::<usize>(), 36).to_string();
let prefix = "M".to_string() + &time_part[time_part.len()-3..] + &random_part[random_part.len()-4..] + "-"; add_ids_to_all(mathml, &prefix, 0);
return mathml;
fn add_ids_to_all(mathml: Element, id_prefix: &str, count: usize) -> usize {
let mut count = count;
if mathml.attribute("id").is_none() {
mathml.set_attribute_value("id", (id_prefix.to_string() + &count.to_string()).as_str());
mathml.set_attribute_value("data-id-added", "true");
count += 1;
};
if crate::xpath_functions::is_leaf(mathml) {
return count;
}
for child in mathml.children() {
let child = as_element(child);
count = add_ids_to_all(child, id_prefix, count);
}
return count;
}
}
pub fn get_element(package: &Package) -> Element {
let doc = package.as_document();
let mut result = None;
for root_child in doc.root().children() {
if let ChildOfRoot::Element(e) = root_child {
assert!(result.is_none());
result = Some(e);
}
};
return result.unwrap();
}
#[allow(dead_code)]
fn trim_doc(doc: &Document) {
for root_child in doc.root().children() {
if let ChildOfRoot::Element(e) = root_child {
trim_element(&e);
} else {
doc.root().remove_child(root_child); }
};
}
pub fn trim_element(e: &Element) {
const TEMP_NBSP: &str = "\u{F8FB}";
if is_leaf(*e) {
make_leaf_element(*e);
return;
}
let mut single_text = "".to_string();
for child in e.children() {
match child {
ChildOfElement::Element(c) => {
trim_element(&c);
},
ChildOfElement::Text(t) => {
single_text += t.text();
e.remove_child(child);
},
_ => {
e.remove_child(child);
}
}
}
let trimmed_text = single_text.replace('Â ', TEMP_NBSP).trim().replace(TEMP_NBSP, "Â ");
if !e.children().is_empty() && !trimmed_text.is_empty() {
error!("trim_element: both element and textual children which shouldn't happen -- ignoring text '{}'", single_text);
}
if e.children().is_empty() && !single_text.is_empty() {
e.set_text(&trimmed_text);
}
fn make_leaf_element(mathml_leaf: Element) {
let children = mathml_leaf.children();
if children.is_empty() {
return;
}
let mut text ="".to_string();
for child in children {
match child {
ChildOfElement::Element(e) => {
if name(&e) == "mglyph" {
text += e.attribute_value("alt").unwrap_or("");
} else {
make_leaf_element(e);
match e.children()[0] {
ChildOfElement::Text(t) => text += t.text(),
_ => panic!("as_text: internal error -- make_leaf_element found non-text child"),
}
}
}
ChildOfElement::Text(t) => text += t.text(),
_ => (),
}
}
mathml_leaf.clear_children();
let trimmed_text = text.replace('Â ', TEMP_NBSP).trim().replace(TEMP_NBSP, "Â ");
mathml_leaf.set_text(&trimmed_text);
}
}
#[allow(dead_code)]
fn is_same_doc(doc1: &Document, doc2: &Document) -> Result<()> {
if doc1.root().children().len() != doc2.root().children().len() {
bail!("Children of docs have {} != {} children", doc1.root().children().len(), doc2.root().children().len());
}
for (i, (c1, c2)) in doc1.root().children().iter().zip(doc2.root().children().iter()).enumerate() {
match c1 {
ChildOfRoot::Element(e1) => {
if let ChildOfRoot::Element(e2) = c2 {
is_same_element(e1, e2)?;
} else {
bail!("child #{}, first is element, second is something else", i);
}
},
ChildOfRoot::Comment(com1) => {
if let ChildOfRoot::Comment(com2) = c2 {
if com1.text() != com2.text() {
bail!("child #{} -- comment text differs", i);
}
} else {
bail!("child #{}, first is comment, second is something else", i);
}
}
ChildOfRoot::ProcessingInstruction(p1) => {
if let ChildOfRoot::ProcessingInstruction(p2) = c2 {
if p1.target() != p2.target() || p1.value() != p2.value() {
bail!("child #{} -- processing instruction differs", i);
}
} else {
bail!("child #{}, first is processing instruction, second is something else", i);
}
}
}
};
return Ok( () );
}
#[allow(dead_code)]
pub fn is_same_element(e1: &Element, e2: &Element) -> Result<()> {
if name(e1) != name(e2) {
bail!("Names not the same: {}, {}", name(e1), name(e2));
}
if e1.children().len() != e2.children().len() {
bail!("Children of {} have {} != {} children", name(e1), e1.children().len(), e2.children().len());
}
if let Err(e) = attrs_are_same(e1.attributes(), e2.attributes()) {
bail!("In element {}, {}", name(e1), e);
}
for (i, (c1, c2)) in e1.children().iter().zip(e2.children().iter()).enumerate() {
match c1 {
ChildOfElement::Element(child1) => {
if let ChildOfElement::Element(child2) = c2 {
is_same_element(child1, child2)?;
} else {
bail!("{} child #{}, first is element, second is something else", name(e1), i);
}
},
ChildOfElement::Comment(com1) => {
if let ChildOfElement::Comment(com2) = c2 {
if com1.text() != com2.text() {
bail!("{} child #{} -- comment text differs", name(e1), i);
}
} else {
bail!("{} child #{}, first is comment, second is something else", name(e1), i);
}
}
ChildOfElement::ProcessingInstruction(p1) => {
if let ChildOfElement::ProcessingInstruction(p2) = c2 {
if p1.target() != p2.target() || p1.value() != p2.value() {
bail!("{} child #{} -- processing instruction differs", name(e1), i);
}
} else {
bail!("{} child #{}, first is processing instruction, second is something else", name(e1), i);
}
}
ChildOfElement::Text(t1) => {
if let ChildOfElement::Text(t2) = c2 {
if t1.text() != t2.text() {
bail!("{} child #{} -- text differs", name(e1), i);
}
} else {
bail!("{} child #{}, first is text, second is something else", name(e1), i);
}
}
}
};
return Ok( () );
fn attrs_are_same(attrs1: Vec<Attribute>, attrs2: Vec<Attribute>) -> Result<()> {
if attrs1.len() != attrs2.len() {
bail!("Attributes have different length: {:?} != {:?}", attrs1, attrs2);
}
for attr1 in attrs1 {
if !attrs2.iter().any(|attr2| attr1.name().local_part() == attr2.name().local_part() && attr1.value() == attr2.value() ) {
bail!("Attribute {} not in [{}]", print_attr(&attr1), print_attrs(&attrs2));
}
}
return Ok( () );
fn print_attr(attr: &Attribute) -> String {
return format!("@{}='{}'", attr.name().local_part(), attr.value());
}
fn print_attrs(attrs: &[Attribute]) -> String {
return attrs.iter()
.map(print_attr)
.collect::<Vec<String>>()
.join(", ");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn are_parsed_strs_equal(str1: &str, str2: &str) -> bool {
let package1 = &parser::parse(str1).expect("Failed to parse input");
let doc1 = package1.as_document();
trim_doc(&doc1);
debug!("doc1:\n{}", mml_to_string(&get_element(&package1)));
let package2 = parser::parse(str2).expect("Failed to parse input");
let doc2 = package2.as_document();
trim_doc(&doc2);
debug!("doc2:\n{}", mml_to_string(&get_element(&package2)));
match is_same_doc(&doc1, &doc2) {
Ok(_) => return true,
Err(e) => panic!("{}", e),
}
}
#[test]
fn trim_same() {
let trimmed_str = "<math><mrow><mo>-</mo><mi>a</mi></mrow></math>";
assert!(are_parsed_strs_equal(trimmed_str, trimmed_str));
}
#[test]
fn trim_whitespace() {
let trimmed_str = "<math><mrow><mo>-</mo><mi> a </mi></mrow></math>";
let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
assert!(are_parsed_strs_equal(trimmed_str, whitespace_str));
}
#[test]
fn no_trim_whitespace_nbsp() {
let trimmed_str = "<math><mrow><mo>-</mo><mtext>  a </mtext></mrow></math>";
let whitespace_str = "<math> <mrow ><mo>-</mo><mtext>  a </mtext></mrow ></math>";
assert!(are_parsed_strs_equal(trimmed_str, whitespace_str));
}
#[test]
fn trim_comment() {
let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
let comment_str = "<math><mrow><mo>-</mo><!--a comment --><mi> a </mi></mrow></math>";
assert!(are_parsed_strs_equal(comment_str, whitespace_str));
}
#[test]
fn replace_mglyph() {
let mglyph_str = "<math>
<mrow>
<mi>X<mglyph fontfamily='my-braid-font' index='2' alt='23braid' /></mi>
<mo>+</mo>
<mi>
<mglyph fontfamily='my-braid-font' index='5' alt='132braid' />Y
</mi>
<mo>=</mo>
<mi>
<mglyph fontfamily='my-braid-font' index='3' alt='13braid' />
</mi>
</mrow>
</math>";
let result_str = "<math>
<mrow>
<mi>X23braid</mi>
<mo>+</mo>
<mi>132braidY</mi>
<mo>=</mo>
<mi>13braid</mi>
</mrow>
</math>";
assert!(are_parsed_strs_equal(mglyph_str, result_str));
}
#[test]
fn trim_differs() {
let whitespace_str = "<math> <mrow ><mo>-</mo><mi> a </mi></mrow ></math>";
let different_str = "<math> <mrow ><mo>-</mo><mi> b </mi></mrow ></math>";
let package1 = &parser::parse(whitespace_str).expect("Failed to parse input");
let doc1 = package1.as_document();
trim_doc(&doc1);
debug!("doc1:\n{}", mml_to_string(&get_element(&package1)));
let package2 = parser::parse(different_str).expect("Failed to parse input");
let doc2 = package2.as_document();
trim_doc(&doc2);
debug!("doc2:\n{}", mml_to_string(&get_element(&package2)));
assert!(is_same_doc(&doc1, &doc2).is_err());
}
#[test]
fn test_entities() {
set_rules_dir(super::super::abs_rules_dir_path()).unwrap();
let entity_str = set_mathml("<math><mrow><mo>−</mo><mi>𝕞</mi></mrow></math>".to_string()).unwrap();
let converted_str = set_mathml("<math><mrow><mo>−</mo><mi>𝕞</mi></mrow></math>".to_string()).unwrap();
lazy_static! {
static ref ID_MATCH: Regex = Regex::new(r#"id='.+?' "#).unwrap();
}
let entity_str = ID_MATCH.replace_all(&entity_str, "");
let converted_str = ID_MATCH.replace_all(&converted_str, "");
assert_eq!(entity_str, converted_str);
}
#[test]
fn can_recover_from_invalid_set_rules_dir() {
use std::env;
env::set_var("MathCATRulesDir", "MathCATRulesDir");
assert!(set_rules_dir("someInvalidRulesDir".to_string()).is_err());
assert!(set_rules_dir(super::super::abs_rules_dir_path()).is_ok());
assert!(set_mathml("<math><mn>1</mn></math>".to_string()).is_ok());
}
}