use crate::rules::math::parser::MathToken;
use super::math_token_rule::{MathEncodeState, MathTokenEngine, MathTokenResult, MathTokenRule};
const COMBINING_MARK_RANGES: &[(u32, u32)] = &[
(0x0300, 0x036F),
(0x1AB0, 0x1AFF),
(0x1DC0, 0x1DFF),
(0x20D0, 0x20FF),
(0xFE20, 0xFE2F),
];
#[cfg(not(tarpaulin_include))]
fn is_combining_mark_codepoint(c: char) -> bool {
let cp = c as u32;
COMBINING_MARK_RANGES
.iter()
.any(|(lo, hi)| (*lo..=*hi).contains(&cp))
}
#[cfg(not(tarpaulin_include))]
fn is_combining_mark_token(tok: Option<&MathToken>) -> bool {
matches!(tok, Some(MathToken::MathSymbol(c)) if is_combining_mark_codepoint(*c))
}
pub fn encode_decimal_point(
tokens: &[MathToken],
i: usize,
prev_was_number: &mut bool,
result: &mut Vec<u8>,
) {
let prev_baseline_is_number = {
let mut j = i;
while j > 0 && is_combining_mark_token(tokens.get(j - 1)) {
j -= 1;
}
j > 0 && matches!(tokens.get(j - 1), Some(MathToken::Number(_)))
};
if !*prev_was_number && !prev_baseline_is_number {
let next = tokens.get(i + 1);
let has_next_number = matches!(next, Some(MathToken::Number(_)))
|| (matches!(next, Some(MathToken::MathSymbol('\u{0307}')))
&& matches!(tokens.get(i + 2), Some(MathToken::Number(_))));
if has_next_number {
result.push(60);
*prev_was_number = true;
}
} else if prev_baseline_is_number {
*prev_was_number = true;
}
result.push(50);
}
pub struct DecimalPointRule;
impl MathTokenRule for DecimalPointRule {
fn name(&self) -> &'static str {
"DecimalPointRule"
}
fn priority(&self) -> u16 {
50
}
fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
matches!(tokens.get(index), Some(MathToken::DecimalPoint))
}
fn apply(
&self,
tokens: &[MathToken],
index: usize,
result: &mut Vec<u8>,
state: &mut MathEncodeState,
_engine: &MathTokenEngine,
) -> Result<MathTokenResult, String> {
encode_decimal_point(tokens, index, &mut state.prev_was_number, result);
Ok(MathTokenResult::Consumed(1))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn enc(input: &str) -> Vec<u8> {
crate::encode(input).unwrap_or_default()
}
#[test]
fn decimal_point_rule_metadata() {
let r = DecimalPointRule;
assert_eq!(r.priority(), 50);
assert_eq!(r.name(), "DecimalPointRule");
}
#[test]
fn leading_decimal_emits_number_sign() {
let tokens = vec![MathToken::DecimalPoint, MathToken::Number("47".into())];
let mut prev = false;
let mut result = Vec::new();
encode_decimal_point(&tokens, 0, &mut prev, &mut result);
assert_eq!(result.first(), Some(&60));
assert!(prev);
}
#[test]
fn decimal_with_combining_mark_baseline() {
let tokens = vec![
MathToken::Number("2".into()),
MathToken::MathSymbol('\u{0305}'),
MathToken::DecimalPoint,
MathToken::Number("3".into()),
];
let mut prev = false;
let mut result = Vec::new();
encode_decimal_point(&tokens, 2, &mut prev, &mut result);
assert!(prev);
assert_eq!(result, vec![50]);
}
#[test]
fn leading_decimal_with_dot_above_next() {
let tokens = vec![
MathToken::DecimalPoint,
MathToken::MathSymbol('\u{0307}'),
MathToken::Number("9".into()),
];
let mut prev = false;
let mut result = Vec::new();
encode_decimal_point(&tokens, 0, &mut prev, &mut result);
assert!(result.starts_with(&[60]));
}
#[test]
fn decimal_in_full_expression() {
let bytes = enc("$3.14$");
assert!(!bytes.is_empty());
}
}