change: action handler

Signed-off-by: Pakin <pakin.t@forth.co.th>
This commit is contained in:
Pakin 2026-05-25 11:07:47 +07:00
parent 41cc051b71
commit 63b809dcf3
4 changed files with 4838 additions and 12 deletions

View file

@ -34,6 +34,7 @@ Stmt: Statement = {
},
"DEBUGVAR" <name:Ident> => Statement::DebugVar { name },
"Open" <filename:STRING> => Statement::Open { filename },
<name:Ident> "(" <args:Comma<Expr>?> ")" => Statement::Call { name, args: args.unwrap_or_default() },
};
Expr: Expression = {
@ -61,6 +62,7 @@ AndExpr: Expression = {
CmpExpr: Expression = {
AddExpr,
<left:CmpExpr> "=" <right:AddExpr> => Expression::BinaryOp { left: Box::new(left), op: BinaryOpKind::Eq, right: Box::new(right) },
<left:CmpExpr> "!=" <right:AddExpr> => Expression::BinaryOp { left: Box::new(left), op: BinaryOpKind::Neq, right: Box::new(right) },
<left:CmpExpr> "<=" <right:AddExpr> => Expression::BinaryOp { left: Box::new(left), op: BinaryOpKind::Lte, right: Box::new(right) },
<left:CmpExpr> ">=" <right:AddExpr> => Expression::BinaryOp { left: Box::new(left), op: BinaryOpKind::Gte, right: Box::new(right) },
<left:CmpExpr> "<" <right:AddExpr> => Expression::BinaryOp { left: Box::new(left), op: BinaryOpKind::Lt, right: Box::new(right) },
@ -91,9 +93,18 @@ AtomExpr: Expression = {
"True" => Expression::Literal(LiteralValue::Bool(true)),
"False" => Expression::Literal(LiteralValue::Bool(false)),
<name:Ident> "(" <args:Comma<Expr>?> ")" => Expression::FunctionCall { name, args: args.unwrap_or_default() },
"$" <name:Ident> "." <prop:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: format!("{}.{}", name, prop), is_negative: false }) },
"$" <s:STRING> "." <prop:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: format!("{}.{}", s, prop), is_negative: false }) },
"$" "-" <name:Ident> "." <prop:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: format!("{}.{}", name, prop), is_negative: true }) },
"$" "-" <s:STRING> "." <prop:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: format!("{}.{}", s, prop), is_negative: true }) },
"$" <name:Ident> => Expression::SpecialVar { name, is_negative: false },
"$" <s:STRING> => Expression::SpecialVar { name: s, is_negative: false },
"$" "-" <name:Ident> => Expression::SpecialVar { name, is_negative: true },
"$" "-" <s:STRING> => Expression::SpecialVar { name: s, is_negative: true },
"@" <name:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name, is_negative: false }) },
"@" <name:Ident> "." <prop:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: format!("{}.{}", name, prop), is_negative: false }) },
"@" <s:STRING> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: s, is_negative: false }) },
"@" <s:STRING> "." <prop:Ident> => Expression::AutoVarExpr { variable: Box::new(Expression::SpecialVar { name: format!("{}.{}", s, prop), is_negative: false }) },
<name:Ident> => Expression::Identifier(name),
"(" <e:Expr> ")" => e,
};
@ -107,7 +118,7 @@ Comma<T>: Vec<T> = {
};
Stmts: Vec<Statement> = {
<stmts:Stmt+> => stmts,
<stmts:Stmt*> => stmts,
};
STRING: String = {
@ -119,11 +130,15 @@ NUM: f64 = {
};
Ident: String = {
<s:r"[a-zA-Z_][a-zA-Z0-9_]*"> => s.to_string()
<s:r"[a-zA-Z_][a-zA-Z0-9_#]*"> => s.to_string()
};
match {
r"\s+" => {},
r"\s+",
} else {
r#""[^"]*""#,
r"[0-9]+(\.[0-9]+)?",
} else {
"Var",
"If",
"Then",
@ -136,10 +151,13 @@ match {
"False",
"DEBUGVAR",
"Open",
"assigned",
} else {
"<=",
">=",
"&&",
"||",
"!=",
"!",
"$",
"@",
@ -152,10 +170,8 @@ match {
"/",
"<",
">",
".",
"=",
"assigned",
} else {
r#""[^"]*""#,
r"[0-9]+(\.[0-9]+)?",
r"[a-zA-Z_][a-zA-Z0-9_]*",
r"[a-zA-Z_][a-zA-Z0-9_#]*",
}

4295
src/xml/parser/grammar.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,5 @@
pub mod preprocess;
use crate::xml::error::ParserResult;
#[allow(clippy::all)]
@ -5,18 +7,19 @@ pub mod grammar {
include!(concat!(env!("OUT_DIR"), "/xml/parser/grammar.rs"));
}
pub use crate::xml::ast::nodes::{AssignmentType, BinaryOpKind, Expression, LiteralValue, Program, Statement, UnaryOpKind};
use grammar::ProgramParser;
pub use crate::xml::ast::nodes::{AssignmentType, BinaryOpKind, Expression, LiteralValue, Program, Statement, UnaryOpKind};
pub struct ScriptParser;
impl ScriptParser {
pub fn parse(source: &str) -> ParserResult<Program> {
let preprocessed = preprocess::preprocess(source);
let parser = ProgramParser::new();
match parser.parse(source) {
match parser.parse(&preprocessed) {
Ok(program) => Ok(program),
Err(e) => Err(crate::xml::error::ParseError::from_lalrpop_error(source, e)),
Err(e) => Err(crate::xml::error::ParseError::from_lalrpop_error(&preprocessed, e)),
}
}
}
@ -152,11 +155,17 @@ mod tests {
}
#[test]
fn test_comparison() {
fn test_comparison_eq() {
let result = ScriptParser::parse("If x = 5 Then Var y = 10 EndIf");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_comparison_neq() {
let result = ScriptParser::parse("If x != 5 Then Var y = 10 EndIf");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_logical_and() {
let result = ScriptParser::parse("If x = 5 && y = 3 Then Var z = 1 EndIf");
@ -193,6 +202,45 @@ mod tests {
}
}
#[test]
fn test_call_statement() {
let result = ScriptParser::parse("MACHINE_SET_IDLE(3)");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
if let Statement::Call { name, args } = &program.statements[0] {
assert_eq!(name, "MACHINE_SET_IDLE");
assert_eq!(args.len(), 1);
} else {
panic!("Expected Call statement");
}
}
#[test]
fn test_call_with_multiple_args() {
let result = ScriptParser::parse(r#"STRCONTAIN("9501", MaterialAvailable, TaobinOnline)"#);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
if let Statement::Call { name, args } = &program.statements[0] {
assert_eq!(name, "STRCONTAIN");
assert_eq!(args.len(), 3);
} else {
panic!("Expected Call statement");
}
}
#[test]
fn test_call_no_args() {
let result = ScriptParser::parse("RefreshAll()");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
if let Statement::Call { name, args } = &program.statements[0] {
assert_eq!(name, "RefreshAll");
assert!(args.is_empty());
} else {
panic!("Expected Call statement");
}
}
#[test]
fn test_for_loop() {
let result = ScriptParser::parse("For i In items Var x = i EndFor");
@ -212,4 +260,110 @@ mod tests {
let result = ScriptParser::parse("Var x = (1 + 2) * 3");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_preprocessor_action_call() {
let result = ScriptParser::parse("MACHINE_SET_IDLE 3");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
if let Statement::Call { name, args } = &program.statements[0] {
assert_eq!(name, "MACHINE_SET_IDLE");
assert_eq!(args.len(), 1);
} else {
panic!("Expected Call statement, got {:?}", program.statements[0]);
}
}
#[test]
fn test_preprocessor_strip_comment() {
let result = ScriptParser::parse("; this is a comment\nVar x = 5");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
assert_eq!(program.statements.len(), 1);
}
#[test]
fn test_preprocessor_var_not_assigned_with_value() {
let result = ScriptParser::parse("Var x !assigned StringFmt($count, DisplayFormat, PreScaleConvertShow)");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
if let Statement::VarDecl { name, value, assignment_type } = &program.statements[0] {
assert_eq!(name, "x");
assert_eq!(*assignment_type, AssignmentType::Equals);
assert!(value.is_some());
} else {
panic!("Expected VarDecl");
}
}
#[test]
fn test_preprocessor_unknown_action() {
let result = ScriptParser::parse("CacheVarStr(\"get\", XMLProfile)");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
if let Statement::Call { name, args } = &program.statements[0] {
assert_eq!(name, "CacheVarStr");
assert_eq!(args.len(), 2);
} else {
panic!("Expected Call statement");
}
}
#[test]
fn test_dollar_var_with_property() {
let result = ScriptParser::parse("Var x = @\"12-01-01-0003\".Price");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_hash_in_identifier() {
let result = ScriptParser::parse("DEBUGVAR Not#LanguageLoaded");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
fn test_preprocess_hyphenated_dollar_var() {
let preprocessed = preprocess::preprocess("If $12-01-01-0003.Price = -1 Then Var x = 1 EndIf");
assert!(preprocessed.contains("@\"12-01-01-0003\".Price"), "preprocessed: {}", preprocessed);
}
#[test]
fn test_real_script_snippet() {
let script = r#"
MACHINE_SET_IDLE 3
Var CountryName = "Thailand"
If CountryName = "Thailand" Then
Var TaobinPremiumEnable = 1
Else
Var TaobinPremiumEnable = 0
EndIf
DEBUGVAR LangProcess
Var ToggleAfterEventProfileOff = 0
Var credit_card_enable = ""
READ_FILE "/mnt/sdcard/credit_card_enable" credit_card_enable
CacheVarStr "get" XMLProfile
"#;
let result = ScriptParser::parse(script);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
let program = result.unwrap();
assert!(program.statements.len() >= 6, "expected at least 6 statements, got {}", program.statements.len());
}
#[test]
fn test_neq_in_condition() {
let result = ScriptParser::parse("If x != \"\" Then Var y = 1 EndIf");
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
#[test]
#[ignore]
fn test_page_board_event_open() {
let content = std::fs::read_to_string("page_board.xml").unwrap();
let start = content.find("<EventOpen>").unwrap();
let end = content.find("</EventOpen>").unwrap();
let script = &content[start + 12..end].trim();
let result = ScriptParser::parse(script);
assert!(result.is_ok(), "parse failed: {:?}", result.err());
}
}

View file

@ -0,0 +1,361 @@
use std::fmt::Write;
const KEYWORDS: &[&str] = &[
"Var", "If", "Then", "Else", "EndIf", "For", "In", "EndFor",
"True", "False", "DEBUGVAR", "Open", "assigned",
];
fn is_keyword(word: &str) -> bool {
KEYWORDS.contains(&word)
}
fn strip_comments(source: &str) -> String {
let mut result = String::new();
for line in source.lines() {
let stripped = strip_comment_from_line(line);
if !stripped.trim().is_empty() {
result.push_str(stripped.trim());
result.push('\n');
}
}
result
}
fn strip_comment_from_line(line: &str) -> String {
let mut result = String::new();
let mut in_string = false;
let mut chars = line.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '"' {
in_string = !in_string;
result.push(ch);
} else if ch == '\\' && in_string {
result.push(ch);
if let Some(next) = chars.next() {
result.push(next);
}
} else if ch == ';' && !in_string {
break;
} else {
result.push(ch);
}
}
result
}
fn needs_quoting(name: &str) -> bool {
name.contains('-') || name.starts_with(|c: char| c.is_ascii_digit())
}
fn normalize_dollar_vars(text: &str) -> String {
let mut result = String::new();
let chars: Vec<char> = text.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '$' {
i += 1;
let neg = i < chars.len() && chars[i] == '-';
if neg {
i += 1;
}
let name_start = i;
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '-' || chars[i] == '_' || chars[i] == '#') {
i += 1;
}
let name: String = chars[name_start..i].iter().collect();
if i < chars.len() && chars[i] == '.' {
i += 1;
let prop_start = i;
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_' || chars[i] == '#') {
i += 1;
}
let prop: String = chars[prop_start..i].iter().collect();
if neg {
write!(result, "$-\"{}\".{}", name, prop).unwrap();
} else {
write!(result, "@\"{}\".{}", name, prop).unwrap();
}
} else if needs_quoting(&name) {
if neg {
write!(result, "$-\"{}\"", name).unwrap();
} else {
write!(result, "$\"{}\"", name).unwrap();
}
} else if neg {
write!(result, "$-{}", name).unwrap();
} else {
write!(result, "${}", name).unwrap();
}
} else {
result.push(chars[i]);
i += 1;
}
}
result
}
fn is_action_start(word: &str) -> bool {
!is_keyword(word) && !word.is_empty()
}
fn tokenize_action_args(rest: &str) -> Vec<String> {
let mut args = Vec::new();
let mut current = String::new();
let mut in_string = false;
let mut paren_depth = 0;
for ch in rest.chars() {
if in_string {
current.push(ch);
if ch == '"' {
in_string = false;
}
continue;
}
match ch {
'"' => {
current.push(ch);
in_string = true;
}
'(' => {
current.push(ch);
paren_depth += 1;
}
')' => {
current.push(ch);
if paren_depth > 0 {
paren_depth -= 1;
}
if paren_depth == 0 && !current.trim().is_empty() {
args.push(current.trim().to_string());
current.clear();
}
}
' ' | '\t' if paren_depth == 0 => {
if !current.is_empty() {
args.push(current.trim().to_string());
current.clear();
}
}
',' if paren_depth == 0 => {
if !current.is_empty() {
args.push(current.trim().to_string());
current.clear();
}
}
_ => {
current.push(ch);
}
}
}
if !current.trim().is_empty() {
args.push(current.trim().to_string());
}
args
}
fn handle_var_line(line: &str) -> String {
let line = normalize_dollar_vars(line);
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("Var ") {
let rest = rest.trim_start();
if let Some(_rest2) = rest.strip_prefix("!assigned") {
return line.to_string();
}
let name_end = rest.find(|c: char| c.is_whitespace() || c == '=' || c == '!')
.unwrap_or(rest.len());
let name = &rest[..name_end];
let after_name = rest[name_end..].trim_start();
if after_name.starts_with('=') {
return line.to_string();
}
if after_name.starts_with('!') {
if let Some(without_bang) = after_name.strip_prefix("!assigned") {
if without_bang.trim().is_empty() {
return line.to_string();
}
let val = without_bang.trim();
return format!("Var {} = {}", name, val);
}
}
if after_name.starts_with('"') || after_name.chars().next().map_or(false, |c| c.is_ascii_digit()) {
return format!("Var {} = {}", name, after_name);
}
if !name.is_empty() && !after_name.is_empty() {
return format!("Var {} = {}", name, after_name);
}
}
line.to_string()
}
fn normalize_line(line: &str) -> String {
let line = normalize_dollar_vars(line);
let trimmed = line.trim();
if trimmed.is_empty() {
return String::new();
}
let first_word_end = trimmed.find(|c: char| c.is_whitespace() || c == '(' || c == '=')
.unwrap_or(trimmed.len());
let first_word = &trimmed[..first_word_end];
if first_word == "Var" {
return handle_var_line(&line);
}
if first_word == "If" || first_word == "Else" || first_word == "EndIf"
|| first_word == "For" || first_word == "EndFor"
|| first_word == "DEBUGVAR"
|| first_word == "Open"
{
return line.trim().to_string();
}
if is_keyword(first_word) {
return line.trim().to_string();
}
let rest = trimmed[first_word_end..].trim_start();
let rest = rest.trim_end();
if rest.is_empty() {
return format!("{}()", first_word);
}
if rest.starts_with('(') {
return trimmed.to_string();
}
let args = tokenize_action_args(rest);
let args_str = args.join(", ");
format!("{}({})", first_word, args_str)
}
pub fn preprocess(source: &str) -> String {
let no_comments = strip_comments(source);
let mut result = String::new();
for line in no_comments.lines() {
let normalized = normalize_line(line);
if !normalized.is_empty() {
result.push_str(&normalized);
result.push('\n');
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_comments() {
assert_eq!(strip_comments("; this is a comment\n"), "");
assert_eq!(strip_comments("Var x = 5 ; inline\n"), "Var x = 5\n");
assert_eq!(strip_comments(r#"Var x = "hello ; here""#), "Var x = \"hello ; here\"\n");
}
#[test]
fn test_normalize_action_call() {
assert_eq!(normalize_line("MACHINE_SET_IDLE 3"), "MACHINE_SET_IDLE(3)");
assert_eq!(normalize_line("CacheVarStr \"get\" XMLProfile"), "CacheVarStr(\"get\", XMLProfile)");
assert_eq!(normalize_line("StopLongPlay"), "StopLongPlay()");
}
#[test]
fn test_normalize_var_with_equals() {
assert_eq!(normalize_line("Var x = 5"), "Var x = 5");
assert_eq!(normalize_line("Var x = \"hello\""), "Var x = \"hello\"");
}
#[test]
fn test_normalize_var_without_equals() {
assert_eq!(normalize_line("Var return \"\""), "Var return = \"\"");
}
#[test]
fn test_normalize_var_not_assigned() {
assert_eq!(normalize_line("Var x !assigned"), "Var x !assigned");
}
#[test]
fn test_normalize_var_not_assigned_with_value() {
assert_eq!(
normalize_line("Var x !assigned StringFmt($12-01-01-0003.Price, DisplayFormat, PreScaleConvertShow)"),
"Var x = StringFmt(@\"12-01-01-0003\".Price, DisplayFormat, PreScaleConvertShow)"
);
}
#[test]
fn test_normalize_keyword_lines_unchanged() {
assert_eq!(normalize_line("If x = 5 Then"), "If x = 5 Then");
assert_eq!(normalize_line("DEBUGVAR myVar"), "DEBUGVAR myVar");
assert_eq!(normalize_line("Open \"file.xml\""), "Open \"file.xml\"");
}
#[test]
fn test_normalize_dollar_var_property() {
assert_eq!(normalize_dollar_vars("$myVar"), "$myVar");
assert_eq!(normalize_dollar_vars("$-count"), "$-count");
}
#[test]
fn test_preprocess_multiline() {
let input = "; comment\nVar x = 5\nMACHINE_SET_IDLE 3\n";
let result = preprocess(input);
assert!(result.contains("Var x = 5"));
assert!(result.contains("MACHINE_SET_IDLE(3)"));
assert!(!result.contains("comment"));
}
#[test]
fn test_action_with_string_and_ident_args() {
assert_eq!(
normalize_line("STRCONTAIN \"9501\" MaterialAvailable TaobinOnline"),
"STRCONTAIN(\"9501\", MaterialAvailable, TaobinOnline)"
);
}
#[test]
fn test_action_with_number_and_string() {
assert_eq!(
normalize_line("OpenInst 0 \"/mnt/sdcard/file.xml\""),
"OpenInst(0, \"/mnt/sdcard/file.xml\")"
);
}
#[test]
fn test_neq_in_condition() {
let line = "If x != \"\" Then";
assert_eq!(normalize_line(line), "If x != \"\" Then");
}
#[test]
fn test_dollar_var_with_hyphen() {
assert_eq!(normalize_dollar_vars("$12-01-01-0003"), "$\"12-01-01-0003\"");
assert_eq!(normalize_dollar_vars("$12-01-01-0003.Discount"), "@\"12-01-01-0003\".Discount");
}
#[test]
fn test_dollar_var_negative_with_property() {
assert_eq!(normalize_dollar_vars("$-12-01-01-0003.Price"), "$-\"12-01-01-0003\".Price");
}
}