mapres 3.dev20__tar.gz → 3.dev22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mapres-3.dev20/src/mapres.egg-info → mapres-3.dev22}/PKG-INFO +1 -1
- {mapres-3.dev20 → mapres-3.dev22}/pyproject.toml +1 -1
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/datamap.py +5 -11
- mapres-3.dev22/src/mapres/evaluator.py +123 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/parser.py +21 -34
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/tokenizer.py +99 -100
- {mapres-3.dev20 → mapres-3.dev22/src/mapres.egg-info}/PKG-INFO +1 -1
- mapres-3.dev20/src/mapres/evaluator.py +0 -221
- {mapres-3.dev20 → mapres-3.dev22}/LICENSE +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/README.md +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/setup.cfg +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/__init__.py +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/ast.py +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/cache.py +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/exceptions.py +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/layers.py +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/maps/__init__.py +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/maps/color.py +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/maps/time.py +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres/resolver.py +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres.egg-info/SOURCES.txt +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres.egg-info/dependency_links.txt +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres.egg-info/requires.txt +0 -0
- {mapres-3.dev20 → mapres-3.dev22}/src/mapres.egg-info/top_level.txt +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import re
|
|
2
1
|
from dataclasses import dataclass, fields, is_dataclass
|
|
3
2
|
|
|
4
3
|
# syntax identifiers
|
|
@@ -30,15 +29,13 @@ class DataMap:
|
|
|
30
29
|
|
|
31
30
|
Mode metadata (__mode__) controls behavior:
|
|
32
31
|
- None: plain static map
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
32
|
+
- 'dynamic': values may be provided by callables/providers
|
|
33
|
+
- 'config': config-style maps (no dynamic evaluation)
|
|
34
|
+
- 'callable': map exposes callable methods for nested calls
|
|
36
35
|
'''
|
|
37
36
|
__syntax__: str = None
|
|
38
|
-
__mode__: str | None = None
|
|
39
|
-
__recursive__: bool = False
|
|
37
|
+
__mode__: str | None = None # 'dynamic', 'config', 'callable', or None
|
|
40
38
|
__missing_key__: str = 'error' # 'error', 'silent', 'placeholder'
|
|
41
|
-
__ignore_delimiters__: bool = False
|
|
42
39
|
|
|
43
40
|
def as_map(self):
|
|
44
41
|
'''
|
|
@@ -95,14 +92,11 @@ class DataMap:
|
|
|
95
92
|
return out
|
|
96
93
|
|
|
97
94
|
|
|
98
|
-
def datamap(_cls=None, *, syntax=None, mode=None,
|
|
99
|
-
|
|
95
|
+
def datamap(_cls=None, *, syntax=None, mode=None, missing_key=None):
|
|
100
96
|
def wrap(cls):
|
|
101
97
|
cls.__syntax__ = syntax
|
|
102
98
|
cls.__mode__ = mode
|
|
103
|
-
cls.__recursive__ = recursive
|
|
104
99
|
cls.__missing_key__ = missing_key or 'error'
|
|
105
|
-
cls.__ignore_delimiters__ = ignore_delimiters or False
|
|
106
100
|
|
|
107
101
|
namespace = dict(cls.__dict__)
|
|
108
102
|
namespace['__dict__'] = {}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from .ast import TemplateNode, TextNode, IdentNode, CallNode
|
|
2
|
+
from .tokenizer import Tokenizer
|
|
3
|
+
from .parser import Parser
|
|
4
|
+
from .exceptions import MapResError, MissingKeyError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EvaluationError(MapResError):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Evaluator:
|
|
12
|
+
def __init__(self, layerstack, ctx=None):
|
|
13
|
+
self.layerstack = layerstack
|
|
14
|
+
self.ctx = ctx or {}
|
|
15
|
+
self._seen = set() # cycle detection
|
|
16
|
+
|
|
17
|
+
# public api
|
|
18
|
+
def evaluate(self, node):
|
|
19
|
+
return self._eval(node)
|
|
20
|
+
|
|
21
|
+
# internal dispatch
|
|
22
|
+
def _eval(self, node):
|
|
23
|
+
if isinstance(node, TemplateNode):
|
|
24
|
+
return self._eval_template(node)
|
|
25
|
+
|
|
26
|
+
if isinstance(node, TextNode):
|
|
27
|
+
return node.text
|
|
28
|
+
|
|
29
|
+
if isinstance(node, IdentNode):
|
|
30
|
+
return self._eval_ident(node)
|
|
31
|
+
|
|
32
|
+
if isinstance(node, CallNode):
|
|
33
|
+
return self._eval_call(node)
|
|
34
|
+
|
|
35
|
+
raise EvaluationError(f'Unknown AST node: {node}')
|
|
36
|
+
|
|
37
|
+
# template
|
|
38
|
+
def _eval_template(self, node: TemplateNode):
|
|
39
|
+
parts = []
|
|
40
|
+
for child in node.children:
|
|
41
|
+
parts.append(self._eval(child))
|
|
42
|
+
out = ''.join(parts)
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
# identifier
|
|
46
|
+
def _eval_ident(self, node: IdentNode):
|
|
47
|
+
name = node.name
|
|
48
|
+
|
|
49
|
+
# cycle detection
|
|
50
|
+
if name in self._seen:
|
|
51
|
+
raise EvaluationError(f"Cycle detected in identifier '{name}'")
|
|
52
|
+
self._seen.add(name)
|
|
53
|
+
|
|
54
|
+
parts = name.split('.')
|
|
55
|
+
|
|
56
|
+
# 1. ctx first
|
|
57
|
+
val = self._lookup_hierarchical(self.ctx, parts)
|
|
58
|
+
if val is not None:
|
|
59
|
+
self._seen.remove(name)
|
|
60
|
+
return str(val)
|
|
61
|
+
|
|
62
|
+
# 2. syntax-bound maps
|
|
63
|
+
for m in self.layerstack.all_maps():
|
|
64
|
+
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
d = self._map_to_dict(m)
|
|
68
|
+
val = self._lookup_hierarchical(d, parts)
|
|
69
|
+
if val is not None:
|
|
70
|
+
self._seen.remove(name)
|
|
71
|
+
return str(val)
|
|
72
|
+
|
|
73
|
+
self._seen.remove(name)
|
|
74
|
+
raise MissingKeyError(name)
|
|
75
|
+
|
|
76
|
+
# call (nested token)
|
|
77
|
+
def _eval_call(self, node: CallNode):
|
|
78
|
+
outer = node.name
|
|
79
|
+
|
|
80
|
+
if outer in self._seen:
|
|
81
|
+
raise EvaluationError(f"Cycle detected in call '{outer}'")
|
|
82
|
+
self._seen.add(outer)
|
|
83
|
+
|
|
84
|
+
arg_value = self._eval(node.arg)
|
|
85
|
+
parts = [outer, arg_value]
|
|
86
|
+
|
|
87
|
+
# ctx first
|
|
88
|
+
val = self._lookup_hierarchical(self.ctx, parts)
|
|
89
|
+
if val is not None:
|
|
90
|
+
self._seen.remove(outer)
|
|
91
|
+
return str(val)
|
|
92
|
+
|
|
93
|
+
# syntax-bound maps
|
|
94
|
+
for m in self.layerstack.all_maps():
|
|
95
|
+
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
d = self._map_to_dict(m)
|
|
99
|
+
val = self._lookup_hierarchical(d, parts)
|
|
100
|
+
if val is not None:
|
|
101
|
+
self._seen.remove(outer)
|
|
102
|
+
return str(val)
|
|
103
|
+
|
|
104
|
+
self._seen.remove(outer)
|
|
105
|
+
raise MissingKeyError(f'{outer}({arg_value})')
|
|
106
|
+
|
|
107
|
+
# helpers
|
|
108
|
+
def _map_to_dict(self, m):
|
|
109
|
+
if hasattr(m, 'as_map'):
|
|
110
|
+
return m.as_map()
|
|
111
|
+
if isinstance(m, dict):
|
|
112
|
+
return m
|
|
113
|
+
raise EvaluationError(f'Invalid map object: {m}')
|
|
114
|
+
|
|
115
|
+
def _lookup_hierarchical(self, root, parts):
|
|
116
|
+
cur = root
|
|
117
|
+
for p in parts:
|
|
118
|
+
if not isinstance(cur, dict):
|
|
119
|
+
return None
|
|
120
|
+
if p not in cur:
|
|
121
|
+
return None
|
|
122
|
+
cur = cur[p]
|
|
123
|
+
return cur
|
|
@@ -22,11 +22,10 @@ class Parser:
|
|
|
22
22
|
def expect(self, type_: TokenType) -> Token:
|
|
23
23
|
tok = self.peek()
|
|
24
24
|
if tok is None or tok.type != type_:
|
|
25
|
-
raise ParserError(f
|
|
25
|
+
raise ParserError(f'Expected {type_}, got {tok}')
|
|
26
26
|
self.advance()
|
|
27
27
|
return tok
|
|
28
28
|
|
|
29
|
-
# top-level template parse
|
|
30
29
|
def parse(self) -> TemplateNode:
|
|
31
30
|
children: list[Node] = []
|
|
32
31
|
while self.peek() is not None:
|
|
@@ -38,88 +37,76 @@ class Parser:
|
|
|
38
37
|
children.append(self.parse_token())
|
|
39
38
|
return TemplateNode(children)
|
|
40
39
|
|
|
41
|
-
# nested template parse (for IDENT(...))
|
|
42
40
|
def parse_until(self, stop_types: tuple[TokenType, ...]) -> TemplateNode:
|
|
43
41
|
children: list[Node] = []
|
|
44
42
|
while True:
|
|
45
43
|
tok = self.peek()
|
|
46
44
|
if tok is None or tok.type in stop_types:
|
|
47
45
|
break
|
|
48
|
-
|
|
49
46
|
if tok.type is TokenType.TEXT:
|
|
50
47
|
children.append(TextNode(tok.value))
|
|
51
48
|
self.advance()
|
|
52
49
|
else:
|
|
53
50
|
children.append(self.parse_token())
|
|
54
|
-
|
|
55
51
|
return TemplateNode(children)
|
|
56
52
|
|
|
57
|
-
# dispatch by opening delimiter
|
|
58
53
|
def parse_token(self) -> Node:
|
|
59
54
|
tok = self.peek()
|
|
60
55
|
|
|
61
56
|
if tok.type is TokenType.COLON_OPEN:
|
|
62
|
-
return self.
|
|
57
|
+
return self.parse_colon()
|
|
63
58
|
|
|
64
59
|
if tok.type is TokenType.BRACE_OPEN:
|
|
65
|
-
return self.
|
|
60
|
+
return self.parse_brace()
|
|
66
61
|
|
|
67
62
|
if tok.type is TokenType.DOLLAR_OPEN:
|
|
68
|
-
return self.
|
|
63
|
+
return self.parse_dollar()
|
|
69
64
|
|
|
70
65
|
if tok.type is TokenType.ANGLE_OPEN:
|
|
71
|
-
return self.
|
|
66
|
+
return self.parse_angle()
|
|
72
67
|
|
|
73
68
|
if tok.type is TokenType.PIPE_OPEN:
|
|
74
|
-
return self.
|
|
69
|
+
return self.parse_pipe()
|
|
75
70
|
|
|
76
71
|
if tok.type is TokenType.PERCENT_OPEN:
|
|
77
|
-
return self.
|
|
72
|
+
return self.parse_percent()
|
|
78
73
|
|
|
79
|
-
raise ParserError(f
|
|
74
|
+
raise ParserError(f'Unexpected token {tok}')
|
|
80
75
|
|
|
81
|
-
# core pattern: IDENT or IDENT(arg)
|
|
82
76
|
def _parse_ident_or_call(self, closing_type: TokenType, syntax_name: str) -> Node:
|
|
83
77
|
ident_tok = self.expect(TokenType.IDENT)
|
|
84
78
|
name = ident_tok.value
|
|
85
79
|
|
|
86
|
-
# IDENT(arg)
|
|
87
80
|
if self.peek() and self.peek().type is TokenType.LPAREN:
|
|
88
|
-
self.advance()
|
|
89
|
-
|
|
90
|
-
# parse only until RPAREN
|
|
81
|
+
self.advance()
|
|
91
82
|
arg = self.parse_until((TokenType.RPAREN,))
|
|
92
|
-
|
|
93
83
|
self.expect(TokenType.RPAREN)
|
|
94
84
|
self.expect(closing_type)
|
|
95
85
|
return CallNode(name=name, arg=arg, syntax=syntax_name)
|
|
96
86
|
|
|
97
|
-
# IDENT
|
|
98
87
|
self.expect(closing_type)
|
|
99
88
|
return IdentNode(name=name, syntax=syntax_name)
|
|
100
89
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def parse_colon_token(self) -> Node:
|
|
90
|
+
def parse_colon(self) -> Node:
|
|
104
91
|
self.expect(TokenType.COLON_OPEN)
|
|
105
|
-
return self._parse_ident_or_call(TokenType.COLON_CLOSE,
|
|
92
|
+
return self._parse_ident_or_call(TokenType.COLON_CLOSE, 'colons')
|
|
106
93
|
|
|
107
|
-
def
|
|
94
|
+
def parse_brace(self) -> Node:
|
|
108
95
|
self.expect(TokenType.BRACE_OPEN)
|
|
109
|
-
return self._parse_ident_or_call(TokenType.BRACE_CLOSE,
|
|
96
|
+
return self._parse_ident_or_call(TokenType.BRACE_CLOSE, 'braces')
|
|
110
97
|
|
|
111
|
-
def
|
|
98
|
+
def parse_dollar(self) -> Node:
|
|
112
99
|
self.expect(TokenType.DOLLAR_OPEN)
|
|
113
|
-
return self._parse_ident_or_call(TokenType.DOLLAR_CLOSE,
|
|
100
|
+
return self._parse_ident_or_call(TokenType.DOLLAR_CLOSE, 'dollars')
|
|
114
101
|
|
|
115
|
-
def
|
|
102
|
+
def parse_angle(self) -> Node:
|
|
116
103
|
self.expect(TokenType.ANGLE_OPEN)
|
|
117
|
-
return self._parse_ident_or_call(TokenType.ANGLE_CLOSE,
|
|
104
|
+
return self._parse_ident_or_call(TokenType.ANGLE_CLOSE, 'angles')
|
|
118
105
|
|
|
119
|
-
def
|
|
106
|
+
def parse_pipe(self) -> Node:
|
|
120
107
|
self.expect(TokenType.PIPE_OPEN)
|
|
121
|
-
return self._parse_ident_or_call(TokenType.PIPE_CLOSE,
|
|
108
|
+
return self._parse_ident_or_call(TokenType.PIPE_CLOSE, 'pipes')
|
|
122
109
|
|
|
123
|
-
def
|
|
110
|
+
def parse_percent(self) -> Node:
|
|
124
111
|
self.expect(TokenType.PERCENT_OPEN)
|
|
125
|
-
return self._parse_ident_or_call(TokenType.PERCENT_CLOSE,
|
|
112
|
+
return self._parse_ident_or_call(TokenType.PERCENT_CLOSE, 'percents')
|
|
@@ -33,37 +33,29 @@ class Token:
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class Tokenizer:
|
|
36
|
-
def __init__(self, text):
|
|
36
|
+
def __init__(self, text: str):
|
|
37
37
|
self.text = text
|
|
38
38
|
self.i = 0
|
|
39
39
|
self.n = len(text)
|
|
40
|
-
self.tokens = []
|
|
40
|
+
self.tokens: list[Token] = []
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
self.in_pipe = False
|
|
44
|
-
self.in_percent = False
|
|
45
|
-
self.in_colon = False
|
|
46
|
-
|
|
47
|
-
# mixed states
|
|
48
|
-
self.in_dollar = False
|
|
49
|
-
|
|
50
|
-
def peek(self, k=0):
|
|
42
|
+
def peek(self, k: int = 0) -> str:
|
|
51
43
|
idx = self.i + k
|
|
52
44
|
if idx >= self.n:
|
|
53
45
|
return ''
|
|
54
46
|
return self.text[idx]
|
|
55
47
|
|
|
56
|
-
def advance(self, k=1):
|
|
48
|
+
def advance(self, k: int = 1) -> None:
|
|
57
49
|
self.i += k
|
|
58
50
|
|
|
59
|
-
def match(self, s):
|
|
51
|
+
def match(self, s: str) -> bool:
|
|
60
52
|
return self.text.startswith(s, self.i)
|
|
61
53
|
|
|
62
|
-
def emit(self, type_, value=None):
|
|
54
|
+
def emit(self, type_, value=None) -> None:
|
|
63
55
|
self.tokens.append(Token(type_, value, self.i))
|
|
64
56
|
|
|
65
|
-
def tokenize(self):
|
|
66
|
-
buf = []
|
|
57
|
+
def tokenize(self) -> list[Token]:
|
|
58
|
+
buf: list[str] = []
|
|
67
59
|
|
|
68
60
|
def flush_text():
|
|
69
61
|
if buf:
|
|
@@ -95,93 +87,109 @@ class Tokenizer:
|
|
|
95
87
|
if self.match(r'\)'):
|
|
96
88
|
buf.append(')'); self.advance(2); continue
|
|
97
89
|
|
|
98
|
-
# ---
|
|
99
|
-
|
|
100
|
-
# braces {{ }}
|
|
90
|
+
# --- {{IDENT}} ---
|
|
101
91
|
if self.match('{{'):
|
|
102
92
|
flush_text()
|
|
103
|
-
self.
|
|
104
|
-
self.advance(2)
|
|
105
|
-
continue
|
|
106
|
-
|
|
107
|
-
if self.match('}}'):
|
|
108
|
-
flush_text()
|
|
109
|
-
self.emit(TokenType.BRACE_CLOSE)
|
|
93
|
+
start = self.i
|
|
110
94
|
self.advance(2)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if self.match('>'):
|
|
121
|
-
flush_text()
|
|
122
|
-
self.emit(TokenType.ANGLE_CLOSE)
|
|
95
|
+
ident = self._read_ident()
|
|
96
|
+
if ident is not None and self.match('}}'):
|
|
97
|
+
self.emit(TokenType.BRACE_OPEN)
|
|
98
|
+
self.emit(TokenType.IDENT, ident)
|
|
99
|
+
self.advance(2)
|
|
100
|
+
self.emit(TokenType.BRACE_CLOSE)
|
|
101
|
+
continue
|
|
102
|
+
self.i = start
|
|
103
|
+
buf.append(self.peek())
|
|
123
104
|
self.advance()
|
|
124
105
|
continue
|
|
125
106
|
|
|
126
|
-
# ---
|
|
127
|
-
|
|
107
|
+
# --- ${IDENT} ---
|
|
128
108
|
if self.match('${'):
|
|
129
109
|
flush_text()
|
|
130
|
-
self.
|
|
131
|
-
self.in_dollar = True
|
|
110
|
+
start = self.i
|
|
132
111
|
self.advance(2)
|
|
112
|
+
ident = self._read_ident()
|
|
113
|
+
if ident is not None and self.peek() == '}':
|
|
114
|
+
self.emit(TokenType.DOLLAR_OPEN)
|
|
115
|
+
self.emit(TokenType.IDENT, ident)
|
|
116
|
+
self.advance()
|
|
117
|
+
self.emit(TokenType.DOLLAR_CLOSE)
|
|
118
|
+
continue
|
|
119
|
+
self.i = start
|
|
120
|
+
buf.append(self.peek())
|
|
121
|
+
self.advance()
|
|
133
122
|
continue
|
|
134
123
|
|
|
135
|
-
|
|
124
|
+
# --- <IDENT> ---
|
|
125
|
+
if ch == '<':
|
|
136
126
|
flush_text()
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
127
|
+
start = self.i
|
|
128
|
+
self.advance()
|
|
129
|
+
ident = self._read_ident()
|
|
130
|
+
if ident is not None and self.peek() == '>':
|
|
131
|
+
self.emit(TokenType.ANGLE_OPEN)
|
|
132
|
+
self.emit(TokenType.IDENT, ident)
|
|
133
|
+
self.advance()
|
|
134
|
+
self.emit(TokenType.ANGLE_CLOSE)
|
|
135
|
+
continue
|
|
136
|
+
self.i = start
|
|
137
|
+
buf.append(self.peek())
|
|
142
138
|
self.advance()
|
|
143
139
|
continue
|
|
144
140
|
|
|
145
|
-
# ---
|
|
146
|
-
|
|
147
|
-
# pipes |...|
|
|
148
|
-
if self.match('|'):
|
|
141
|
+
# --- |IDENT| ---
|
|
142
|
+
if ch == '|':
|
|
149
143
|
flush_text()
|
|
150
|
-
|
|
144
|
+
start = self.i
|
|
145
|
+
self.advance()
|
|
146
|
+
ident = self._read_ident()
|
|
147
|
+
if ident is not None and self.peek() == '|':
|
|
151
148
|
self.emit(TokenType.PIPE_OPEN)
|
|
152
|
-
self.
|
|
153
|
-
|
|
149
|
+
self.emit(TokenType.IDENT, ident)
|
|
150
|
+
self.advance()
|
|
154
151
|
self.emit(TokenType.PIPE_CLOSE)
|
|
155
|
-
|
|
152
|
+
continue
|
|
153
|
+
self.i = start
|
|
154
|
+
buf.append(self.peek())
|
|
156
155
|
self.advance()
|
|
157
156
|
continue
|
|
158
157
|
|
|
159
|
-
#
|
|
160
|
-
if
|
|
158
|
+
# --- %IDENT% ---
|
|
159
|
+
if ch == '%':
|
|
161
160
|
flush_text()
|
|
162
|
-
|
|
161
|
+
start = self.i
|
|
162
|
+
self.advance()
|
|
163
|
+
ident = self._read_ident()
|
|
164
|
+
if ident is not None and self.peek() == '%':
|
|
163
165
|
self.emit(TokenType.PERCENT_OPEN)
|
|
164
|
-
self.
|
|
165
|
-
|
|
166
|
+
self.emit(TokenType.IDENT, ident)
|
|
167
|
+
self.advance()
|
|
166
168
|
self.emit(TokenType.PERCENT_CLOSE)
|
|
167
|
-
|
|
169
|
+
continue
|
|
170
|
+
self.i = start
|
|
171
|
+
buf.append(self.peek())
|
|
168
172
|
self.advance()
|
|
169
173
|
continue
|
|
170
174
|
|
|
171
|
-
#
|
|
172
|
-
if
|
|
175
|
+
# --- :IDENT: ---
|
|
176
|
+
if ch == ':':
|
|
173
177
|
flush_text()
|
|
174
|
-
|
|
178
|
+
start = self.i
|
|
179
|
+
self.advance()
|
|
180
|
+
ident = self._read_ident()
|
|
181
|
+
if ident is not None and self.peek() == ':':
|
|
175
182
|
self.emit(TokenType.COLON_OPEN)
|
|
176
|
-
self.
|
|
177
|
-
|
|
183
|
+
self.emit(TokenType.IDENT, ident)
|
|
184
|
+
self.advance()
|
|
178
185
|
self.emit(TokenType.COLON_CLOSE)
|
|
179
|
-
|
|
186
|
+
continue
|
|
187
|
+
self.i = start
|
|
188
|
+
buf.append(self.peek())
|
|
180
189
|
self.advance()
|
|
181
190
|
continue
|
|
182
191
|
|
|
183
192
|
# --- parens ---
|
|
184
|
-
|
|
185
193
|
if ch == '(':
|
|
186
194
|
flush_text()
|
|
187
195
|
self.emit(TokenType.LPAREN)
|
|
@@ -194,39 +202,30 @@ class Tokenizer:
|
|
|
194
202
|
self.advance()
|
|
195
203
|
continue
|
|
196
204
|
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
if ch.isalnum():
|
|
200
|
-
flush_text()
|
|
201
|
-
start = self.i
|
|
202
|
-
|
|
203
|
-
# first char: [a-zA-Z0-9]
|
|
204
|
-
self.advance()
|
|
205
|
-
|
|
206
|
-
# middle chars: [a-zA-Z0-9._]*
|
|
207
|
-
while True:
|
|
208
|
-
nxt = self.peek()
|
|
209
|
-
if not nxt:
|
|
210
|
-
break
|
|
211
|
-
if nxt.isalnum() or nxt in '._':
|
|
212
|
-
self.advance()
|
|
213
|
-
else:
|
|
214
|
-
break
|
|
215
|
-
|
|
216
|
-
ident = self.text[start:self.i]
|
|
217
|
-
|
|
218
|
-
# enforce: no leading/trailing '_', no '-'
|
|
219
|
-
if ident.startswith('_') or ident.endswith('_') or '-' in ident:
|
|
220
|
-
# treat as TEXT instead
|
|
221
|
-
buf.append(ident)
|
|
222
|
-
else:
|
|
223
|
-
self.emit(TokenType.IDENT, ident)
|
|
224
|
-
continue
|
|
225
|
-
|
|
226
|
-
# --- fallback: TEXT ---
|
|
227
|
-
|
|
205
|
+
# fallback TEXT
|
|
228
206
|
buf.append(ch)
|
|
229
207
|
self.advance()
|
|
230
208
|
|
|
231
209
|
flush_text()
|
|
232
210
|
return self.tokens
|
|
211
|
+
|
|
212
|
+
def _read_ident(self) -> str | None:
|
|
213
|
+
start = self.i
|
|
214
|
+
ch = self.peek()
|
|
215
|
+
if not ch or not ch.isalnum():
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
self.advance()
|
|
219
|
+
while True:
|
|
220
|
+
nxt = self.peek()
|
|
221
|
+
if not nxt:
|
|
222
|
+
break
|
|
223
|
+
if nxt.isalnum() or nxt in '._':
|
|
224
|
+
self.advance()
|
|
225
|
+
else:
|
|
226
|
+
break
|
|
227
|
+
|
|
228
|
+
ident = self.text[start:self.i]
|
|
229
|
+
if ident.startswith('_') or ident.endswith('_') or '-' in ident:
|
|
230
|
+
return None
|
|
231
|
+
return ident
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
from .ast import TemplateNode, TextNode, IdentNode, CallNode
|
|
2
|
-
from .tokenizer import Tokenizer
|
|
3
|
-
from .parser import Parser
|
|
4
|
-
from .exceptions import MapResError, MissingKeyError
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class EvaluationError(MapResError):
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Evaluator:
|
|
12
|
-
def __init__(self, layerstack, ctx=None):
|
|
13
|
-
self.layerstack = layerstack
|
|
14
|
-
self.ctx = ctx or {}
|
|
15
|
-
self._seen = set() # cycle detection
|
|
16
|
-
|
|
17
|
-
# public api
|
|
18
|
-
def evaluate(self, node):
|
|
19
|
-
return self._eval(node)
|
|
20
|
-
|
|
21
|
-
# internal dispatch
|
|
22
|
-
def _eval(self, node):
|
|
23
|
-
if isinstance(node, TemplateNode):
|
|
24
|
-
return self._eval_template(node)
|
|
25
|
-
|
|
26
|
-
if isinstance(node, TextNode):
|
|
27
|
-
return node.text
|
|
28
|
-
|
|
29
|
-
if isinstance(node, IdentNode):
|
|
30
|
-
return self._eval_ident(node)
|
|
31
|
-
|
|
32
|
-
if isinstance(node, CallNode):
|
|
33
|
-
return self._eval_call(node)
|
|
34
|
-
|
|
35
|
-
raise EvaluationError(f'Unknown AST node: {node}')
|
|
36
|
-
|
|
37
|
-
# template
|
|
38
|
-
def _eval_template(self, node: TemplateNode):
|
|
39
|
-
parts = []
|
|
40
|
-
for child in node.children:
|
|
41
|
-
parts.append(self._eval(child))
|
|
42
|
-
out = ''.join(parts)
|
|
43
|
-
|
|
44
|
-
# recursion over full template output
|
|
45
|
-
if self._has_recursive_maps():
|
|
46
|
-
seen = set()
|
|
47
|
-
depth = 0
|
|
48
|
-
max_depth = self._get_max_depth()
|
|
49
|
-
|
|
50
|
-
while depth < max_depth:
|
|
51
|
-
new_out = self._re_resolve(out)
|
|
52
|
-
if new_out == out:
|
|
53
|
-
break
|
|
54
|
-
if new_out in seen:
|
|
55
|
-
raise EvaluationError('Cycle detected in template recursion')
|
|
56
|
-
seen.add(new_out)
|
|
57
|
-
out = new_out
|
|
58
|
-
depth += 1
|
|
59
|
-
|
|
60
|
-
return out
|
|
61
|
-
|
|
62
|
-
# identifier
|
|
63
|
-
def _eval_ident(self, node: IdentNode):
|
|
64
|
-
name = node.name
|
|
65
|
-
|
|
66
|
-
# ignore_delimiters: treat token as literal for this syntax
|
|
67
|
-
if self._syntax_ignores_delimiters(node.syntax):
|
|
68
|
-
return self._literal_from_syntax(name, node.syntax)
|
|
69
|
-
|
|
70
|
-
# cycle detection
|
|
71
|
-
if name in self._seen:
|
|
72
|
-
raise EvaluationError(f"Cycle detected in identifier '{name}'")
|
|
73
|
-
self._seen.add(name)
|
|
74
|
-
|
|
75
|
-
parts = name.split('.')
|
|
76
|
-
|
|
77
|
-
# 1. ctx first
|
|
78
|
-
val = self._lookup_hierarchical(self.ctx, parts)
|
|
79
|
-
if val is not None:
|
|
80
|
-
self._seen.remove(name)
|
|
81
|
-
return str(val)
|
|
82
|
-
|
|
83
|
-
# 2. syntax-bound maps
|
|
84
|
-
for m in self.layerstack.all_maps():
|
|
85
|
-
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
86
|
-
continue
|
|
87
|
-
|
|
88
|
-
d = self._map_to_dict(m)
|
|
89
|
-
val = self._lookup_hierarchical(d, parts)
|
|
90
|
-
if val is not None:
|
|
91
|
-
self._seen.remove(name)
|
|
92
|
-
return str(val)
|
|
93
|
-
|
|
94
|
-
self._seen.remove(name)
|
|
95
|
-
raise MissingKeyError(name)
|
|
96
|
-
|
|
97
|
-
# call (nested token)
|
|
98
|
-
def _eval_call(self, node: CallNode):
|
|
99
|
-
outer = node.name
|
|
100
|
-
|
|
101
|
-
# ignore_delimiters: treat call token as literal
|
|
102
|
-
if self._syntax_ignores_delimiters(node.syntax):
|
|
103
|
-
arg_text = self._eval(node.arg)
|
|
104
|
-
return self._literal_from_syntax(outer, node.syntax, is_call=True, arg_text=arg_text)
|
|
105
|
-
|
|
106
|
-
if outer in self._seen:
|
|
107
|
-
raise EvaluationError(f"Cycle detected in call '{outer}'")
|
|
108
|
-
self._seen.add(outer)
|
|
109
|
-
|
|
110
|
-
arg_value = self._eval(node.arg)
|
|
111
|
-
parts = [outer, arg_value]
|
|
112
|
-
|
|
113
|
-
# ctx first
|
|
114
|
-
val = self._lookup_hierarchical(self.ctx, parts)
|
|
115
|
-
if val is not None:
|
|
116
|
-
self._seen.remove(outer)
|
|
117
|
-
return str(val)
|
|
118
|
-
|
|
119
|
-
# syntax-bound maps
|
|
120
|
-
for m in self.layerstack.all_maps():
|
|
121
|
-
if getattr(m, '__syntax__', None) not in (None, node.syntax):
|
|
122
|
-
continue
|
|
123
|
-
|
|
124
|
-
d = self._map_to_dict(m)
|
|
125
|
-
val = self._lookup_hierarchical(d, parts)
|
|
126
|
-
if val is not None:
|
|
127
|
-
self._seen.remove(outer)
|
|
128
|
-
return str(val)
|
|
129
|
-
|
|
130
|
-
self._seen.remove(outer)
|
|
131
|
-
raise MissingKeyError(f'{outer}({arg_value})')
|
|
132
|
-
|
|
133
|
-
# helpers
|
|
134
|
-
def _map_to_dict(self, m):
|
|
135
|
-
if hasattr(m, 'as_map'):
|
|
136
|
-
return m.as_map()
|
|
137
|
-
if isinstance(m, dict):
|
|
138
|
-
return m
|
|
139
|
-
raise EvaluationError(f'Invalid map object: {m}')
|
|
140
|
-
|
|
141
|
-
def _lookup_hierarchical(self, root, parts):
|
|
142
|
-
cur = root
|
|
143
|
-
for p in parts:
|
|
144
|
-
if not isinstance(cur, dict):
|
|
145
|
-
return None
|
|
146
|
-
if p not in cur:
|
|
147
|
-
return None
|
|
148
|
-
cur = cur[p]
|
|
149
|
-
return cur
|
|
150
|
-
|
|
151
|
-
def _has_recursive_maps(self):
|
|
152
|
-
for m in self.layerstack.all_maps():
|
|
153
|
-
if getattr(m, '__recursive__', False) is True:
|
|
154
|
-
return True
|
|
155
|
-
return False
|
|
156
|
-
|
|
157
|
-
def _get_max_depth(self):
|
|
158
|
-
depths = [
|
|
159
|
-
getattr(m, '__max_depth__', 10)
|
|
160
|
-
for m in self.layerstack.all_maps()
|
|
161
|
-
if getattr(m, '__recursive__', False) is True
|
|
162
|
-
]
|
|
163
|
-
return max(depths) if depths else 0
|
|
164
|
-
|
|
165
|
-
def _re_resolve(self, text: str) -> str:
|
|
166
|
-
tokens = Tokenizer(text).tokenize()
|
|
167
|
-
ast = Parser(tokens).parse()
|
|
168
|
-
return self.evaluate(ast)
|
|
169
|
-
|
|
170
|
-
def _syntax_ignores_delimiters(self, syntax_name: str) -> bool:
|
|
171
|
-
for m in self.layerstack.all_maps():
|
|
172
|
-
if getattr(m, '__syntax__', None) == syntax_name:
|
|
173
|
-
if getattr(m, '__ignore_delimiters__', False):
|
|
174
|
-
return True
|
|
175
|
-
return False
|
|
176
|
-
|
|
177
|
-
def _literal_from_syntax(self, name: str, syntax_name: str, is_call: bool = False, arg_text: str | None = None) -> str:
|
|
178
|
-
if not is_call:
|
|
179
|
-
if syntax_name == 'colons':
|
|
180
|
-
return f':{name}:'
|
|
181
|
-
if syntax_name == 'braces':
|
|
182
|
-
return f'{{{{{name}}}}}'
|
|
183
|
-
if syntax_name == 'dollars':
|
|
184
|
-
return f'${{{name}}}'
|
|
185
|
-
if syntax_name == 'angles':
|
|
186
|
-
return f'<{name}>'
|
|
187
|
-
if syntax_name == 'pipes':
|
|
188
|
-
return f'|{name}|'
|
|
189
|
-
if syntax_name == 'percents':
|
|
190
|
-
return f'%{name}%'
|
|
191
|
-
if syntax_name == 'paren_dollars':
|
|
192
|
-
return f'$({name})'
|
|
193
|
-
if syntax_name == 'at_tags':
|
|
194
|
-
return f'@{name}@'
|
|
195
|
-
if syntax_name == 'hash_tags':
|
|
196
|
-
return f'#{name}#'
|
|
197
|
-
return name
|
|
198
|
-
|
|
199
|
-
# call literal: syntax-specific wrapping of name(arg_text)
|
|
200
|
-
arg = arg_text or ''
|
|
201
|
-
inner = f'{name}({arg})'
|
|
202
|
-
|
|
203
|
-
if syntax_name == 'colons':
|
|
204
|
-
return f':{inner}:'
|
|
205
|
-
if syntax_name == 'braces':
|
|
206
|
-
return f'{{{{{inner}}}}}'
|
|
207
|
-
if syntax_name == 'dollars':
|
|
208
|
-
return f'${{{inner}}}'
|
|
209
|
-
if syntax_name == 'angles':
|
|
210
|
-
return f'<{inner}>'
|
|
211
|
-
if syntax_name == 'pipes':
|
|
212
|
-
return f'|{inner}|'
|
|
213
|
-
if syntax_name == 'percents':
|
|
214
|
-
return f'%{inner}%'
|
|
215
|
-
if syntax_name == 'paren_dollars':
|
|
216
|
-
return f'$({inner})'
|
|
217
|
-
if syntax_name == 'at_tags':
|
|
218
|
-
return f'@{inner}@'
|
|
219
|
-
if syntax_name == 'hash_tags':
|
|
220
|
-
return f'#{inner}#'
|
|
221
|
-
return inner
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|