"""Parse Naninovel expressions into data, never Python/JavaScript source code.""" import re import math TOKEN = re.compile(r'''\s*(?:(\d+(?:\.\d*)?(?:[eE][+-]?\d+)?|\.\d+)|([A-Za-z_]\w*)|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')|(&&|\|\||==|!=|<>|<=|>=|<<|>>|[()+\-*/%<>=!~?:,&|^]))''') PRECEDENCE = {"or": 2, "||": 2, "and": 3, "&&": 3, "|": 4, "^": 5, "&": 6, "==": 7, "=": 7, "!=": 7, "<>": 7, "<": 8, ">": 8, "<=": 8, ">=": 8, "<<": 9, ">>": 9, "+": 10, "-": 10, "*": 11, "/": 11, "%": 11} FUNCTIONS = {"Random", "random", "Rand", "GetRandValue", "Min", "Max", "Abs", "Round", "Pow"} ARITY = {"Random": (1, 2), "random": (1, 2), "Rand": (1, 2), "GetRandValue": (1, 1), "Min": (1, 100), "Max": (1, 100), "Abs": (1, 1), "Round": (1, 2), "Pow": (2, 2)} def parse(source: str, functions: set[str] | None = None) -> list: source = source.strip().removesuffix(";").strip() tokens, offset = [], 0 while offset < len(source): if source[offset].isspace(): offset += 1 continue wrapped = source[offset] == "\\" and offset + 1 < len(source) and source[offset + 1] in "\"'" if source[offset] in "\"'" or wrapped: quote = source[offset + 1] if wrapped else source[offset] offset += 2 if wrapped else 1 value, closed = [], False while offset < len(source): char = source[offset] if wrapped and char == "\\" and offset + 1 < len(source) and source[offset + 1] == quote: offset += 2 closed = True break if not wrapped and char == quote: offset += 1 closed = True break if char == "\\" and offset + 1 < len(source): offset += 1 char = {"n": "\n", "r": "\r", "t": "\t"}.get(source[offset], source[offset]) value.append(char) offset += 1 if not closed: raise ValueError("Не закрыта строка в выражении Naninovel") tokens.append((2, "".join(value))) continue match = TOKEN.match(source, offset) if not match: raise ValueError(f"Не поддерживается выражение Naninovel: {source[offset:offset + 60]}") tokens.append((next(i for i, v in enumerate(match.groups()) if v is not None), next(v for v in match.groups() if v is not None))) offset = match.end() position = 0 allowed = FUNCTIONS if functions is None else functions def peek(): return ("\x00string" if tokens[position][0] == 2 else tokens[position][1]) if position < len(tokens) else "" def take(expected=None): nonlocal position if position >= len(tokens) or (expected is not None and peek() != expected): raise ValueError(f"Ожидалось {expected or 'выражение'}: {source}") token = tokens[position] position += 1 return token def expression(minimum=1): kind, token = take() if kind != 2 and token in {"-", "+", "!", "not", "~"}: left = ["unary", token, expression(12)] elif kind == 3 and token == "(": left = expression() take(")") elif kind == 0: value = float(token) if any(c in token for c in ".eE") else int(token) if not math.isfinite(value): raise ValueError("Неконечная числовая константа Naninovel") left = ["const", value] elif kind == 2: left = ["const", token] elif kind == 1: if token.lower() in {"true", "false", "null"}: left = ["const", {"true": True, "false": False, "null": None}[token.lower()]] elif peek() == "(": if token not in allowed and token.lower() != "if": raise ValueError(f"Не поддерживается функция Naninovel: {token}") take("(") args = [] if peek() != ")": while True: args.append(expression()) if peek() != ",": break take(",") take(")") if functions is None and token in ARITY and not ARITY[token][0] <= len(args) <= ARITY[token][1]: raise ValueError(f"Неподдерживаемое число аргументов Naninovel: {token}") if token.lower() == "if": if len(args) != 3: raise ValueError("If Naninovel требует три аргумента") left = ["if", *args] else: left = ["call", token, args] else: left = ["name", token] else: raise ValueError(f"Неожиданный токен {token}: {source}") while peek() in PRECEDENCE and PRECEDENCE[peek()] >= minimum: op = take()[1] left = ["bin", op, left, expression(PRECEDENCE[op] + 1)] if minimum <= 1 and peek() == "?": take("?") yes = expression() take(":") left = ["if", left, yes, expression()] return left ast = expression() if position != len(tokens): raise ValueError(f"Лишние токены в выражении Naninovel: {source}") return ast def expr(source: str, functions: set[str] | None = None) -> dict: return {"k": "nani", "ast": parse(source, functions)} def assignment(source: str, functions: set[str] | None = None) -> dict: match = re.fullmatch(r"\s*([A-Za-z_]\w*)\s*([+\-*/%&|^]?)=(?!=)(.*?)\s*;?\s*", source, re.S) if not match: raise ValueError(f"Не поддерживается присваивание Naninovel: {source}") ast = parse(match[3], functions) if match[2]: ast = ["bin", match[2], ["name", match[1]], ast] return {"op": "set", "t": {"k": "name", "id": match[1]}, "v": {"k": "nani", "ast": ast}}