use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use js_sys::{Array, Object, Reflect};
use crate::{
context::KatexContext,
core,
types::{OutputFormat, Settings, StrictMode, StrictSetting, TrustSetting},
};
use crate::macro_expander::MacroMap;
use crate::macros::MacroDefinition;
use std::sync::OnceLock;
static KATEX_CONTEXT: OnceLock<KatexContext> = OnceLock::new();
fn get_context() -> &'static KatexContext {
KATEX_CONTEXT.get_or_init(KatexContext::default)
}
#[wasm_bindgen]
pub fn render(
tex: &str,
element: &web_sys::Node,
options: Option<Settings>,
) -> Result<(), JsValue> {
let ctx = get_context();
let mut settings = options.unwrap_or_default();
settings.output = OutputFormat::HtmlAndMathml;
core::render(ctx, tex, element, &settings).map_err(|e| JsValue::from_str(&format!("{e}")))
}
#[wasm_bindgen]
pub fn render_to_string(tex: &str, options: Option<Settings>) -> Result<String, JsValue> {
let ctx = get_context();
let mut settings = options.unwrap_or_default();
settings.output = OutputFormat::HtmlAndMathml;
core::render_to_string(ctx, tex, &settings).map_err(|e| JsValue::from_str(&format!("{e}")))
}
fn parse_js_options(js: &JsValue) -> Result<Settings, JsValue> {
if js.is_undefined() || js.is_null() {
return Ok(Settings::default());
}
if !js.is_object() {
return Err(JsValue::from_str("options must be a plain object"));
}
let obj: Object = Object::from(js.clone());
let get = |key: &str| -> Result<JsValue, JsValue> {
Reflect::get(&obj, &JsValue::from_str(key))
.map_err(|_| JsValue::from_str(&format!("failed to read option '{}'", key)))
};
let opt_bool = |key: &str| -> Result<Option<bool>, JsValue> {
let v = get(key)?;
if v.is_undefined() || v.is_null() {
Ok(None)
} else {
v.as_bool()
.map(Some)
.ok_or_else(|| JsValue::from_str(&format!("option '{}' must be a boolean", key)))
}
};
let opt_string = |key: &str| -> Result<Option<String>, JsValue> {
let v = get(key)?;
if v.is_undefined() || v.is_null() {
Ok(None)
} else {
v.as_string()
.map(Some)
.ok_or_else(|| JsValue::from_str(&format!("option '{}' must be a string", key)))
}
};
let opt_f64 = |key: &str| -> Result<Option<f64>, JsValue> {
let v = get(key)?;
if v.is_undefined() || v.is_null() {
Ok(None)
} else {
v.as_f64()
.map(Some)
.ok_or_else(|| JsValue::from_str(&format!("option '{}' must be a number", key)))
}
};
let mut settings = Settings::default();
if let Some(dm) = opt_bool("displayMode")? {
settings.display_mode = dm;
} else if let Some(d) = opt_bool("display")? {
settings.display_mode = d;
}
match (opt_bool("throwOnError")?, opt_bool("noThrow")?) {
(Some(toe), _) => {
settings.throw_on_error = toe;
}
(None, Some(no_throw)) => {
settings.throw_on_error = !no_throw;
}
(None, None) => {
}
}
if let Some(color) = opt_string("errorColor")? {
settings.error_color = color;
}
let macros_val = get("macros")?;
if !macros_val.is_undefined() && !macros_val.is_null() {
if Array::is_array(¯os_val) {
return Err(JsValue::from_str("option 'macros' must be a plain object, not an array"));
}
if !macros_val.is_object() {
return Err(JsValue::from_str("option 'macros' must be an object"));
}
let macros_obj: Object = Object::from(macros_val);
let keys = Object::keys(¯os_obj);
let mut m: MacroMap = MacroMap::default();
for key in keys.iter() {
let k = key
.as_string()
.ok_or_else(|| JsValue::from_str("macros keys must be strings"))?;
let val = Reflect::get(¯os_obj, &JsValue::from_str(&k))
.map_err(|_| JsValue::from_str(&format!("failed to read macros['{}']", k)))?;
let s = val
.as_string()
.ok_or_else(|| JsValue::from_str(&format!("macros['{}'] must be a string", k)))?;
m.insert(k, MacroDefinition::String(s));
}
{
let mut target = settings.macros.borrow_mut();
*target = m;
}
}
let strict_val = get("strict")?;
if !strict_val.is_undefined() && !strict_val.is_null() {
if let Some(b) = strict_val.as_bool() {
settings.strict = StrictSetting::Bool(b);
} else if let Some(s) = strict_val.as_string() {
match s.to_lowercase().as_str() {
"ignore" => settings.strict = StrictSetting::Mode(StrictMode::Ignore),
"warn" => settings.strict = StrictSetting::Mode(StrictMode::Warn),
"error" => settings.strict = StrictSetting::Mode(StrictMode::Error),
other => {
return Err(JsValue::from_str(&format!(
"option 'strict' string not recognized: '{}'; expected 'ignore' | 'warn' | 'error'",
other
)));
}
}
} else {
return Err(JsValue::from_str(
"option 'strict' must be a boolean or one of: 'ignore' | 'warn' | 'error'",
));
}
}
let trust_val = get("trust")?;
if !trust_val.is_undefined() && !trust_val.is_null() {
if let Some(b) = trust_val.as_bool() {
settings.trust = TrustSetting::Bool(b);
} else {
return Err(JsValue::from_str(
"option 'trust' currently supports only boolean",
));
}
}
if let Some(b) = opt_bool("leqno")? {
settings.leqno = b;
}
if let Some(b) = opt_bool("fleqn")? {
settings.fleqn = b;
}
if let Some(b) = opt_bool("colorIsTextColor")? {
settings.color_is_text_color = b;
}
if let Some(b) = opt_bool("globalGroup")? {
settings.global_group = b;
}
if let Some(n) = opt_f64("minRuleThickness")? {
if !n.is_finite() || n.is_sign_negative() {
return Err(JsValue::from_str(
"option 'minRuleThickness' must be a non-negative finite number",
));
}
settings.min_rule_thickness = n;
}
if let Some(n) = opt_f64("sizeMultiplier")? {
if !n.is_finite() || n.is_sign_negative() {
return Err(JsValue::from_str(
"option 'sizeMultiplier' must be a non-negative finite number",
));
}
settings.size_multiplier = n;
}
if let Some(n) = opt_f64("maxExpand")? {
if !n.is_finite() {
return Err(JsValue::from_str(
"option 'maxExpand' must be a finite non-negative integer",
));
}
if n < 0.0 {
return Err(JsValue::from_str(
"option 'maxExpand' must be non-negative",
));
}
if (n.fract()).abs() > 0.0 {
return Err(JsValue::from_str(
"option 'maxExpand' must be an integer",
));
}
settings.max_expand = n as usize;
}
if let Some(c) = opt_string("color")? {
settings.color = Some(c);
}
Ok(settings)
}
#[wasm_bindgen]
pub fn render_with_options(
tex: &str,
element: web_sys::Element,
js_options: JsValue,
) -> Result<(), JsValue> {
let ctx = get_context();
let mut settings = parse_js_options(&js_options)?;
settings.output = OutputFormat::HtmlAndMathml;
let node: web_sys::Node = element.unchecked_into();
core::render(ctx, tex, &node, &settings)
.map_err(|e| JsValue::from_str(&format!("{e}")))
}
#[wasm_bindgen]
pub fn render_to_string_with_options(tex: &str, js_options: JsValue) -> Result<String, JsValue> {
let ctx = get_context();
let mut settings = parse_js_options(&js_options)?;
let obj = Object::from(js_options.clone());
let out_v = Reflect::get(&obj, &JsValue::from_str("output"))
.map_err(|_| JsValue::from_str("failed to read option 'output'"))?;
let fmt = if let Some(s) = out_v.as_string() {
match s.to_lowercase().as_str() {
"html" => OutputFormat::Html,
"mathml" => OutputFormat::Mathml,
_ => OutputFormat::HtmlAndMathml,
}
} else {
OutputFormat::HtmlAndMathml
};
settings.output = fmt;
core::render_to_string(ctx, tex, &settings)
.map_err(|e| JsValue::from_str(&format!("{e}")))
}
#[wasm_bindgen]
pub fn render_to_mathml(tex: &str, options: Option<Settings>) -> Result<String, JsValue> {
let ctx = get_context();
let mut settings = options.unwrap_or_default();
settings.output = OutputFormat::Mathml;
core::render_to_string(ctx, tex, &settings).map_err(|e| JsValue::from_str(&format!("{e}")))
}
#[wasm_bindgen]
pub fn render_to_html(tex: &str, options: Option<Settings>) -> Result<String, JsValue> {
let ctx = get_context();
let mut settings = options.unwrap_or_default();
settings.output = OutputFormat::Html;
core::render_to_string(ctx, tex, &settings).map_err(|e| JsValue::from_str(&format!("{e}")))
}