use std::sync::LazyLock;
use regex::Regex;
use crate::clean::entities::urlencode;
pub const ERROR_SPAN: &str = "<span class=\"error\"></span>";
pub fn ucfirst(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}
pub fn lcfirst(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_lowercase().chain(chars).collect(),
None => String::new(),
}
}
pub fn call_parser_function(name: &str, args: &[String]) -> String {
let arg = |i: usize| args.get(i).map(String::as_str);
match name {
"#invoke" => String::new(),
"#expr" => match args {
[expr] => sharp_expr(expr),
_ => String::new(),
},
"#if" => match args.len() {
0 | 1 => String::new(), _ => sharp_if(&args[0], &args[1], arg(2)),
},
"#ifeq" => match args.len() {
0..=2 => String::new(), _ => sharp_ifeq(&args[0], &args[1], &args[2], arg(3)),
},
"#iferror" => match args.len() {
0 => String::new(),
_ => sharp_iferror(&args[0], arg(1).unwrap_or(""), arg(2)),
},
"#switch" => match args.len() {
0 => String::new(),
_ => sharp_switch(&args[0], &args[1..]),
},
"#ifexpr" | "#ifexist" | "#rel2abs" | "#time" | "#timel" | "#titleparts" => String::new(),
"urlencode" => arg(0).map(urlencode).unwrap_or_default(),
"lc" => arg(0).map(str::to_lowercase).unwrap_or_default(),
"uc" => arg(0).map(str::to_uppercase).unwrap_or_default(),
"lcfirst" => arg(0).map(lcfirst).unwrap_or_default(),
"ucfirst" => arg(0).map(ucfirst).unwrap_or_default(),
"int" => arg(0)
.and_then(|s| s.trim().parse::<i64>().ok())
.map(|n| n.to_string())
.unwrap_or_default(),
"padleft" => String::new(),
_ => String::new(),
}
}
fn sharp_if(test: &str, if_true: &str, if_false: Option<&str>) -> String {
if !test.trim().is_empty() {
let value = if_true.trim();
if !value.is_empty() {
return value.to_string();
}
} else if let Some(if_false) = if_false
&& !if_false.is_empty()
{
return if_false.trim().to_string();
}
String::new()
}
fn sharp_ifeq(lvalue: &str, rvalue: &str, if_true: &str, if_false: Option<&str>) -> String {
let rvalue = rvalue.trim();
if !rvalue.is_empty() {
if lvalue.trim() == rvalue {
if !if_true.is_empty() {
return if_true.trim().to_string();
}
} else if let Some(if_false) = if_false
&& !if_false.is_empty()
{
return if_false.trim().to_string();
}
}
String::new()
}
static ERROR_TAG_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"^<(?:strong|span|p|div)\s(?:[^\s>]*\s+)*?class="(?:[^"\s>]*\s+)*?error(?:\s[^">]*)?""#,
)
.unwrap()
});
fn sharp_iferror(test: &str, then: &str, else_: Option<&str>) -> String {
if ERROR_TAG_RE.is_match(test) {
then.to_string()
} else {
match else_ {
None => test.trim().to_string(),
Some(else_) => else_.trim().to_string(),
}
}
}
fn sharp_switch(primary: &str, params: &[String]) -> String {
let primary = primary.trim();
let mut found = false; let mut default: Option<&str> = None;
for param in params {
match param.split_once('=') {
Some((lvalue, rvalue)) => {
let lvalue = lvalue.trim();
let rvalue = rvalue.trim();
if found || lvalue.split('|').any(|v| v.trim() == primary) {
return rvalue.to_string();
}
if lvalue == "#default" {
default = Some(rvalue);
}
}
None => {
if param.trim() == primary {
found = true;
}
}
}
}
default.unwrap_or("").to_string()
}
fn sharp_expr(expr: &str) -> String {
let prepared = expr.replace('=', "==").replace("mod", "%");
match Parser::evaluate(&prepared) {
Ok(value) => value.python_str(),
Err(()) => ERROR_SPAN.to_string(),
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Value {
Bool(bool),
Int(i64),
Float(f64),
}
impl Value {
fn python_str(self) -> String {
match self {
Value::Bool(b) => if b { "True" } else { "False" }.to_string(),
Value::Int(n) => n.to_string(),
Value::Float(x) => {
if x.is_nan() {
"nan".to_string()
} else if x.is_infinite() {
if x > 0.0 { "inf" } else { "-inf" }.to_string()
} else if x == x.trunc() && x.abs() < 1e16 {
format!("{x:.1}") } else {
format!("{x}")
}
}
}
}
fn truthy(self) -> bool {
match self {
Value::Bool(b) => b,
Value::Int(n) => n != 0,
Value::Float(x) => x != 0.0,
}
}
fn as_f64(self) -> f64 {
match self {
Value::Bool(b) => b as i64 as f64,
Value::Int(n) => n as f64,
Value::Float(x) => x,
}
}
fn as_int(self) -> Option<i64> {
match self {
Value::Bool(b) => Some(b as i64),
Value::Int(n) => Some(n),
Value::Float(_) => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Token {
Number(Value),
Plus,
Minus,
Star,
DoubleStar,
Slash,
DoubleSlash,
Percent,
Eq, Lt,
Gt,
LParen,
RParen,
And,
Or,
Not,
}
struct Parser {
tokens: Vec<Token>,
pos: usize,
}
impl Parser {
fn evaluate(expr: &str) -> Result<Value, ()> {
let mut parser = Parser {
tokens: tokenize(expr)?,
pos: 0,
};
if parser.tokens.is_empty() {
return Err(()); }
let value = parser.or_expr()?;
if parser.pos != parser.tokens.len() {
return Err(()); }
Ok(value)
}
fn peek(&self) -> Option<Token> {
self.tokens.get(self.pos).copied()
}
fn eat(&mut self, token: Token) -> bool {
if self.peek() == Some(token) {
self.pos += 1;
true
} else {
false
}
}
fn or_expr(&mut self) -> Result<Value, ()> {
let mut value = self.and_expr()?;
while self.eat(Token::Or) {
let rhs = self.and_expr()?;
if !value.truthy() {
value = rhs;
}
}
Ok(value)
}
fn and_expr(&mut self) -> Result<Value, ()> {
let mut value = self.not_expr()?;
while self.eat(Token::And) {
let rhs = self.not_expr()?;
if value.truthy() {
value = rhs;
}
}
Ok(value)
}
fn not_expr(&mut self) -> Result<Value, ()> {
if self.eat(Token::Not) {
let value = self.not_expr()?;
Ok(Value::Bool(!value.truthy()))
} else {
self.comparison()
}
}
fn comparison(&mut self) -> Result<Value, ()> {
let lhs = self.arith()?;
let op = match self.peek() {
Some(t @ (Token::Eq | Token::Lt | Token::Gt)) => t,
_ => return Ok(lhs),
};
self.pos += 1;
let rhs = self.arith()?;
if matches!(self.peek(), Some(Token::Eq | Token::Lt | Token::Gt)) {
return Err(());
}
let (a, b) = (lhs.as_f64(), rhs.as_f64());
Ok(Value::Bool(match op {
Token::Eq => a == b,
Token::Lt => a < b,
Token::Gt => a > b,
_ => unreachable!(),
}))
}
fn arith(&mut self) -> Result<Value, ()> {
let mut value = self.term()?;
loop {
if self.eat(Token::Plus) {
value = binary_add(value, self.term()?, false)?;
} else if self.eat(Token::Minus) {
value = binary_add(value, self.term()?, true)?;
} else {
return Ok(value);
}
}
}
fn term(&mut self) -> Result<Value, ()> {
let mut value = self.unary()?;
loop {
let token = match self.peek() {
Some(t @ (Token::Star | Token::Slash | Token::DoubleSlash | Token::Percent)) => t,
_ => return Ok(value),
};
self.pos += 1;
let rhs = self.unary()?;
value = binary_mul(value, rhs, token)?;
}
}
fn unary(&mut self) -> Result<Value, ()> {
if self.eat(Token::Minus) {
let value = self.unary()?;
Ok(match value {
Value::Int(n) => Value::Int(n.checked_neg().ok_or(())?),
Value::Bool(b) => Value::Int(-(b as i64)),
Value::Float(x) => Value::Float(-x),
})
} else if self.eat(Token::Plus) {
self.unary()
} else {
self.power()
}
}
fn power(&mut self) -> Result<Value, ()> {
let base = self.atom()?;
if self.eat(Token::DoubleStar) {
let exponent = self.unary()?;
return binary_pow(base, exponent);
}
Ok(base)
}
fn atom(&mut self) -> Result<Value, ()> {
match self.peek() {
Some(Token::Number(v)) => {
self.pos += 1;
Ok(v)
}
Some(Token::LParen) => {
self.pos += 1;
let value = self.or_expr()?;
if !self.eat(Token::RParen) {
return Err(());
}
Ok(value)
}
_ => Err(()),
}
}
}
fn binary_add(lhs: Value, rhs: Value, subtract: bool) -> Result<Value, ()> {
match (lhs.as_int(), rhs.as_int()) {
(Some(a), Some(b)) => {
let r = if subtract {
a.checked_sub(b)
} else {
a.checked_add(b)
};
r.map(Value::Int).ok_or(())
}
_ => {
let (a, b) = (lhs.as_f64(), rhs.as_f64());
Ok(Value::Float(if subtract { a - b } else { a + b }))
}
}
}
fn binary_mul(lhs: Value, rhs: Value, op: Token) -> Result<Value, ()> {
if let (Some(a), Some(b)) = (lhs.as_int(), rhs.as_int()) {
return match op {
Token::Star => a.checked_mul(b).map(Value::Int).ok_or(()),
Token::Slash => {
if b == 0 {
Err(()) } else {
Ok(Value::Float(a as f64 / b as f64)) }
}
Token::DoubleSlash => {
if b == 0 {
Err(())
} else {
let (q, r) = (a / b, a % b);
Ok(Value::Int(if r != 0 && (r < 0) != (b < 0) {
q - 1
} else {
q
}))
}
}
Token::Percent => {
if b == 0 {
Err(())
} else {
let r = a % b;
Ok(Value::Int(if r != 0 && (r < 0) != (b < 0) {
r + b
} else {
r
}))
}
}
_ => unreachable!(),
};
}
let (a, b) = (lhs.as_f64(), rhs.as_f64());
match op {
Token::Star => Ok(Value::Float(a * b)),
Token::Slash => {
if b == 0.0 {
Err(())
} else {
Ok(Value::Float(a / b))
}
}
Token::DoubleSlash => {
if b == 0.0 {
Err(())
} else {
Ok(Value::Float((a / b).floor()))
}
}
Token::Percent => {
if b == 0.0 {
Err(())
} else {
Ok(Value::Float(a - b * (a / b).floor()))
}
}
_ => unreachable!(),
}
}
fn binary_pow(base: Value, exponent: Value) -> Result<Value, ()> {
if let (Some(b), Some(e)) = (base.as_int(), exponent.as_int()) {
if e >= 0 {
let e = u32::try_from(e).map_err(|_| ())?;
return b.checked_pow(e).map(Value::Int).ok_or(());
}
if b == 0 {
return Err(()); }
return Ok(Value::Float((b as f64).powi(e as i32)));
}
Ok(Value::Float(base.as_f64().powf(exponent.as_f64())))
}
fn tokenize(expr: &str) -> Result<Vec<Token>, ()> {
let bytes = expr.as_bytes();
let mut tokens = Vec::new();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
match b {
b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c => i += 1,
b'(' => {
tokens.push(Token::LParen);
i += 1;
}
b')' => {
tokens.push(Token::RParen);
i += 1;
}
b'+' => {
tokens.push(Token::Plus);
i += 1;
}
b'-' => {
tokens.push(Token::Minus);
i += 1;
}
b'%' => {
tokens.push(Token::Percent);
i += 1;
}
b'*' => {
if bytes.get(i + 1) == Some(&b'*') {
tokens.push(Token::DoubleStar);
i += 2;
} else {
tokens.push(Token::Star);
i += 1;
}
}
b'/' => {
if bytes.get(i + 1) == Some(&b'/') {
tokens.push(Token::DoubleSlash);
i += 2;
} else {
tokens.push(Token::Slash);
i += 1;
}
}
b'=' => {
if bytes.get(i + 1) == Some(&b'=') {
tokens.push(Token::Eq);
i += 2;
} else {
return Err(());
}
}
b'<' => {
tokens.push(Token::Lt);
i += 1;
}
b'>' => {
tokens.push(Token::Gt);
i += 1;
}
b'0'..=b'9' | b'.' => {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let mut is_float = false;
if i < bytes.len() && bytes[i] == b'.' {
is_float = true;
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
}
if i < bytes.len() && (bytes[i] | 0x20) == b'e' {
let mut j = i + 1;
if j < bytes.len() && (bytes[j] == b'+' || bytes[j] == b'-') {
j += 1;
}
if j < bytes.len() && bytes[j].is_ascii_digit() {
is_float = true;
i = j;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
}
}
let literal = &expr[start..i];
if literal == "." {
return Err(());
}
if is_float {
tokens.push(Token::Number(Value::Float(
literal.parse().map_err(|_| ())?,
)));
} else {
if literal.len() > 1 && literal.starts_with('0') {
return Err(());
}
tokens.push(Token::Number(Value::Int(literal.parse().map_err(|_| ())?)));
}
}
b'a'..=b'z' | b'A'..=b'Z' | b'_' => {
let start = i;
while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') {
i += 1;
}
match &expr[start..i] {
"and" => tokens.push(Token::And),
"or" => tokens.push(Token::Or),
"not" => tokens.push(Token::Not),
"True" => tokens.push(Token::Number(Value::Bool(true))),
"False" => tokens.push(Token::Number(Value::Bool(false))),
_ => return Err(()), }
}
_ => return Err(()),
}
}
Ok(tokens)
}
#[cfg(test)]
mod tests {
use super::*;
fn pf(name: &str, args: &[&str]) -> String {
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
call_parser_function(name, &args)
}
#[test]
fn expr_matches_python_eval() {
assert_eq!(pf("#expr", &["2*3+4"]), "10");
assert_eq!(pf("#expr", &["10/4"]), "2.5");
assert_eq!(pf("#expr", &["4/2"]), "2.0"); assert_eq!(pf("#expr", &["7//2"]), "3");
assert_eq!(pf("#expr", &["7 mod 2"]), "1");
assert_eq!(pf("#expr", &["-7 mod 3"]), "2"); assert_eq!(pf("#expr", &["2**10"]), "1024");
assert_eq!(pf("#expr", &["1 = 1"]), "True"); assert_eq!(pf("#expr", &["1 < 2 "]), "True");
assert_eq!(pf("#expr", &["1 and 2"]), "2"); assert_eq!(pf("#expr", &["0 or 5"]), "5");
assert_eq!(pf("#expr", &["not 0"]), "True");
}
#[test]
fn expr_errors_match_python() {
assert_eq!(pf("#expr", &["1 <= 2"]), ERROR_SPAN); assert_eq!(pf("#expr", &["1/0"]), ERROR_SPAN);
assert_eq!(pf("#expr", &["round(2.5)"]), ERROR_SPAN); assert_eq!(pf("#expr", &["4 div 2"]), ERROR_SPAN); assert_eq!(pf("#expr", &[""]), ERROR_SPAN);
assert_eq!(pf("#expr", &["011"]), ERROR_SPAN); assert_eq!(pf("#expr", &["1", "2"]), ""); }
#[test]
fn sharp_if_family() {
assert_eq!(pf("#if", &["x", " yes ", "no"]), "yes");
assert_eq!(pf("#if", &[" ", "yes", " no "]), "no");
assert_eq!(pf("#if", &["", "yes"]), "");
assert_eq!(pf("#if", &["x"]), ""); assert_eq!(pf("#ifeq", &["a", "a", "same", "diff"]), "same");
assert_eq!(pf("#ifeq", &["a", "b", "same", "diff"]), "diff");
assert_eq!(pf("#ifeq", &["a", "", "same", "diff"]), "");
}
#[test]
fn sharp_switch_cases() {
assert_eq!(pf("#switch", &["b", "a=1", "b=2", "#default=9"]), "2");
assert_eq!(pf("#switch", &["z", "a=1", "#default=9"]), "9");
assert_eq!(pf("#switch", &["c", "b", "c", "d=fall", "e=x"]), "fall");
assert_eq!(pf("#switch", &["z", "a=1", "fallback"]), "");
}
#[test]
fn string_functions() {
assert_eq!(pf("lc", &["ABC"]), "abc");
assert_eq!(pf("uc", &["abc"]), "ABC");
assert_eq!(pf("ucfirst", &["hello world"]), "Hello world");
assert_eq!(pf("lcfirst", &["Hello"]), "hello");
assert_eq!(pf("urlencode", &["a b&c"]), "a%20b%26c");
assert_eq!(pf("int", &[" 42 "]), "42");
assert_eq!(pf("int", &["4.5"]), ""); assert_eq!(pf("padleft", &["0", "3", "x"]), ""); assert_eq!(pf("#time", &["Y"]), "");
assert_eq!(pf("#invoke", &["Module", "fn"]), "");
assert_eq!(pf("nosuchfunction", &["x"]), "");
}
}