nodeslicer/infra/prusaslicer_profiles/parser.py
2026-08-26 12:02:01 +03:00

159 lines
5.7 KiB
Python

from __future__ import annotations
import operator
import re
from typing import Any
TOKENS = [
('OP', r'==|!=|<=|>=|=~|!~|&&|\|\||[()\[\]!><]'),
('REGEX', r'/((?:\\/|[^/])*)/'),
('STRING', r'"(?:[^"\\]|\\.)*"|\'(?:[^\'\\]|\\.)*\''),
('NUMBER', r'\d+(?:\.\d+)?'),
('IDENT', r'[a-zA-Z_]\w*'),
]
TOKEN_RE = re.compile('|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKENS))
_OPS = {
'==': (str, operator.eq),
'!=': (str, operator.ne),
'<': (float, operator.lt),
'>': (float, operator.gt),
'<=': (float, operator.le),
'>=': (float, operator.ge),
}
def _tokenize(text: str) -> list[tuple[str, Any]]:
tokens: list[tuple[str, Any]] = []
for m in TOKEN_RE.finditer(text):
kind: str = m.lastgroup if m.lastgroup else ""
val = m.group(kind)
if kind == 'OP':
if val == '&&': tokens.append(('OP', 'and'))
elif val == '||': tokens.append(('OP', 'or'))
elif val in ('(', ')', '[', ']'): tokens.append((val, val))
else: tokens.append(('OP', val))
elif kind == 'REGEX':
tokens.append(('REGEX', re.compile(val[1:-1])))
elif kind == 'STRING':
tokens.append(('LIT', val[1:-1]))
elif kind == 'NUMBER':
tokens.append(('LIT', val))
elif kind == 'IDENT':
if val == 'and': tokens.append(('OP', 'and'))
elif val == 'or': tokens.append(('OP', 'or'))
else: tokens.append(('IDENT', val))
return tokens
class ExprNode:
"""Represents an expression node in the AST."""
def __init__(self, node: tuple[Any, ...]) -> None:
self.node = node
def eval(self, context: dict[str, Any]) -> Any:
return self._eval_node(self.node, context)
@staticmethod
def _eval_node(node: tuple[Any, ...], context: dict[str, Any]) -> Any:
ntype = node[0]
if ntype == 'lit':
return node[1]
if ntype == 'var':
return context.get(node[1], '0')
if ntype == 'index':
val = str(context.get(node[1], '0'))
parts = val.split(',')
idx = node[2]
return parts[idx].strip() if idx < len(parts) else '0'
if ntype == 'unary':
cval = ExprNode._eval_node(node[2], context)
try:
return not float(cval)
except (ValueError, TypeError):
return not bool(cval)
if ntype == 'bin':
op, left, right = node[1], node[2], node[3]
if op == 'and':
lv = ExprNode._eval_node(left, context)
if not lv or lv == '0': return False
return ExprNode._eval_node(right, context)
if op == 'or':
lv = ExprNode._eval_node(left, context)
if lv and lv != '0': return True
return ExprNode._eval_node(right, context)
lv = ExprNode._eval_node(left, context)
rv = ExprNode._eval_node(right, context)
if op in _OPS:
caster, func = _OPS[op]
try:
return func(caster(lv), caster(rv))
except (ValueError, TypeError):
return func(str(lv), str(rv))
if op == '=~':
return rv.search(str(lv)) is not None
if op == '!~':
return rv.search(str(lv)) is None
return '0'
class Parser:
"""Recursive descent parser for PrusaSlicer expressions."""
def __init__(self, text: str) -> None:
self.tokens = _tokenize(text)
self.pos = 0
def parse(self) -> ExprNode:
node = self._parse_or()
if self.pos < len(self.tokens):
raise SyntaxError(f'Trailing tokens: {self.tokens[self.pos:]}')
return ExprNode(node)
def _peek(self) -> tuple[str, Any] | None:
return self.tokens[self.pos] if self.pos < len(self.tokens) else None
def _advance(self) -> tuple[str, Any]:
tok = self.tokens[self.pos]
self.pos += 1
return tok
def _parse_or(self) -> tuple[Any, ...]:
left = self._parse_and()
while (tok := self._peek()) and tok == ('OP', 'or'):
self._advance()
left = ('bin', 'or', left, self._parse_and())
return left
def _parse_and(self) -> tuple[Any, ...]:
left = self._parse_not()
while (tok := self._peek()) and tok == ('OP', 'and'):
self._advance()
left = ('bin', 'and', left, self._parse_not())
return left
def _parse_not(self) -> tuple[Any, ...]:
if (tok := self._peek()) and tok == ('OP', '!'):
self._advance()
return ('unary', '!', self._parse_not())
return self._parse_comp()
def _parse_comp(self) -> tuple[Any, ...]:
left = self._parse_atom()
if (tok := self._peek()) and tok[0] == 'OP' and tok[1] in ('==', '!=', '<', '>', '<=', '>=', '=~', '!~'):
op = self._advance()[1]
left = ('bin', op, left, self._parse_atom())
return left
def _parse_atom(self) -> tuple[Any, ...]:
tok = self._advance()
ttype, val = tok
if ttype in ('LIT', 'REGEX'):
return ('lit', val)
if ttype == 'IDENT':
if self._peek() == ('[', '['):
self._advance()
idx_tok = self._advance()
if self._advance() != (']', ']'): raise SyntaxError('Expected ]')
return ('index', val, int(idx_tok[1]))
return ('var', val)
if ttype == '(':
node = self._parse_or()
if self._advance() != (')', ')'): raise SyntaxError('Expected )')
return node
raise SyntaxError(f'Unexpected token: {tok}')