use std::{cell::Cell, sync::LazyLock};
use fend_core::SpanKind;
use crate::engines::EngineResponse;
use super::regex;
pub fn request(query: &str) -> EngineResponse {
let query = clean_query(query.to_string());
let Some(result_html) = evaluate(&query, true) else {
return EngineResponse::new();
};
EngineResponse::answer_html(format!(
r#"
{query} =
{result_html}
"#,
query = html_escape::encode_text(&query),
))
}
pub fn request_autocomplete(query: &str) -> Vec {
let mut results = Vec::new();
let query = clean_query(query.to_string());
if let Some(result) = evaluate(&query, false) {
results.push(format!("= {result}"));
}
results
}
fn clean_query(query: String) -> String {
query.strip_suffix('=').unwrap_or(&query).trim().to_string()
}
#[derive(Debug)]
pub struct Span {
pub text: String,
pub kind: SpanKind,
}
fn evaluate(query: &str, html: bool) -> Option {
let spans = evaluate_into_spans(query, html);
if spans.is_empty() {
return None;
}
if !html {
return Some(
spans
.iter()
.map(|span| span.text.clone())
.collect::>()
.join(""),
);
}
let mut result_html = String::new();
for span in &spans {
let class = match span.kind {
fend_core::SpanKind::Number
| fend_core::SpanKind::Boolean
| fend_core::SpanKind::Date => "answer-calc-constant",
fend_core::SpanKind::String => "answer-calc-string",
_ => "",
};
if !class.is_empty() {
result_html.push_str(&format!(
r#"{text}"#,
text = html_escape::encode_text(&span.text)
));
} else {
result_html.push_str(&html_escape::encode_text(&span.text));
}
}
// if the result was a single hex number then we add the decimal equivalent
// below
if spans.len() == 1
&& spans[0].kind == fend_core::SpanKind::Number
&& spans[0].text.starts_with("0x")
{
let hex = spans[0].text.trim_start_matches("0x");
if let Ok(num) = u64::from_str_radix(hex, 16) {
result_html.push_str(&format!(
r#" "#,
num = num
));
}
}
Some(result_html)
}
pub static FEND_CONTEXT: LazyLock = LazyLock::new(|| {
let mut context = fend_core::Context::new();
// make lowercase f and c work
context.define_custom_unit_v1("f", "f", "°F", &fend_core::CustomUnitAttribute::Alias);
context.define_custom_unit_v1("c", "c", "°C", &fend_core::CustomUnitAttribute::Alias);
// make random work
context.set_random_u32_fn(rand::random::);
fend_core::evaluate("ord=(x: x to codepoint)", &mut context).unwrap();
context
});
struct Interrupter {
invocations_left: Cell,
}
impl fend_core::Interrupt for Interrupter {
fn should_interrupt(&self) -> bool {
if self.invocations_left.get() == 0 {
return true;
}
self.invocations_left.set(self.invocations_left.get() - 1);
false
}
}
fn evaluate_into_spans(query: &str, multiline: bool) -> Vec {
// match queries like "chr(8831)" or "8831 to char"
let re = regex!(
r"^(?:(?:chr|charcode|char|charcode)(?:| for| of)\s*\(?\s*(\d+)\s*\)?)|(?:(\d+) (?:|to |into |as )(?:charcode|char|character))$"
);
if let Some(m) = re.captures(query) {
if let Some(ord) = m
.get(1)
.or_else(|| m.get(2))
.and_then(|m| m.as_str().parse::().ok())
{
let chr = std::char::from_u32(ord);
if let Some(chr) = chr {
return vec![Span {
text: format!("'{chr}'"),
kind: fend_core::SpanKind::String,
}];
} else {
return vec![];
}
}
}
// fend incorrectly triggers on these often
{
// at least 3 characters and not one of the short constants
if query.len() < 3 && !matches!(query.to_lowercase().as_str(), "pi" | "e" | "c") {
return vec![];
}
// probably a query operator thing or a url, fend evaluates these but it
// shouldn't
if regex!("^[a-z]{2,}:").is_match(query) {
return vec![];
}
// if it starts and ends with quotes then the person was just searching in
// quotes and didn't mean to evaluate a string
if query.starts_with('"')
&& query.ends_with('"')
&& query.chars().filter(|c| *c == '"').count() == 2
{
return vec![];
}
}
let mut context = FEND_CONTEXT.clone();
if multiline {
// this makes it generate slightly nicer outputs for some queries like 2d6
context.set_output_mode_terminal();
}
// avoids stackoverflows and queries that take too long
// examples:
// - Y = (\f. (\x. f x x)) (\x. f x x); Y(Y)
// - 10**100000000
let interrupt = Interrupter {
invocations_left: Cell::new(1000),
};
let Ok(result) = fend_core::evaluate_with_interrupt(query, &mut context, &interrupt) else {
return vec![];
};
let main_result = result.get_main_result();
if main_result == query {
return vec![];
}
result
.get_main_result_spans()
.filter(|span| !span.string().is_empty())
.map(|span| Span {
text: span.string().to_string(),
kind: span.kind(),
})
.collect()
}