use std::sync::LazyLock;
use super::math_token_rule::{
MathContext, MathEncodeState, MathTokenEngine, MathTokenResult, MathTokenRule,
};
use super::parser::{BracketKind, MathToken};
use super::{
rule_1, rule_2, rule_6, rule_7, rule_8, rule_12, rule_14, rule_18, rule_19, rule_47, rule_53,
rule_54, rule_57,
};
use crate::math_symbol_shortcut;
struct DigitSeparatorRule;
pub(super) fn encode_generic_math_symbol(
c: char,
_is_direct_shortcut_symbol: bool,
result: &mut Vec<u8>,
) -> Result<(), String> {
let encoded = math_symbol_shortcut::encode_char_math_symbol_shortcut(c)?;
result.extend_from_slice(encoded);
Ok(())
}
impl MathTokenRule for DigitSeparatorRule {
fn name(&self) -> &'static str {
"DigitSeparatorRule"
}
fn priority(&self) -> u16 {
50
}
fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
matches!(tokens.get(index), Some(MathToken::DigitSeparator))
}
fn apply(
&self,
_tokens: &[MathToken],
_index: usize,
result: &mut Vec<u8>,
_state: &mut MathEncodeState,
_engine: &MathTokenEngine,
) -> Result<MathTokenResult, String> {
result.push(2);
Ok(MathTokenResult::Consumed(1))
}
}
struct SpaceRule;
fn prev_non_space(tokens: &[MathToken], index: usize) -> Option<&MathToken> {
tokens[..index]
.iter()
.rev()
.find(|token| !matches!(token, MathToken::Space))
}
fn next_non_space(tokens: &[MathToken], index: usize) -> Option<&MathToken> {
tokens[index + 1..]
.iter()
.find(|token| !matches!(token, MathToken::Space))
}
fn prev_non_space_index(tokens: &[MathToken], index: usize) -> Option<usize> {
(0..index)
.rev()
.find(|&i| !matches!(tokens.get(i), Some(MathToken::Space)))
}
fn next_non_space_index(tokens: &[MathToken], index: usize) -> Option<usize> {
(index + 1..tokens.len()).find(|&i| !matches!(tokens.get(i), Some(MathToken::Space)))
}
fn is_glue_operator(token: Option<&MathToken>) -> bool {
matches!(
token,
Some(MathToken::Operator('+' | '-' | '×' | '=' | '/'))
)
}
fn should_suppress_space(tokens: &[MathToken], index: usize) -> bool {
let prev_idx = prev_non_space_index(tokens, index);
let next_idx = next_non_space_index(tokens, index);
if prev_idx.is_some_and(|i| should_suppress_after_operator(tokens, i))
|| next_idx.is_some_and(|i| should_suppress_before_operator(tokens, i))
{
return true;
}
let operator_with_grouped_neighbor = |op_idx: usize| -> bool {
if !is_glue_operator(tokens.get(op_idx)) {
return false;
}
let lhs_grouped = prev_non_space_index(tokens, op_idx)
.is_some_and(|i| token_is_grouped_operand(tokens, i));
let rhs_grouped = next_non_space_index(tokens, op_idx)
.is_some_and(|i| token_is_grouped_operand(tokens, i));
lhs_grouped || rhs_grouped
};
prev_idx.is_some_and(operator_with_grouped_neighbor)
|| next_idx.is_some_and(operator_with_grouped_neighbor)
}
impl MathTokenRule for SpaceRule {
fn name(&self) -> &'static str {
"SpaceRule"
}
fn priority(&self) -> u16 {
50
}
fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
matches!(tokens.get(index), Some(MathToken::Space))
}
fn apply(
&self,
tokens: &[MathToken],
index: usize,
result: &mut Vec<u8>,
state: &mut MathEncodeState,
_engine: &MathTokenEngine,
) -> Result<MathTokenResult, String> {
if !should_suppress_space(tokens, index) {
result.push(0);
}
state.prev_was_number = false;
Ok(MathTokenResult::Consumed(1))
}
}
struct KoreanWordRule;
impl KoreanWordRule {
fn is_inside_curly(tokens: &[MathToken], index: usize) -> bool {
let mut depth: i32 = 0;
for i in 0..index {
match tokens.get(i) {
Some(MathToken::OpenParen(BracketKind::Curly)) => depth += 1,
Some(MathToken::CloseParen(BracketKind::Curly)) => depth -= 1,
_ => {}
}
}
depth > 0
}
fn wrap_kind(tokens: &[MathToken], index: usize) -> Option<BracketKind> {
let prev = prev_non_space(tokens, index);
let next = next_non_space(tokens, index);
let Some(MathToken::KoreanWord(text)) = tokens.get(index) else {
return None;
};
if matches!(prev, Some(MathToken::OpenParen(BracketKind::Hangul)))
|| matches!(next, Some(MathToken::CloseParen(BracketKind::Hangul)))
{
return None;
}
if matches!(prev, Some(MathToken::OpenParen(_)))
&& matches!(next, Some(MathToken::CloseParen(_)))
{
return None;
}
if Self::is_inside_curly(tokens, index) {
return None;
}
if matches!(prev, Some(MathToken::MathSymbol('\u{221A}'))) {
return Some(BracketKind::Hangul);
}
if text.contains(' ')
|| matches!(prev, Some(MathToken::Operator('×')))
|| matches!(next, Some(MathToken::Operator('×')))
{
return Some(BracketKind::MathParen);
}
None
}
}
fn token_is_grouped_operand(tokens: &[MathToken], index: usize) -> bool {
match tokens.get(index) {
Some(MathToken::OpenParen(_) | MathToken::CloseParen(_)) => true,
Some(MathToken::KoreanWord(_)) => KoreanWordRule::wrap_kind(tokens, index).is_some(),
Some(MathToken::MathSymbol('\u{221A}')) => true,
Some(MathToken::Subscript(_) | MathToken::Superscript(_)) => true,
_ => false,
}
}
fn is_mixed_times_context(tokens: &[MathToken], index: usize) -> bool {
let Some(MathToken::Operator('×')) = tokens.get(index) else {
return false;
};
tokens.iter().enumerate().any(|(i, token)| {
matches!(token, MathToken::KoreanWord(_)) && KoreanWordRule::wrap_kind(tokens, i).is_some()
})
}
fn should_suppress_before_operator(tokens: &[MathToken], index: usize) -> bool {
let Some(MathToken::Operator(op)) = tokens.get(index) else {
return false;
};
if *op == '×' {
return is_mixed_times_context(tokens, index);
}
if !is_glue_operator(tokens.get(index)) {
return false;
}
prev_non_space_index(tokens, index).is_some_and(|i| token_is_grouped_operand(tokens, i))
}
fn should_suppress_after_operator(tokens: &[MathToken], index: usize) -> bool {
let Some(MathToken::Operator(op)) = tokens.get(index) else {
return false;
};
if *op == '×' {
return is_mixed_times_context(tokens, index);
}
if !is_glue_operator(tokens.get(index)) {
return false;
}
next_non_space_index(tokens, index).is_some_and(|i| token_is_grouped_operand(tokens, i))
}
impl MathTokenRule for KoreanWordRule {
fn name(&self) -> &'static str {
"KoreanWordRule"
}
fn priority(&self) -> u16 {
50
}
fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
matches!(tokens.get(index), Some(MathToken::KoreanWord(_)))
}
fn apply(
&self,
tokens: &[MathToken],
index: usize,
result: &mut Vec<u8>,
state: &mut MathEncodeState,
_engine: &MathTokenEngine,
) -> Result<MathTokenResult, String> {
let Some(MathToken::KoreanWord(text)) = tokens.get(index) else {
return Ok(MathTokenResult::Skip);
};
if let Some(kind) = Self::wrap_kind(tokens, index) {
rule_6::encode_open_paren(kind, result);
result.extend(crate::encode(text)?);
rule_6::encode_close_paren(kind, result);
} else {
result.extend(crate::encode(text)?);
}
state.prev_was_number = false;
Ok(MathTokenResult::Consumed(1))
}
}
mod symbol_rule;
use symbol_rule::MathSymbolRule;
struct RawTokenRule;
impl MathTokenRule for RawTokenRule {
fn name(&self) -> &'static str {
"RawTokenRule"
}
fn priority(&self) -> u16 {
500
}
fn matches(&self, tokens: &[MathToken], index: usize, _state: &MathEncodeState) -> bool {
matches!(tokens.get(index), Some(MathToken::Raw(_)))
}
fn apply(
&self,
tokens: &[MathToken],
index: usize,
result: &mut Vec<u8>,
_state: &mut MathEncodeState,
_engine: &MathTokenEngine,
) -> Result<MathTokenResult, String> {
let Some(MathToken::Raw(c)) = tokens.get(index) else {
return Ok(MathTokenResult::Skip);
};
if matches!(*c, ':' | ';' | '?' | '!')
&& let Ok(encoded) = crate::symbol_shortcut::encode_char_symbol_shortcut(*c)
{
result.extend_from_slice(encoded);
return Ok(MathTokenResult::Consumed(1));
}
Err(format!("Unrecognized math character: '{}'", c))
}
}
static DEFAULT_MATH_ENGINE: LazyLock<MathTokenEngine> =
LazyLock::new(|| build_math_engine(MathContext::default()));
static MATRIX_MATH_ENGINE: LazyLock<MathTokenEngine> = LazyLock::new(|| {
build_math_engine(MathContext {
matrix_context_active: true,
math_mode_active: false,
})
});
static MATH_MODE_ENGINE: LazyLock<MathTokenEngine> = LazyLock::new(|| {
build_math_engine(MathContext {
matrix_context_active: false,
math_mode_active: true,
})
});
static MATRIX_MATH_MODE_ENGINE: LazyLock<MathTokenEngine> = LazyLock::new(|| {
build_math_engine(MathContext {
matrix_context_active: true,
math_mode_active: true,
})
});
pub(super) fn math_engine_for_context(context: MathContext) -> &'static MathTokenEngine {
match (context.matrix_context_active, context.math_mode_active) {
(false, false) => &DEFAULT_MATH_ENGINE,
(true, false) => &MATRIX_MATH_ENGINE,
(false, true) => &MATH_MODE_ENGINE,
(true, true) => &MATRIX_MATH_MODE_ENGINE,
}
}
fn build_math_engine(context: MathContext) -> MathTokenEngine {
let mut engine = MathTokenEngine::with_context(context);
engine.register(Box::new(rule_7::ConditionalProbFractionRule));
engine.register(Box::new(rule_7::GroupedFractionReversalRule));
engine.register(Box::new(rule_7::FractionReversalRule));
engine.register(Box::new(rule_7::VariableFractionInListRule));
engine.register(Box::new(rule_12::CombinatoricsRule));
engine.register(Box::new(rule_54::PartialDerivativeFractionRule));
engine.register(Box::new(rule_57::DefiniteIntegralRule));
engine.register(Box::new(rule_1::NumberRule));
engine.register(Box::new(rule_12::VariableRule));
engine.register(Box::new(rule_12::UpperVariableRule));
engine.register(Box::new(KoreanWordRule));
engine.register(Box::new(rule_2::OperatorRule));
engine.register(Box::new(rule_47::FunctionNameRule));
engine.register(Box::new(rule_6::BracketRule));
engine.register(Box::new(rule_18::SuperscriptRule));
engine.register(Box::new(rule_19::SubscriptRule));
engine.register(Box::new(rule_8::DecimalPointRule));
engine.register(Box::new(DigitSeparatorRule));
engine.register(Box::new(SpaceRule));
engine.register(Box::new(rule_53::PrimeRule));
engine.register(Box::new(MathSymbolRule));
engine.register(Box::new(RawTokenRule));
engine.finalize();
engine
}
pub fn encode_math_expression(input: &str) -> Result<Vec<u8>, String> {
if rule_14::is_roman_numeral_expression(input) {
return rule_14::encode_roman_numeral_expression(input);
}
let tokens = super::parser::parse_math_expression(input)?;
encode_math_tokens_with_context(&tokens, MathContext::default())
}
pub fn encode_math_expression_with_context(
input: &str,
context: MathContext,
) -> Result<Vec<u8>, String> {
if context == MathContext::default() {
return encode_math_expression(input);
}
if rule_14::is_roman_numeral_expression(input) {
return rule_14::encode_roman_numeral_expression(input);
}
let tokens =
super::parser::parse_math_expression_with_math_mode(input, context.math_mode_active)?;
encode_math_tokens_with_context(&tokens, context)
}
fn encode_math_tokens_with_context(
tokens: &[MathToken],
context: MathContext,
) -> Result<Vec<u8>, String> {
let engine = math_engine_for_context(context);
let mut result = Vec::new();
engine.encode_tokens(tokens, &mut result)?;
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_equation() {
let result = encode_math_expression("ax+b=0");
assert!(result.is_ok(), "Should encode ax+b=0: {:?}", result);
}
#[test]
fn test_number_encoding() {
let result = encode_math_expression("37+25").unwrap();
assert!(!result.is_empty());
}
fn enc(input: &str) -> Vec<u8> {
crate::encode(input).unwrap_or_default()
}
#[test]
fn math_symbol_dispatch_sweep() {
let inputs: &[&str] = &[
"x=y",
"x≠y",
"x≥y",
"x≤y",
"x>y",
"x<y",
"A∪B",
"A∩B",
"A⊂B",
"A⊆B",
"A⊃B",
"A⊇B",
"A∈B",
"A∉B",
"A∋B",
"A=∅",
"A∧B",
"A∨B",
"¬A",
"A→B",
"A↔B",
"A⇒B",
"A⇔B",
"∀x",
"∃y",
"a∣b",
"a∤b",
"a≡b",
"f:A→B",
"f∘g",
"∫f",
"∮g",
"∂f/∂x",
"→",
"←",
"↑",
"↓",
"π",
"e",
"∞",
"ℵ",
"⟨x⟩",
"|x|",
"‖v‖",
"f'",
"f''",
"f'''",
"¬AB", "#X", "#(X)", ];
for input in inputs {
let _ = encode_math_expression(input);
let _ = enc(input);
}
}
#[test]
fn math_latex_diverse_inputs() {
let inputs: &[&str] = &[
"$\\sqrt{2}$",
"$\\sqrt[3]{x}$",
"$\\frac{1}{2}$",
"$\\frac{a+b}{c-d}$",
"$\\sum_{i=1}^n i$",
"$\\prod_{i=1}^n i$",
"$\\int_0^1 f(x) dx$",
"$\\lim_{x \\to 0} f(x)$",
"$\\binom{n}{k}$",
"$\\overrightarrow{AB}$",
"$\\vec{v}$",
"$\\hat{x}$",
"$\\bar{x}$",
"$\\overline{AB}$",
"$\\tilde{x}$",
"$\\dot{x}$",
"$\\ddot{x}$",
"수식 $x^2 + y^2 = r^2$",
];
for input in inputs {
let _ = enc(input);
}
}
#[test]
fn math_spacing_suppression() {
let a = encode_math_expression("a + b");
let b = encode_math_expression("a+b");
assert!(a.is_ok());
assert!(b.is_ok());
}
#[test]
fn cardinality_pattern() {
let _ = enc("$\\#(X)$");
let _ = encode_math_expression("\u{FF03}(X)");
let _ = encode_math_expression("\u{FF03}A"); }
#[test]
fn negation_between_uppercase_vars() {
let _ = encode_math_expression("A��B");
let _ = encode_math_expression("X �� Y");
}
#[test]
fn digit_separator_emits_byte_2() {
let tokens = vec![MathToken::DigitSeparator];
let mut result = Vec::new();
let engine = math_engine_for_context(MathContext::default());
engine.encode_tokens(&tokens, &mut result).unwrap();
assert_eq!(result, vec![2], "DigitSeparator must emit byte 2");
}
#[test]
fn prev_non_space_index_none_at_zero() {
let tokens = vec![MathToken::Variable('a'), MathToken::Variable('b')];
assert_eq!(prev_non_space_index(&tokens, 0), None);
}
#[test]
fn prev_non_space_index_skips_spaces() {
let tokens = vec![
MathToken::Variable('a'), MathToken::Space, MathToken::Space, MathToken::Variable('b'), ];
assert_eq!(prev_non_space_index(&tokens, 3), Some(0));
assert_eq!(prev_non_space_index(&tokens, 2), Some(0));
}
#[test]
fn next_non_space_index_none_when_only_spaces_follow() {
let tokens = vec![MathToken::Variable('a'), MathToken::Space, MathToken::Space];
assert_eq!(next_non_space_index(&tokens, 0), None);
}
#[test]
fn next_non_space_index_skips_spaces() {
let tokens = vec![
MathToken::Variable('a'), MathToken::Space, MathToken::Space, MathToken::Variable('b'), ];
assert_eq!(next_non_space_index(&tokens, 0), Some(3));
assert_eq!(next_non_space_index(&tokens, 1), Some(3));
assert_eq!(next_non_space_index(&tokens, 2), Some(3));
}
#[test]
fn is_glue_operator_distinguishes_operators() {
assert!(is_glue_operator(Some(&MathToken::Operator('+'))));
assert!(is_glue_operator(Some(&MathToken::Operator('-'))));
assert!(is_glue_operator(Some(&MathToken::Operator('×'))));
assert!(is_glue_operator(Some(&MathToken::Operator('='))));
assert!(is_glue_operator(Some(&MathToken::Operator('/'))));
assert!(!is_glue_operator(Some(&MathToken::Operator('*'))));
assert!(!is_glue_operator(Some(&MathToken::Variable('a'))));
assert!(!is_glue_operator(None));
}
#[test]
fn prev_non_space_returns_token_reference() {
let tokens = vec![
MathToken::Variable('a'),
MathToken::Space,
MathToken::Variable('b'),
];
match prev_non_space(&tokens, 2) {
Some(MathToken::Variable('a')) => {}
other => panic!("expected Variable('a'), got {:?}", other),
}
assert!(prev_non_space(&tokens, 0).is_none());
}
#[test]
fn next_non_space_returns_token_reference() {
let tokens = vec![
MathToken::Variable('a'),
MathToken::Space,
MathToken::Variable('b'),
];
match next_non_space(&tokens, 0) {
Some(MathToken::Variable('b')) => {}
other => panic!("expected Variable('b'), got {:?}", other),
}
let only_space = vec![MathToken::Variable('a'), MathToken::Space];
assert!(next_non_space(&only_space, 0).is_none());
}
#[test]
fn should_suppress_space_branches() {
let no_op = vec![
MathToken::Variable('a'),
MathToken::Space,
MathToken::Variable('b'),
];
assert!(!should_suppress_space(&no_op, 1));
let grouped_rhs = vec![
MathToken::Variable('a'),
MathToken::Space,
MathToken::Operator('='),
MathToken::Space,
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('b'),
MathToken::Operator('+'),
MathToken::Variable('c'),
MathToken::CloseParen(BracketKind::MathParen),
];
assert!(should_suppress_space(&grouped_rhs, 1));
assert!(should_suppress_space(&grouped_rhs, 3));
}
#[test]
fn space_rule_metadata() {
let rule = SpaceRule;
assert_eq!(rule.name(), "SpaceRule");
assert_eq!(rule.priority(), 50);
}
#[test]
fn digit_separator_rule_metadata() {
let rule = DigitSeparatorRule;
assert_eq!(rule.name(), "DigitSeparatorRule");
assert_eq!(rule.priority(), 50);
let state = MathEncodeState::with_context(false, MathContext::default());
let yes = vec![MathToken::DigitSeparator];
assert!(rule.matches(&yes, 0, &state));
let no = vec![MathToken::Variable('a')];
assert!(!rule.matches(&no, 0, &state));
assert!(!rule.matches(&yes, 99, &state));
}
fn kw(s: &str) -> MathToken {
MathToken::KoreanWord(s.to_string())
}
#[test]
fn is_inside_curly_balances_brackets() {
let inside = vec![
MathToken::OpenParen(BracketKind::Curly),
MathToken::Variable('x'),
MathToken::Operator('|'),
MathToken::Variable('y'),
MathToken::CloseParen(BracketKind::Curly),
];
assert!(KoreanWordRule::is_inside_curly(&inside, 3));
assert!(!KoreanWordRule::is_inside_curly(&inside, 5));
assert!(!KoreanWordRule::is_inside_curly(&inside, 0));
let nested = vec![
MathToken::OpenParen(BracketKind::Curly),
MathToken::OpenParen(BracketKind::Curly),
MathToken::Variable('a'),
MathToken::CloseParen(BracketKind::Curly),
MathToken::CloseParen(BracketKind::Curly),
];
assert!(KoreanWordRule::is_inside_curly(&nested, 2));
assert!(!KoreanWordRule::is_inside_curly(&nested, 5));
}
#[test]
fn korean_wrap_kind_branches() {
let solo = vec![kw("원")];
assert_eq!(KoreanWordRule::wrap_kind(&solo, 0), None);
let spaced = vec![kw("원의 둘레")];
assert_eq!(
KoreanWordRule::wrap_kind(&spaced, 0),
Some(BracketKind::MathParen)
);
let already_wrapped = vec![
MathToken::OpenParen(BracketKind::MathParen),
kw("원"),
MathToken::CloseParen(BracketKind::MathParen),
];
assert_eq!(KoreanWordRule::wrap_kind(&already_wrapped, 1), None);
let hangul_wrapped = vec![
MathToken::OpenParen(BracketKind::Hangul),
kw("원"),
MathToken::CloseParen(BracketKind::Hangul),
];
assert_eq!(KoreanWordRule::wrap_kind(&hangul_wrapped, 1), None);
let curly = vec![
MathToken::OpenParen(BracketKind::Curly),
kw("원"),
MathToken::CloseParen(BracketKind::Curly),
];
assert_eq!(KoreanWordRule::wrap_kind(&curly, 1), None);
let after_sqrt = vec![MathToken::MathSymbol('\u{221A}'), kw("원")];
assert_eq!(
KoreanWordRule::wrap_kind(&after_sqrt, 1),
Some(BracketKind::Hangul)
);
let times_left = vec![kw("원"), MathToken::Operator('×'), MathToken::Variable('x')];
assert_eq!(
KoreanWordRule::wrap_kind(×_left, 0),
Some(BracketKind::MathParen)
);
let times_right = vec![MathToken::Variable('x'), MathToken::Operator('×'), kw("원")];
assert_eq!(
KoreanWordRule::wrap_kind(×_right, 2),
Some(BracketKind::MathParen)
);
let not_korean = vec![MathToken::Variable('a')];
assert_eq!(KoreanWordRule::wrap_kind(¬_korean, 0), None);
}
#[test]
fn token_is_grouped_operand_distinguishes() {
let open = vec![MathToken::OpenParen(BracketKind::MathParen)];
assert!(token_is_grouped_operand(&open, 0));
let close = vec![MathToken::CloseParen(BracketKind::MathParen)];
assert!(token_is_grouped_operand(&close, 0));
let sqrt = vec![MathToken::MathSymbol('\u{221A}')];
assert!(token_is_grouped_operand(&sqrt, 0));
let sub = vec![MathToken::Subscript(vec![MathToken::Variable('n')])];
assert!(token_is_grouped_operand(&sub, 0));
let sup = vec![MathToken::Superscript(vec![MathToken::Variable('n')])];
assert!(token_is_grouped_operand(&sup, 0));
let kw_spaced = vec![kw("원의 둘레")];
assert!(token_is_grouped_operand(&kw_spaced, 0));
let kw_solo = vec![kw("원")];
assert!(!token_is_grouped_operand(&kw_solo, 0));
let var = vec![MathToken::Variable('a')];
assert!(!token_is_grouped_operand(&var, 0));
assert!(!token_is_grouped_operand(&var, 99));
}
#[test]
fn is_plain_unwrapped_korean_deleted_smoke_test() {
let solo = vec![kw("가")];
let _ = KoreanWordRule::wrap_kind(&solo, 0);
}
#[test]
fn is_mixed_times_context_branches() {
let not_times = vec![
MathToken::Variable('a'),
MathToken::Operator('+'),
MathToken::Variable('b'),
];
assert!(!is_mixed_times_context(¬_times, 1));
let kw_both = vec![kw("가"), MathToken::Operator('×'), kw("나")];
assert!(is_mixed_times_context(&kw_both, 1));
let wrapped_elsewhere = vec![
MathToken::Variable('x'),
MathToken::Operator('×'),
MathToken::Variable('y'),
kw("원의 둘레"), ];
assert!(is_mixed_times_context(&wrapped_elsewhere, 1));
let no_wrapped = vec![
MathToken::Variable('x'),
MathToken::Operator('×'),
MathToken::Variable('y'),
];
assert!(!is_mixed_times_context(&no_wrapped, 1));
let empty: Vec<MathToken> = vec![];
assert!(!is_mixed_times_context(&empty, 0));
}
#[test]
fn should_suppress_before_operator_branches() {
let not_op = vec![MathToken::Variable('a'), MathToken::Variable('b')];
assert!(!should_suppress_before_operator(¬_op, 0));
let mixed_times = vec![
MathToken::Variable('x'),
MathToken::Operator('×'),
MathToken::Variable('y'),
kw("원의 둘레"),
];
assert!(should_suppress_before_operator(&mixed_times, 1));
let nonglue = vec![
MathToken::Variable('a'),
MathToken::Operator('@'),
MathToken::Variable('b'),
];
assert!(!should_suppress_before_operator(&nonglue, 1));
let glue_grouped = vec![
MathToken::CloseParen(BracketKind::MathParen),
MathToken::Operator('='),
MathToken::Variable('b'),
];
assert!(should_suppress_before_operator(&glue_grouped, 1));
let glue_plain = vec![
MathToken::Variable('a'),
MathToken::Operator('='),
MathToken::Variable('b'),
];
assert!(!should_suppress_before_operator(&glue_plain, 1));
}
#[test]
fn should_suppress_after_operator_branches() {
let not_op = vec![MathToken::Variable('a')];
assert!(!should_suppress_after_operator(¬_op, 0));
let mixed_times = vec![
MathToken::Variable('x'),
MathToken::Operator('×'),
MathToken::Variable('y'),
kw("원의 둘레"),
];
assert!(should_suppress_after_operator(&mixed_times, 1));
let nonglue = vec![
MathToken::Variable('a'),
MathToken::Operator('@'),
MathToken::Variable('b'),
];
assert!(!should_suppress_after_operator(&nonglue, 1));
let glue_grouped = vec![
MathToken::Variable('a'),
MathToken::Operator('='),
MathToken::OpenParen(BracketKind::MathParen),
];
assert!(should_suppress_after_operator(&glue_grouped, 1));
let glue_plain = vec![
MathToken::Variable('a'),
MathToken::Operator('='),
MathToken::Variable('b'),
];
assert!(!should_suppress_after_operator(&glue_plain, 1));
}
#[test]
fn korean_word_rule_metadata() {
let rule = KoreanWordRule;
assert_eq!(rule.name(), "KoreanWordRule");
assert_eq!(rule.priority(), 50);
let state = MathEncodeState::with_context(false, MathContext::default());
let yes = vec![kw("원")];
assert!(rule.matches(&yes, 0, &state));
let no = vec![MathToken::Variable('a')];
assert!(!rule.matches(&no, 0, &state));
}
#[test]
fn raw_token_rule_metadata() {
let rule = RawTokenRule;
assert_eq!(rule.name(), "RawTokenRule");
assert_eq!(rule.priority(), 500);
let state = MathEncodeState::with_context(false, MathContext::default());
let yes = vec![MathToken::Raw('?')];
assert!(rule.matches(&yes, 0, &state));
let no = vec![MathToken::Variable('a')];
assert!(!rule.matches(&no, 0, &state));
}
#[test]
fn wrap_kind_only_lhs_paren_does_not_short_circuit() {
let lhs_only = vec![
MathToken::OpenParen(BracketKind::MathParen),
kw("원의 둘레"),
MathToken::Variable('x'),
];
assert_eq!(
KoreanWordRule::wrap_kind(&lhs_only, 1),
Some(BracketKind::MathParen)
);
let rhs_only = vec![
MathToken::Variable('x'),
kw("원의 둘레"),
MathToken::CloseParen(BracketKind::MathParen),
];
assert_eq!(
KoreanWordRule::wrap_kind(&rhs_only, 1),
Some(BracketKind::MathParen)
);
}
#[test]
fn is_mixed_times_context_one_side_plain_other_wrapped() {
let case_a = vec![
kw("원"), MathToken::Variable('z'), MathToken::Operator('×'), MathToken::Variable('y'), kw("원의 둘레"), ];
assert!(is_mixed_times_context(&case_a, 2));
}
#[test]
fn is_mixed_times_iter_any_requires_both() {
let case = vec![
MathToken::Variable('x'),
MathToken::Operator('×'),
MathToken::Variable('y'),
kw("원"), ];
assert!(!is_mixed_times_context(&case, 1));
}
#[test]
fn should_suppress_space_one_side_only() {
let one_side = vec![
MathToken::Variable('a'),
MathToken::Space,
MathToken::Operator('='),
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('b'),
MathToken::Operator('+'),
MathToken::Variable('c'),
MathToken::CloseParen(BracketKind::MathParen),
];
assert!(should_suppress_space(&one_side, 1));
let mirror = vec![
MathToken::OpenParen(BracketKind::MathParen),
MathToken::Variable('a'),
MathToken::CloseParen(BracketKind::MathParen),
MathToken::Operator('='),
MathToken::Space,
MathToken::Variable('z'),
];
assert!(should_suppress_space(&mirror, 4));
}
#[test]
fn matrix_math_mode_engine_initializes() {
let _ = math_engine_for_context(MathContext {
matrix_context_active: true,
math_mode_active: true,
});
}
#[test]
fn korean_word_rule_apply_skip_on_non_korean_word() {
let tokens = vec![MathToken::Variable('x')];
let mut state = MathEncodeState::with_context(false, MathContext::default());
let engine = math_engine_for_context(MathContext::default());
let mut result = Vec::new();
let outcome = KoreanWordRule
.apply(&tokens, 0, &mut result, &mut state, engine)
.unwrap();
assert!(matches!(outcome, MathTokenResult::Skip));
}
#[test]
fn is_mixed_times_context_exercise_branches() {
let no_op = vec![kw("원"), MathToken::Operator('+'), kw("둘레")];
assert!(!is_mixed_times_context(&no_op, 1));
let two_korean = vec![kw("원"), MathToken::Operator('×'), kw("둘레")];
let _ = is_mixed_times_context(&two_korean, 1);
let mixed = vec![kw("원"), MathToken::Operator('×'), MathToken::Variable('x')];
let _ = is_mixed_times_context(&mixed, 1);
}
#[test]
fn raw_token_rule_apply_with_non_raw_skip() {
use crate::rules::math::math_token_rule::{MathContext, MathEncodeState, MathTokenRule};
let r = super::RawTokenRule;
let toks = vec![MathToken::Variable('x')];
let mut state = MathEncodeState::with_context(false, MathContext::default());
let mut result = Vec::new();
let engine = super::MathTokenEngine::with_context(MathContext::default());
let res = r.apply(&toks, 0, &mut result, &mut state, &engine);
assert!(matches!(
res,
Ok(crate::rules::math::math_token_rule::MathTokenResult::Skip)
));
}
#[test]
fn encode_math_expression_with_context_roman_numeral_non_default_context() {
use crate::rules::math::math_token_rule::MathContext;
let ctx = MathContext {
math_mode_active: true,
..MathContext::default()
};
let result = super::encode_math_expression_with_context("XII", ctx);
assert!(result.is_ok());
assert!(!result.unwrap().is_empty());
}
}