onelaraveljs 1.0.0 → 1.1.1
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.
- package/README.md +1 -1
- package/bin/onejs-build.js +32 -0
- package/package.json +11 -3
- package/scripts/README-template-compiler.md +133 -0
- package/scripts/README.md +61 -0
- package/scripts/__pycache__/build.cpython-314.pyc +0 -0
- package/scripts/__pycache__/compile.cpython-313.pyc +0 -0
- package/scripts/__pycache__/compile.cpython-314.pyc +0 -0
- package/scripts/build.py +573 -0
- package/scripts/check-system-errors.php +214 -0
- package/scripts/compile.py +101 -0
- package/scripts/compiler/README_CONFIG.md +196 -0
- package/scripts/compiler/__init__.py +18 -0
- package/scripts/compiler/__pycache__/__init__.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/binding_directive_service.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/class_binding_handler.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/compiler_utils.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/compiler_utils.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/conditional_handlers.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/conditional_handlers.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/config.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/config.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/declaration_tracker.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/directive_processors.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/directive_processors.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/echo_processor.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/event_directive_processor.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/event_directive_processor.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/function_generators.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/function_generators.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/loop_handlers.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/loop_handlers.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/main_compiler.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/main_compiler.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/parsers.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/parsers.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/php_converter.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/php_converter.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/php_js_converter.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/php_js_converter.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/register_parser.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/register_parser.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/section_handlers.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/section_handlers.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/show_directive_handler.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/style_directive_handler.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/template_analyzer.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/template_analyzer.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/template_processor.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/template_processor.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/template_processors.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/template_processors.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/utils.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/utils.cpython-314.pyc +0 -0
- package/scripts/compiler/__pycache__/wrapper_parser.cpython-313.pyc +0 -0
- package/scripts/compiler/__pycache__/wrapper_parser.cpython-314.pyc +0 -0
- package/scripts/compiler/binding_directive_service.py +103 -0
- package/scripts/compiler/class_binding_handler.py +347 -0
- package/scripts/compiler/cli.py +34 -0
- package/scripts/compiler/code_generator.py +141 -0
- package/scripts/compiler/compiler.config.json +36 -0
- package/scripts/compiler/compiler_utils.py +55 -0
- package/scripts/compiler/conditional_handlers.py +252 -0
- package/scripts/compiler/config.py +107 -0
- package/scripts/compiler/declaration_tracker.py +420 -0
- package/scripts/compiler/directive_processors.py +603 -0
- package/scripts/compiler/echo_processor.py +667 -0
- package/scripts/compiler/event_directive_processor.py +1099 -0
- package/scripts/compiler/fetch_parser.py +49 -0
- package/scripts/compiler/function_generators.py +310 -0
- package/scripts/compiler/loop_handlers.py +224 -0
- package/scripts/compiler/main_compiler.py +1763 -0
- package/scripts/compiler/parsers.py +1418 -0
- package/scripts/compiler/php_converter.py +470 -0
- package/scripts/compiler/php_js_converter.py +603 -0
- package/scripts/compiler/register_parser.py +480 -0
- package/scripts/compiler/section_handlers.py +122 -0
- package/scripts/compiler/show_directive_handler.py +85 -0
- package/scripts/compiler/style_directive_handler.py +169 -0
- package/scripts/compiler/template_analyzer.py +162 -0
- package/scripts/compiler/template_processor.py +1167 -0
- package/scripts/compiler/template_processors.py +1557 -0
- package/scripts/compiler/test_compiler.py +69 -0
- package/scripts/compiler/utils.py +54 -0
- package/scripts/compiler/variables_analyzer.py +135 -0
- package/scripts/compiler/view_identifier_generator.py +278 -0
- package/scripts/compiler/wrapper_parser.py +78 -0
- package/scripts/dev-context.js +311 -0
- package/scripts/dev.js +109 -0
- package/scripts/generate-assets-order.js +200 -0
- package/scripts/migrate-namespace.php +146 -0
- package/scripts/node/MIGRATION.md +190 -0
- package/scripts/node/README.md +269 -0
- package/scripts/node/build.js +208 -0
- package/scripts/node/compiler/compiler-utils.js +38 -0
- package/scripts/node/compiler/conditional-handlers.js +45 -0
- package/scripts/node/compiler/config.js +178 -0
- package/scripts/node/compiler/directive-processors.js +51 -0
- package/scripts/node/compiler/event-directive-processor.js +182 -0
- package/scripts/node/compiler/function-generators.js +239 -0
- package/scripts/node/compiler/loop-handlers.js +45 -0
- package/scripts/node/compiler/main-compiler.js +236 -0
- package/scripts/node/compiler/parsers.js +358 -0
- package/scripts/node/compiler/php-converter.js +227 -0
- package/scripts/node/compiler/register-parser.js +32 -0
- package/scripts/node/compiler/section-handlers.js +46 -0
- package/scripts/node/compiler/template-analyzer.js +50 -0
- package/scripts/node/compiler/template-processor.js +371 -0
- package/scripts/node/compiler/template-processors.js +219 -0
- package/scripts/node/compiler/utils.js +203 -0
- package/scripts/node/compiler/wrapper-parser.js +25 -0
- package/scripts/node/package.json +24 -0
- package/scripts/node/test-compiler.js +52 -0
- package/scripts/node-run.cjs +28 -0
- package/scripts/standardize-directories.php +92 -0
- package/templates/view.module.js +2 -0
- package/templates/view.tpl-raw.js +13 -0
- package/templates/wraper.js +71 -0
|
@@ -0,0 +1,1557 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Processors cho template lines và các directives khác
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from config import JS_FUNCTION_PREFIX, SPA_YIELD_ATTR_PREFIX, SPA_YIELD_SUBSCRIBE_KEY_PREFIX, SPA_YIELD_SUBSCRIBE_TARGET_PREFIX, SPA_YIELD_SUBSCRIBE_ATTR_PREFIX, SPA_YIELD_CONTENT_PREFIX, SPA_YIELD_CHILDREN_PREFIX, SPA_STATECHANGE_PREFIX, APP_VIEW_NAMESPACE
|
|
6
|
+
from php_converter import php_to_js, convert_php_array_to_json
|
|
7
|
+
from directive_processors import DirectiveProcessor
|
|
8
|
+
from utils import extract_balanced_parentheses
|
|
9
|
+
import re
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
class TemplateProcessors:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def _split_top_level(self, s, delimiter):
|
|
17
|
+
"""
|
|
18
|
+
Split string `s` by `delimiter` at top-level only (ignore delimiter inside (), [], {}, and quotes).
|
|
19
|
+
"""
|
|
20
|
+
parts = []
|
|
21
|
+
buf = ''
|
|
22
|
+
depth_par = 0
|
|
23
|
+
depth_br = 0
|
|
24
|
+
depth_cur = 0
|
|
25
|
+
in_single = False
|
|
26
|
+
in_double = False
|
|
27
|
+
i = 0
|
|
28
|
+
dlen = len(delimiter)
|
|
29
|
+
while i < len(s):
|
|
30
|
+
ch = s[i]
|
|
31
|
+
if ch == '\\':
|
|
32
|
+
if i + 1 < len(s):
|
|
33
|
+
buf += ch + s[i+1]
|
|
34
|
+
i += 2
|
|
35
|
+
continue
|
|
36
|
+
buf += ch
|
|
37
|
+
i += 1
|
|
38
|
+
continue
|
|
39
|
+
if in_single:
|
|
40
|
+
buf += ch
|
|
41
|
+
if ch == "'":
|
|
42
|
+
in_single = False
|
|
43
|
+
i += 1
|
|
44
|
+
continue
|
|
45
|
+
if in_double:
|
|
46
|
+
buf += ch
|
|
47
|
+
if ch == '"':
|
|
48
|
+
in_double = False
|
|
49
|
+
i += 1
|
|
50
|
+
continue
|
|
51
|
+
if ch == "'":
|
|
52
|
+
in_single = True
|
|
53
|
+
buf += ch
|
|
54
|
+
i += 1
|
|
55
|
+
continue
|
|
56
|
+
if ch == '"':
|
|
57
|
+
in_double = True
|
|
58
|
+
buf += ch
|
|
59
|
+
i += 1
|
|
60
|
+
continue
|
|
61
|
+
if ch == '(':
|
|
62
|
+
depth_par += 1
|
|
63
|
+
buf += ch
|
|
64
|
+
i += 1
|
|
65
|
+
continue
|
|
66
|
+
if ch == ')':
|
|
67
|
+
depth_par = max(0, depth_par - 1)
|
|
68
|
+
buf += ch
|
|
69
|
+
i += 1
|
|
70
|
+
continue
|
|
71
|
+
if ch == '[':
|
|
72
|
+
depth_br += 1
|
|
73
|
+
buf += ch
|
|
74
|
+
i += 1
|
|
75
|
+
continue
|
|
76
|
+
if ch == ']':
|
|
77
|
+
depth_br = max(0, depth_br - 1)
|
|
78
|
+
buf += ch
|
|
79
|
+
i += 1
|
|
80
|
+
continue
|
|
81
|
+
if ch == '{':
|
|
82
|
+
depth_cur += 1
|
|
83
|
+
buf += ch
|
|
84
|
+
i += 1
|
|
85
|
+
continue
|
|
86
|
+
if ch == '}':
|
|
87
|
+
depth_cur = max(0, depth_cur - 1)
|
|
88
|
+
buf += ch
|
|
89
|
+
i += 1
|
|
90
|
+
continue
|
|
91
|
+
# delimiter match at top level
|
|
92
|
+
if depth_par == 0 and depth_br == 0 and depth_cur == 0 and s[i:i+dlen] == delimiter:
|
|
93
|
+
parts.append(buf)
|
|
94
|
+
buf = ''
|
|
95
|
+
i += dlen
|
|
96
|
+
continue
|
|
97
|
+
buf += ch
|
|
98
|
+
i += 1
|
|
99
|
+
if buf != '':
|
|
100
|
+
parts.append(buf)
|
|
101
|
+
return parts
|
|
102
|
+
|
|
103
|
+
def _extract_vars_from_expr(self, expr):
|
|
104
|
+
"""Extract top-level PHP variable base names from expr (ignore $ inside single-quoted strings)"""
|
|
105
|
+
vars_set = []
|
|
106
|
+
in_single = False
|
|
107
|
+
in_double = False
|
|
108
|
+
escape = False
|
|
109
|
+
i = 0
|
|
110
|
+
while i < len(expr):
|
|
111
|
+
ch = expr[i]
|
|
112
|
+
if escape:
|
|
113
|
+
escape = False
|
|
114
|
+
i += 1
|
|
115
|
+
continue
|
|
116
|
+
if ch == '\\':
|
|
117
|
+
escape = True
|
|
118
|
+
i += 1
|
|
119
|
+
continue
|
|
120
|
+
if in_single:
|
|
121
|
+
if ch == "'":
|
|
122
|
+
in_single = False
|
|
123
|
+
i += 1
|
|
124
|
+
continue
|
|
125
|
+
if in_double:
|
|
126
|
+
if ch == '"':
|
|
127
|
+
in_double = False
|
|
128
|
+
i += 1
|
|
129
|
+
continue
|
|
130
|
+
if ch == '$':
|
|
131
|
+
j = i + 1
|
|
132
|
+
if j < len(expr) and re.match(r'[a-zA-Z_]', expr[j]):
|
|
133
|
+
start = j
|
|
134
|
+
j += 1
|
|
135
|
+
while j < len(expr) and re.match(r'[a-zA-Z0-9_]', expr[j]):
|
|
136
|
+
j += 1
|
|
137
|
+
name = expr[start:j]
|
|
138
|
+
if name not in vars_set:
|
|
139
|
+
vars_set.append(name)
|
|
140
|
+
i = j
|
|
141
|
+
continue
|
|
142
|
+
i += 1
|
|
143
|
+
continue
|
|
144
|
+
if ch == "'":
|
|
145
|
+
in_single = True
|
|
146
|
+
i += 1
|
|
147
|
+
continue
|
|
148
|
+
if ch == '"':
|
|
149
|
+
in_double = True
|
|
150
|
+
i += 1
|
|
151
|
+
continue
|
|
152
|
+
if ch == '$':
|
|
153
|
+
j = i + 1
|
|
154
|
+
if j < len(expr) and re.match(r'[a-zA-Z_]', expr[j]):
|
|
155
|
+
start = j
|
|
156
|
+
j += 1
|
|
157
|
+
while j < len(expr) and re.match(r'[a-zA-Z0-9_]', expr[j]):
|
|
158
|
+
j += 1
|
|
159
|
+
name = expr[start:j]
|
|
160
|
+
if name not in vars_set:
|
|
161
|
+
vars_set.append(name)
|
|
162
|
+
i = j
|
|
163
|
+
continue
|
|
164
|
+
i += 1
|
|
165
|
+
return vars_set
|
|
166
|
+
|
|
167
|
+
def _process_attr_directive(self, inner_expr):
|
|
168
|
+
"""
|
|
169
|
+
Build JS string for @attr inner expression.
|
|
170
|
+
Returns string like ${this.__attr({...})}
|
|
171
|
+
"""
|
|
172
|
+
if not inner_expr:
|
|
173
|
+
return '${this.__attr({})}'
|
|
174
|
+
|
|
175
|
+
attrs = {}
|
|
176
|
+
global_states = []
|
|
177
|
+
|
|
178
|
+
# determine if array form
|
|
179
|
+
s = inner_expr.strip()
|
|
180
|
+
if s and s[0] == '[' and s[-1] == ']':
|
|
181
|
+
inner = s[1:-1].strip()
|
|
182
|
+
pairs = self._split_top_level(inner, ',')
|
|
183
|
+
for pair in pairs:
|
|
184
|
+
if '=>' not in pair:
|
|
185
|
+
continue
|
|
186
|
+
kv = self._split_top_level(pair, '=>')
|
|
187
|
+
if len(kv) < 2:
|
|
188
|
+
continue
|
|
189
|
+
key_raw = kv[0].strip()
|
|
190
|
+
val_raw = '=>'.join(kv[1:]).strip()
|
|
191
|
+
# strip quotes from key
|
|
192
|
+
if (len(key_raw) >= 2) and ((key_raw[0] == '"' and key_raw[-1] == '"') or (key_raw[0] == "'" and key_raw[-1] == "'")):
|
|
193
|
+
key = key_raw[1:-1]
|
|
194
|
+
else:
|
|
195
|
+
key = key_raw
|
|
196
|
+
val = val_raw
|
|
197
|
+
# compute states and render
|
|
198
|
+
states = self._extract_vars_from_expr(val)
|
|
199
|
+
# collect global states - keep top-level variable names only
|
|
200
|
+
for st in states:
|
|
201
|
+
if st not in global_states:
|
|
202
|
+
global_states.append(st)
|
|
203
|
+
# convert php expression to js
|
|
204
|
+
try:
|
|
205
|
+
js_expr = php_to_js(val)
|
|
206
|
+
except Exception:
|
|
207
|
+
# fallback: simple replacements
|
|
208
|
+
js_expr = val.replace('.', ' + ').replace('$', '')
|
|
209
|
+
# Post-process js_expr for this val: restore string literals and fix common mis-conversions
|
|
210
|
+
# collect literals from val (per-attribute) to map __STR_LIT_n__ correctly
|
|
211
|
+
try:
|
|
212
|
+
literals = []
|
|
213
|
+
for m in re.finditer(r"'([^']*)'|\"([^\"]*)\"", val):
|
|
214
|
+
lit = m.group(1) if m.group(1) is not None else m.group(2)
|
|
215
|
+
literals.append(lit)
|
|
216
|
+
except Exception:
|
|
217
|
+
literals = []
|
|
218
|
+
|
|
219
|
+
def _restore_placeholders_local(txt):
|
|
220
|
+
def _r(m):
|
|
221
|
+
idx = int(m.group(1))
|
|
222
|
+
if idx < len(literals):
|
|
223
|
+
return "'" + literals[idx].replace("'", "\\'") + "'"
|
|
224
|
+
return "''"
|
|
225
|
+
return re.sub(r"__STR_LIT_(\d+)__", _r, txt)
|
|
226
|
+
|
|
227
|
+
js_expr = _restore_placeholders_local(js_expr)
|
|
228
|
+
js_expr = re.sub(r"(\b[a-zA-Z_][a-zA-Z0-9_]*)\s*\+\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", r"\1.\2(", js_expr)
|
|
229
|
+
js_expr = re.sub(r"(\b[a-zA-Z_][a-zA-Z0-9_]*)\s*\+\s*\[\s*(['\"])", r"\1[\2", js_expr)
|
|
230
|
+
|
|
231
|
+
attrs[key] = {
|
|
232
|
+
'states': states,
|
|
233
|
+
'render': js_expr
|
|
234
|
+
}
|
|
235
|
+
else:
|
|
236
|
+
# single form: @attr('name', expr)
|
|
237
|
+
m = re.match(r'^(["\'])(.+?)\1\s*,\s*(.+)$', s, flags=re.DOTALL)
|
|
238
|
+
if not m:
|
|
239
|
+
return None
|
|
240
|
+
key = m.group(2)
|
|
241
|
+
val = m.group(3).strip()
|
|
242
|
+
states = self._extract_vars_from_expr(val)
|
|
243
|
+
for st in states:
|
|
244
|
+
if st not in global_states:
|
|
245
|
+
global_states.append(st)
|
|
246
|
+
try:
|
|
247
|
+
js_expr = php_to_js(val)
|
|
248
|
+
except Exception:
|
|
249
|
+
js_expr = val.replace('.', ' + ').replace('$', '')
|
|
250
|
+
# Per-attribute post-processing (restore literals and fix concatenation/method access)
|
|
251
|
+
try:
|
|
252
|
+
literals = []
|
|
253
|
+
for m in re.finditer(r"'([^']*)'|\"([^\"]*)\"", val):
|
|
254
|
+
lit = m.group(1) if m.group(1) is not None else m.group(2)
|
|
255
|
+
literals.append(lit)
|
|
256
|
+
except Exception:
|
|
257
|
+
literals = []
|
|
258
|
+
|
|
259
|
+
def _restore_placeholders_local(txt):
|
|
260
|
+
def _r(m):
|
|
261
|
+
idx = int(m.group(1))
|
|
262
|
+
if idx < len(literals):
|
|
263
|
+
return "'" + literals[idx].replace("'", "\\'") + "'"
|
|
264
|
+
return "''"
|
|
265
|
+
return re.sub(r"__STR_LIT_(\d+)__", _r, txt)
|
|
266
|
+
|
|
267
|
+
js_expr = _restore_placeholders_local(js_expr)
|
|
268
|
+
js_expr = re.sub(r"(\b[a-zA-Z_][a-zA-Z0-9_]*)\s*\+\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", r"\1.\2(", js_expr)
|
|
269
|
+
js_expr = re.sub(r"(\b[a-zA-Z_][a-zA-Z0-9_]*)\s*\+\s*\[\s*(['\"])", r"\1[\2", js_expr)
|
|
270
|
+
|
|
271
|
+
attrs[key] = {
|
|
272
|
+
'states': states,
|
|
273
|
+
'render': js_expr
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Build JS object string
|
|
277
|
+
# global states
|
|
278
|
+
import json
|
|
279
|
+
global_states_js = json.dumps(global_states)
|
|
280
|
+
|
|
281
|
+
# build attrs object string, keeping render as raw JS arrow function
|
|
282
|
+
parts = []
|
|
283
|
+
for k, v in attrs.items():
|
|
284
|
+
states_js = json.dumps(v['states'])
|
|
285
|
+
render_js = v['render']
|
|
286
|
+
# ensure concatenation uses + (php_to_js should handle concat)
|
|
287
|
+
parts.append(f'"{k}": {{"states": {states_js}, "render": () => {render_js}}}')
|
|
288
|
+
|
|
289
|
+
attrs_js = '{' + ', '.join(parts) + '}'
|
|
290
|
+
|
|
291
|
+
# Return only the attrs object as requested by runtime API
|
|
292
|
+
# e.g. {"data-test": {"states": [...], "render": () => ...}, ...}
|
|
293
|
+
obj_js = attrs_js
|
|
294
|
+
return '${' + 'this.__attr(' + obj_js + ')}'
|
|
295
|
+
|
|
296
|
+
def process_template_line(self, line):
|
|
297
|
+
"""Process a regular template line"""
|
|
298
|
+
processed_line = line
|
|
299
|
+
|
|
300
|
+
# Warn when attribute-like directives are used outside HTML tag attributes.
|
|
301
|
+
try:
|
|
302
|
+
# directives that should appear inside tag attributes
|
|
303
|
+
attr_directives = ['attr', 'bind', 'val', 'class']
|
|
304
|
+
# any event-like directive: @word(...)
|
|
305
|
+
event_names = set(['click','change','input','submit','mouseover','mouseenter','mouseleave','keydown','keyup','focus','blur'])
|
|
306
|
+
for m in re.finditer(r'@([a-zA-Z_][a-zA-Z0-9_]*)\s*\(', processed_line):
|
|
307
|
+
name = m.group(1)
|
|
308
|
+
pos = m.start()
|
|
309
|
+
# find containing tag if any
|
|
310
|
+
inside_tag = False
|
|
311
|
+
for tm in re.finditer(r'<\s*[a-zA-Z][^>]*>', processed_line):
|
|
312
|
+
if tm.start() <= pos < tm.end():
|
|
313
|
+
inside_tag = True
|
|
314
|
+
break
|
|
315
|
+
# If it's an attribute directive or a known event directive and not inside tag, warn
|
|
316
|
+
if (name in attr_directives or name in event_names):
|
|
317
|
+
if not inside_tag:
|
|
318
|
+
print(f"Warning: directive '@{name}()' appears outside an HTML tag attribute. Move it inside the tag attributes (e.g. <tag @{name}(... ) ...>) for correct behavior.")
|
|
319
|
+
except Exception:
|
|
320
|
+
pass
|
|
321
|
+
|
|
322
|
+
# Handle @yield directive in HTML content
|
|
323
|
+
def replace_yield_directive(match):
|
|
324
|
+
yield_content = match.group(1).strip()
|
|
325
|
+
dollar_char = '$'
|
|
326
|
+
yield_content_js = php_to_js(yield_content) if yield_content.startswith(dollar_char) else yield_content
|
|
327
|
+
return "${" + JS_FUNCTION_PREFIX + ".yield(" + yield_content_js + ")}"
|
|
328
|
+
|
|
329
|
+
processed_line = re.sub(r'@yield\s*\(\s*(.*?)\s*\)', replace_yield_directive, processed_line)
|
|
330
|
+
|
|
331
|
+
# Handle inline @out(...) occurrences (supports nested/complex expressions)
|
|
332
|
+
try:
|
|
333
|
+
dp = DirectiveProcessor()
|
|
334
|
+
while True:
|
|
335
|
+
m = re.search(r'@out\s*\(', processed_line)
|
|
336
|
+
if not m:
|
|
337
|
+
break
|
|
338
|
+
start = m.start()
|
|
339
|
+
# extract balanced parentheses from start of '('
|
|
340
|
+
content, end_pos = extract_balanced_parentheses(processed_line, m.end() - 1)
|
|
341
|
+
if content is None:
|
|
342
|
+
break
|
|
343
|
+
full = processed_line[start:end_pos]
|
|
344
|
+
replacement = dp.process_out_directive(full)
|
|
345
|
+
if replacement is None:
|
|
346
|
+
break
|
|
347
|
+
processed_line = processed_line[:start] + replacement + processed_line[end_pos:]
|
|
348
|
+
except Exception:
|
|
349
|
+
# Fail gracefully and leave line unchanged
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
# Handle @include directive
|
|
353
|
+
def replace_include_directive(match):
|
|
354
|
+
view_name = match.group(1).strip()
|
|
355
|
+
variables = match.group(2).strip() if match.group(2) else '{}'
|
|
356
|
+
variables_js = convert_php_array_to_json(variables)
|
|
357
|
+
# Remove $ prefix from variables
|
|
358
|
+
variables_js = re.sub(r'\$(\w+)', r'\1', variables_js)
|
|
359
|
+
return "${" + APP_VIEW_NAMESPACE + ".renderView(this.__include('" + view_name + "', " + variables_js + "))}"
|
|
360
|
+
|
|
361
|
+
# Handle @include directive with PHP expressions (like $temp.'.ga-js')
|
|
362
|
+
def replace_include_php_directive(match):
|
|
363
|
+
view_expr = match.group(1).strip()
|
|
364
|
+
variables = match.group(2).strip() if match.group(2) else '{}'
|
|
365
|
+
variables_js = convert_php_array_to_json(variables)
|
|
366
|
+
# Remove $ prefix from variables
|
|
367
|
+
variables_js = re.sub(r'\$(\w+)', r'\1', variables_js)
|
|
368
|
+
# Convert PHP expression to JavaScript
|
|
369
|
+
view_expr_js = php_to_js(view_expr)
|
|
370
|
+
return "${" + APP_VIEW_NAMESPACE + ".renderView(this.__include(" + view_expr_js + ", " + variables_js + "))}"
|
|
371
|
+
|
|
372
|
+
# Handle @include directive with PHP expressions and variables (improved for multiline)
|
|
373
|
+
processed_line = re.sub(r'@include\s*\(\s*([^,\'"][^)]*?)\s*,\s*(\[[^\]]*\]|\{[^\}]*\}|[^)]*)\s*\)', replace_include_php_directive, processed_line, flags=re.DOTALL)
|
|
374
|
+
|
|
375
|
+
# Handle @include directive with string literals and variables (improved for multiline arrays/objects)
|
|
376
|
+
processed_line = re.sub(r'@include\s*\(\s*[\'"]([^\'"]*)[\'"]\s*,\s*(\[[^\]]*\]|\{[^\}]*\}|[^)]*)\s*\)', replace_include_directive, processed_line, flags=re.DOTALL)
|
|
377
|
+
|
|
378
|
+
# Handle @include directive with PHP expressions without variables
|
|
379
|
+
def replace_include_php_no_vars_directive(match):
|
|
380
|
+
view_expr = match.group(1).strip()
|
|
381
|
+
view_expr_js = php_to_js(view_expr)
|
|
382
|
+
return "${" + APP_VIEW_NAMESPACE + ".renderView(this.__include(" + view_expr_js + "))}"
|
|
383
|
+
|
|
384
|
+
processed_line = re.sub(r'@include\s*\(\s*([^,\'"][^)]*?)\s*\)', replace_include_php_no_vars_directive, processed_line)
|
|
385
|
+
|
|
386
|
+
# Handle @include directive with string literals without variables
|
|
387
|
+
processed_line = re.sub(r'@include\s*\(\s*[\'"]([^\'"]*)[\'"]\s*\)', r'${' + APP_VIEW_NAMESPACE + r'.renderView(this.__include("\1", {}))}', processed_line)
|
|
388
|
+
|
|
389
|
+
# Handle @includeif/@includeFf directive with path and data (2 parameters)
|
|
390
|
+
def replace_includeif_2params_directive(match):
|
|
391
|
+
view_path = match.group(1).strip()
|
|
392
|
+
data = match.group(2).strip()
|
|
393
|
+
|
|
394
|
+
# Convert view path to JavaScript
|
|
395
|
+
if view_path.startswith('"') and view_path.endswith('"'):
|
|
396
|
+
view_path_js = view_path
|
|
397
|
+
elif view_path.startswith("'") and view_path.endswith("'"):
|
|
398
|
+
view_path_js = f'"{view_path[1:-1]}"'
|
|
399
|
+
else:
|
|
400
|
+
view_path_js = php_to_js(view_path)
|
|
401
|
+
|
|
402
|
+
# Convert data to JavaScript
|
|
403
|
+
data_js = convert_php_array_to_json(data)
|
|
404
|
+
data_js = re.sub(r'\$(\w+)', r'\1', data_js)
|
|
405
|
+
|
|
406
|
+
return "${" + APP_VIEW_NAMESPACE + ".renderView(this.__includeif(" + view_path_js + ", " + data_js + "))}"
|
|
407
|
+
|
|
408
|
+
# Handle @includeif directive with variables
|
|
409
|
+
def replace_includeif_directive(match):
|
|
410
|
+
view_name = match.group(1).strip()
|
|
411
|
+
variables = match.group(2).strip() if match.group(2) else '{}'
|
|
412
|
+
variables_js = convert_php_array_to_json(variables)
|
|
413
|
+
# Remove $ prefix from variables
|
|
414
|
+
variables_js = re.sub(r'\$(\w+)', r'\1', variables_js)
|
|
415
|
+
return "${" + APP_VIEW_NAMESPACE + ".renderView(this.__includeif('" + view_name + "', " + variables_js + "))}"
|
|
416
|
+
|
|
417
|
+
# Handle @includeif with PHP expressions (must be before string literal patterns)
|
|
418
|
+
processed_line = re.sub(r'@includeif\s*\(\s*([^,]+?)\s*,\s*(\[.*?\])\s*\)', replace_includeif_2params_directive, processed_line, flags=re.IGNORECASE)
|
|
419
|
+
|
|
420
|
+
processed_line = re.sub(r'@includeif\s*\(\s*[\'"]([^\'"]*)[\'"]\s*,\s*(.*?)\s*\)', replace_includeif_directive, processed_line, flags=re.DOTALL | re.IGNORECASE)
|
|
421
|
+
|
|
422
|
+
# Handle @includeIf directive without variables (case insensitive)
|
|
423
|
+
processed_line = re.sub(r'@includeif\s*\(\s*[\'"]([^\'"]*)[\'"]\s*\)', r'${' + APP_VIEW_NAMESPACE + r'.renderView(this.__includeif("\1", {}))}', processed_line, flags=re.IGNORECASE)
|
|
424
|
+
|
|
425
|
+
# Handle @includeWhen/@includewhen directive with condition, path, and data
|
|
426
|
+
def replace_includewhen_directive(match):
|
|
427
|
+
condition = match.group(1).strip()
|
|
428
|
+
view_path = match.group(2).strip()
|
|
429
|
+
data = match.group(3).strip() if match.group(3) else '{}'
|
|
430
|
+
|
|
431
|
+
# Convert condition to JavaScript
|
|
432
|
+
condition_js = php_to_js(condition)
|
|
433
|
+
|
|
434
|
+
# Convert view path to JavaScript
|
|
435
|
+
if view_path.startswith('"') and view_path.endswith('"'):
|
|
436
|
+
view_path_js = view_path
|
|
437
|
+
elif view_path.startswith("'") and view_path.endswith("'"):
|
|
438
|
+
view_path_js = f'"{view_path[1:-1]}"'
|
|
439
|
+
else:
|
|
440
|
+
view_path_js = php_to_js(view_path)
|
|
441
|
+
|
|
442
|
+
# Convert data to JavaScript
|
|
443
|
+
data_js = convert_php_array_to_json(data)
|
|
444
|
+
data_js = re.sub(r'\$(\w+)', r'\1', data_js)
|
|
445
|
+
|
|
446
|
+
return "${" + APP_VIEW_NAMESPACE + ".renderView(this.__includewhen(" + condition_js + ", " + view_path_js + ", " + data_js + "))}"
|
|
447
|
+
|
|
448
|
+
# Handle @includeWhen/@includewhen with 3 parameters
|
|
449
|
+
processed_line = re.sub(r'@includewhen\s*\(\s*([^,]+?)\s*,\s*([^,]+?)\s*,\s*([^)]+?)\s*\)', replace_includewhen_directive, processed_line, flags=re.IGNORECASE)
|
|
450
|
+
|
|
451
|
+
# Handle @template/@view directive - alias of @wrap with enhanced parameter syntax
|
|
452
|
+
# Process this FIRST before @wrap/@wrapper to support template-style syntax
|
|
453
|
+
def replace_template_directive(match):
|
|
454
|
+
expression = match.group(1).strip() if match.group(1) else ''
|
|
455
|
+
|
|
456
|
+
if not expression:
|
|
457
|
+
return '__WRAPPER_CONFIG__ = { enable: true };'
|
|
458
|
+
|
|
459
|
+
# Check if it's template-style syntax (contains : or =>)
|
|
460
|
+
if ':' in expression or '=>' in expression:
|
|
461
|
+
# Parse template parameters and convert to wrapper format
|
|
462
|
+
attributes = self._parse_template_parameters(expression)
|
|
463
|
+
tag = attributes.pop('tag', None)
|
|
464
|
+
|
|
465
|
+
# Process subscribe parameter specially
|
|
466
|
+
if 'subscribe' in attributes:
|
|
467
|
+
subscribe_value = attributes['subscribe']
|
|
468
|
+
attributes['subscribe'] = self._process_subscribe_value(subscribe_value)
|
|
469
|
+
|
|
470
|
+
return self._generate_wrapper_config(attributes, tag)
|
|
471
|
+
else:
|
|
472
|
+
# Simple wrap-style syntax - parse as wrap directive
|
|
473
|
+
if expression.startswith('[') and expression.endswith(']'):
|
|
474
|
+
# Case 3: @view($attributes)
|
|
475
|
+
attributes = self._parse_wrap_attributes(expression)
|
|
476
|
+
return self._generate_wrapper_config(attributes)
|
|
477
|
+
else:
|
|
478
|
+
# Case 2: @view($tag, $attributes)
|
|
479
|
+
parts = self._parse_wrap_expression(expression)
|
|
480
|
+
tag = parts['tag']
|
|
481
|
+
attributes = parts['attributes']
|
|
482
|
+
return self._generate_wrapper_config(attributes, tag)
|
|
483
|
+
|
|
484
|
+
# Handle @template/@view with parameters (multiline support)
|
|
485
|
+
# Note: Process this BEFORE @wrap/@wrapper pattern
|
|
486
|
+
processed_line = re.sub(r'@(?:template|view)\s*\(([^)]*)\)', replace_template_directive, processed_line, flags=re.IGNORECASE | re.DOTALL)
|
|
487
|
+
|
|
488
|
+
# Handle @template/@view without parameters
|
|
489
|
+
processed_line = re.sub(r'@(?:template|view)(?:\s*\(\s*\))?\s*$', '__WRAPPER_CONFIG__ = { enable: true };', processed_line, flags=re.IGNORECASE)
|
|
490
|
+
|
|
491
|
+
# Handle @wrap/@wrapper directive (NOT @view - that's handled above)
|
|
492
|
+
def replace_wrap_directive(match):
|
|
493
|
+
expression = match.group(1).strip() if match.group(1) else ''
|
|
494
|
+
|
|
495
|
+
# Case 1: @wrap() or @wrap (no parameters)
|
|
496
|
+
if not expression:
|
|
497
|
+
return '__WRAPPER_CONFIG__ = { enable: true };'
|
|
498
|
+
|
|
499
|
+
# Parse expression to determine case
|
|
500
|
+
if expression.startswith('[') and expression.endswith(']'):
|
|
501
|
+
# Case 3: @wrap($attributes)
|
|
502
|
+
attributes = self._parse_wrap_attributes(expression)
|
|
503
|
+
return self._generate_wrapper_config(attributes)
|
|
504
|
+
else:
|
|
505
|
+
# Case 2: @wrap($tag, $attributes)
|
|
506
|
+
parts = self._parse_wrap_expression(expression)
|
|
507
|
+
tag = parts['tag']
|
|
508
|
+
attributes = parts['attributes']
|
|
509
|
+
return self._generate_wrapper_config(attributes, tag)
|
|
510
|
+
|
|
511
|
+
# Handle @wrap/@wrapper with parameters (NOT @view)
|
|
512
|
+
processed_line = re.sub(r'@(?:wrap|wrapper)\s*\(\s*([^)]*?)\s*\)', replace_wrap_directive, processed_line, flags=re.IGNORECASE)
|
|
513
|
+
|
|
514
|
+
# Handle @wrap/@wrapper without parameters (NOT @view)
|
|
515
|
+
processed_line = re.sub(r'@(?:wrap|wrapper)(?:\s*\(\s*\))?\s*$', '__WRAPPER_CONFIG__ = { enable: true };', processed_line, flags=re.IGNORECASE)
|
|
516
|
+
|
|
517
|
+
# Handle @endWrap/@endWrapper/@endView/@endTemplate - keep as marker
|
|
518
|
+
processed_line = re.sub(r'@end(?:wrap|wrapper|view|template)(?:\s*\(\s*\))?\s*$', '__WRAPPER_END__', processed_line, flags=re.IGNORECASE)
|
|
519
|
+
|
|
520
|
+
# Handle @yieldAttr directive - improved to group multiple directives
|
|
521
|
+
def process_multiple_yieldattr(line):
|
|
522
|
+
"""Process multiple @yieldattr directives and group them into single on-subscribe-attr"""
|
|
523
|
+
import re
|
|
524
|
+
|
|
525
|
+
# Find all on-yield-attr attributes
|
|
526
|
+
yieldattr_pattern = r'@yieldattr\s*\(\s*[\'"]([^\'"]*)[\'"]\s*,\s*[\'"]([^\'"]*)[\'"]\s*(?:,\s*[\'"]([^\'"]*)[\'"])?\s*\)'
|
|
527
|
+
matches = list(re.finditer(yieldattr_pattern, line, re.IGNORECASE))
|
|
528
|
+
|
|
529
|
+
if not matches:
|
|
530
|
+
return line
|
|
531
|
+
|
|
532
|
+
# Collect all attributes and subscribe mappings
|
|
533
|
+
attributes = []
|
|
534
|
+
subscribe_attrs = []
|
|
535
|
+
|
|
536
|
+
for match in matches:
|
|
537
|
+
attr_key = match.group(1).strip().strip("'\"")
|
|
538
|
+
yield_key = match.group(2).strip().strip("'\"")
|
|
539
|
+
default_value = match.group(3).strip() if match.group(3) else 'null'
|
|
540
|
+
|
|
541
|
+
if default_value != 'null':
|
|
542
|
+
default_value = default_value.strip("'\"")
|
|
543
|
+
default_value = f"'{default_value}'"
|
|
544
|
+
|
|
545
|
+
# Add attribute
|
|
546
|
+
attributes.append(f'{attr_key}="${{{JS_FUNCTION_PREFIX}.yieldContent(\'{yield_key}\', {default_value})}}"')
|
|
547
|
+
# Add to subscribe mapping
|
|
548
|
+
subscribe_attrs.append(f'{attr_key}:{yield_key}')
|
|
549
|
+
|
|
550
|
+
# Replace all @yieldattr with combined result
|
|
551
|
+
result = line
|
|
552
|
+
for match in reversed(matches): # Process in reverse order to maintain positions
|
|
553
|
+
result = result[:match.start()] + '' + result[match.end():]
|
|
554
|
+
|
|
555
|
+
# Add all attributes and single subscribe attribute
|
|
556
|
+
attributes_str = ' '.join(attributes)
|
|
557
|
+
subscribe_str = f'{SPA_YIELD_SUBSCRIBE_ATTR_PREFIX}="{",".join(subscribe_attrs)}"'
|
|
558
|
+
|
|
559
|
+
# Find the position to insert (after the last attribute)
|
|
560
|
+
insert_pos = result.find('>')
|
|
561
|
+
if insert_pos != -1:
|
|
562
|
+
result = result[:insert_pos] + ' ' + attributes_str + ' ' + subscribe_str + result[insert_pos:]
|
|
563
|
+
|
|
564
|
+
return result
|
|
565
|
+
|
|
566
|
+
# Handle @yieldon/@onyield/@yieldListen/@yieldWatch directive with array syntax
|
|
567
|
+
def replace_yieldon_array_directive(match):
|
|
568
|
+
array_content = match.group(1).strip()
|
|
569
|
+
# Parse array content: ['attrKey' => 'yieldKey', '#key' => 'yieldKey', ...]
|
|
570
|
+
result = []
|
|
571
|
+
subscribe_attrs = []
|
|
572
|
+
|
|
573
|
+
# Split by comma but respect quotes and brackets
|
|
574
|
+
items = []
|
|
575
|
+
current_item = ""
|
|
576
|
+
in_quotes = False
|
|
577
|
+
quote_char = ""
|
|
578
|
+
paren_count = 0
|
|
579
|
+
|
|
580
|
+
for char in array_content:
|
|
581
|
+
if (char == '"' or char == "'") and not in_quotes:
|
|
582
|
+
in_quotes = True
|
|
583
|
+
quote_char = char
|
|
584
|
+
elif char == quote_char and in_quotes:
|
|
585
|
+
in_quotes = False
|
|
586
|
+
quote_char = ""
|
|
587
|
+
elif not in_quotes:
|
|
588
|
+
if char == '[':
|
|
589
|
+
paren_count += 1
|
|
590
|
+
elif char == ']':
|
|
591
|
+
paren_count -= 1
|
|
592
|
+
elif char == ',' and paren_count == 0:
|
|
593
|
+
items.append(current_item.strip())
|
|
594
|
+
current_item = ""
|
|
595
|
+
continue
|
|
596
|
+
|
|
597
|
+
current_item += char
|
|
598
|
+
|
|
599
|
+
if current_item.strip():
|
|
600
|
+
items.append(current_item.strip())
|
|
601
|
+
|
|
602
|
+
# Process each item
|
|
603
|
+
for item in items:
|
|
604
|
+
if '=>' in item:
|
|
605
|
+
key, value = item.split('=>', 1)
|
|
606
|
+
key = key.strip().strip("'\"")
|
|
607
|
+
value = value.strip().strip("'\"")
|
|
608
|
+
|
|
609
|
+
# Remove $ prefix from value (state variable)
|
|
610
|
+
if value.startswith('$'):
|
|
611
|
+
value = value[1:]
|
|
612
|
+
|
|
613
|
+
if key == '#content':
|
|
614
|
+
# Special key #content
|
|
615
|
+
result.append(f'{SPA_YIELD_CONTENT_PREFIX}="{value}"')
|
|
616
|
+
elif key == '#children':
|
|
617
|
+
# Special key #children
|
|
618
|
+
result.append(f'{SPA_YIELD_CHILDREN_PREFIX}="{value}"')
|
|
619
|
+
else:
|
|
620
|
+
# Regular attribute - create attribute with yieldContent
|
|
621
|
+
result.append(f'{key}="${{{JS_FUNCTION_PREFIX}.yieldContent(\'{value}\', null)}}"')
|
|
622
|
+
subscribe_attrs.append(f'{key}:{value}')
|
|
623
|
+
|
|
624
|
+
# Add subscribe attribute if there are regular attributes
|
|
625
|
+
if subscribe_attrs:
|
|
626
|
+
result.append(f'{SPA_YIELD_SUBSCRIBE_ATTR_PREFIX}="{",".join(subscribe_attrs)}"')
|
|
627
|
+
|
|
628
|
+
return ' '.join(result)
|
|
629
|
+
|
|
630
|
+
# Handle @yieldon/@onyield/@yieldListen/@yieldWatch directive with array syntax
|
|
631
|
+
# Handle @yieldon/@onyield/@yieldListen/@yieldWatch directive with array syntax - more specific regex to avoid conflicts
|
|
632
|
+
processed_line = re.sub(r'@(?:yieldon|onyield|yieldlisten|yieldwatch)\s*\(\s*\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\]\s*\)', replace_yieldon_array_directive, processed_line, flags=re.DOTALL | re.IGNORECASE)
|
|
633
|
+
|
|
634
|
+
# Handle @yieldon/@onyield/@yieldListen/@yieldWatch directive with simple syntax
|
|
635
|
+
def replace_yieldon_directive(match):
|
|
636
|
+
attr_key = match.group(1).strip()
|
|
637
|
+
yield_key = match.group(2).strip()
|
|
638
|
+
default_value = match.group(3).strip() if match.group(3) else 'null'
|
|
639
|
+
# Remove quotes from parameters
|
|
640
|
+
attr_key = attr_key.strip("'\"")
|
|
641
|
+
yield_key = yield_key.strip("'\"")
|
|
642
|
+
if default_value != 'null':
|
|
643
|
+
default_value = default_value.strip("'\"")
|
|
644
|
+
default_value = f"'{default_value}'"
|
|
645
|
+
|
|
646
|
+
# Create attribute with yieldContent
|
|
647
|
+
result = f'{attr_key}="${{{JS_FUNCTION_PREFIX}.yieldContent(\'{yield_key}\', {default_value})}}"'
|
|
648
|
+
# Add subscribe attribute
|
|
649
|
+
result += f' {SPA_YIELD_SUBSCRIBE_ATTR_PREFIX}="{attr_key}:{yield_key}"'
|
|
650
|
+
return result
|
|
651
|
+
|
|
652
|
+
processed_line = re.sub(r'@(?:yieldon|onyield|yieldlisten|yieldwatch)\s*\(\s*[\'"]([^\'"]*)[\'"]\s*,\s*[\'"]([^\'"]*)[\'"]\s*(?:,\s*[\'"]([^\'"]*)[\'"])?\s*\)', replace_yieldon_directive, processed_line, flags=re.IGNORECASE)
|
|
653
|
+
|
|
654
|
+
# Handle @yieldAttr directive - process after @yieldon to avoid conflicts
|
|
655
|
+
processed_line = process_multiple_yieldattr(processed_line)
|
|
656
|
+
|
|
657
|
+
# Handle @attr directive per HTML tag (skip @attr inside event directive params)
|
|
658
|
+
try:
|
|
659
|
+
while True:
|
|
660
|
+
m = re.search(r'@attr\s*\(', processed_line)
|
|
661
|
+
if not m:
|
|
662
|
+
break
|
|
663
|
+
attr_pos = m.start()
|
|
664
|
+
|
|
665
|
+
# Find the HTML tag that contains this position (if any)
|
|
666
|
+
tag_match = None
|
|
667
|
+
for tm in re.finditer(r'<\s*[a-zA-Z][^>]*>', processed_line):
|
|
668
|
+
if tm.start() <= attr_pos < tm.end():
|
|
669
|
+
tag_match = tm
|
|
670
|
+
break
|
|
671
|
+
|
|
672
|
+
if not tag_match:
|
|
673
|
+
# Not inside a tag — process normally
|
|
674
|
+
content, end_pos = extract_balanced_parentheses(processed_line, m.end() - 1)
|
|
675
|
+
if content is None:
|
|
676
|
+
break
|
|
677
|
+
replacement = self._process_attr_directive(content.strip())
|
|
678
|
+
if replacement is None:
|
|
679
|
+
break
|
|
680
|
+
processed_line = processed_line[:m.start()] + replacement + processed_line[end_pos:]
|
|
681
|
+
continue
|
|
682
|
+
|
|
683
|
+
tag_start = tag_match.start()
|
|
684
|
+
tag_end = tag_match.end()
|
|
685
|
+
tag_text = processed_line[tag_start:tag_end]
|
|
686
|
+
|
|
687
|
+
# Find event directive ranges inside this tag to avoid processing @attr inside them
|
|
688
|
+
event_ranges = []
|
|
689
|
+
for em in re.finditer(r'@[a-zA-Z_][a-zA-Z0-9_]*\s*\(', tag_text):
|
|
690
|
+
em_abs_start = tag_start + em.start()
|
|
691
|
+
# find balanced parentheses globally
|
|
692
|
+
content_ev, ev_end = extract_balanced_parentheses(processed_line, tag_start + em.end() - 1)
|
|
693
|
+
if content_ev is None:
|
|
694
|
+
continue
|
|
695
|
+
event_ranges.append((em_abs_start, ev_end))
|
|
696
|
+
|
|
697
|
+
# Collect all @attr occurrences inside this tag that are NOT inside event ranges
|
|
698
|
+
attrs = []
|
|
699
|
+
for am in re.finditer(r'@attr\s*\(', tag_text):
|
|
700
|
+
am_abs_start = tag_start + am.start()
|
|
701
|
+
content_attr, attr_end = extract_balanced_parentheses(processed_line, tag_start + am.end() - 1)
|
|
702
|
+
if content_attr is None:
|
|
703
|
+
continue
|
|
704
|
+
# Check whether this attr is inside any event range
|
|
705
|
+
inside_event = False
|
|
706
|
+
for er in event_ranges:
|
|
707
|
+
if am_abs_start >= er[0] and am_abs_start < er[1]:
|
|
708
|
+
inside_event = True
|
|
709
|
+
break
|
|
710
|
+
if not inside_event:
|
|
711
|
+
attrs.append((am_abs_start, attr_end, content_attr))
|
|
712
|
+
|
|
713
|
+
if not attrs:
|
|
714
|
+
# No non-event @attr in this tag — skip this occurrence
|
|
715
|
+
# Move past this @attr and continue
|
|
716
|
+
search_pos = m.end()
|
|
717
|
+
next_m = re.search(r'@attr\s*\(', processed_line[search_pos:])
|
|
718
|
+
if not next_m:
|
|
719
|
+
break
|
|
720
|
+
# adjust processed_line search by slicing
|
|
721
|
+
# continue loop to handle next occurrence
|
|
722
|
+
# Recompute global search by updating processed_line in next iteration
|
|
723
|
+
# To avoid infinite loop, remove this occurrence from consideration by replacing temporarily
|
|
724
|
+
# We'll just skip by slicing the string beyond this occurrence
|
|
725
|
+
processed_line = processed_line[:m.end()] + processed_line[m.end():]
|
|
726
|
+
break
|
|
727
|
+
|
|
728
|
+
# Enforce only one @attr per tag — process the first non-event occurrence
|
|
729
|
+
first_attr = attrs[0]
|
|
730
|
+
a_start, a_end, a_content = first_attr
|
|
731
|
+
replacement = self._process_attr_directive(a_content.strip())
|
|
732
|
+
if replacement is None:
|
|
733
|
+
# nothing to replace
|
|
734
|
+
break
|
|
735
|
+
processed_line = processed_line[:a_start] + replacement + processed_line[a_end:]
|
|
736
|
+
|
|
737
|
+
# If there are more non-event @attr in same tag, warn and leave them untouched
|
|
738
|
+
if len(attrs) > 1:
|
|
739
|
+
print(f"Warning: multiple @attr found on single tag at position {tag_start}. Only the first was applied.")
|
|
740
|
+
|
|
741
|
+
# Restart processing from beginning because string changed
|
|
742
|
+
continue
|
|
743
|
+
except Exception:
|
|
744
|
+
pass
|
|
745
|
+
# Fallback: if any @attr(...) remain (edge cases where tag-scoped processing missed them),
|
|
746
|
+
# process them globally.
|
|
747
|
+
try:
|
|
748
|
+
while True:
|
|
749
|
+
m = re.search(r'@attr\s*\(', processed_line)
|
|
750
|
+
if not m:
|
|
751
|
+
break
|
|
752
|
+
content, end_pos = extract_balanced_parentheses(processed_line, m.end() - 1)
|
|
753
|
+
if content is None:
|
|
754
|
+
break
|
|
755
|
+
replacement = self._process_attr_directive(content.strip())
|
|
756
|
+
if replacement is None:
|
|
757
|
+
break
|
|
758
|
+
processed_line = processed_line[:m.start()] + replacement + processed_line[end_pos:]
|
|
759
|
+
except Exception:
|
|
760
|
+
pass
|
|
761
|
+
|
|
762
|
+
# Handle @subscribe directive with array syntax
|
|
763
|
+
def replace_subscribe_array_directive(match):
|
|
764
|
+
array_content = match.group(1).strip()
|
|
765
|
+
# Parse array content: ['attrkey' => $stateKey, '#children' => $childrenState, '#content' => $contentState]
|
|
766
|
+
result = []
|
|
767
|
+
|
|
768
|
+
# Split by comma but respect quotes and brackets
|
|
769
|
+
items = []
|
|
770
|
+
current_item = ""
|
|
771
|
+
in_quotes = False
|
|
772
|
+
quote_char = ""
|
|
773
|
+
paren_count = 0
|
|
774
|
+
|
|
775
|
+
for char in array_content:
|
|
776
|
+
if (char == '"' or char == "'") and not in_quotes:
|
|
777
|
+
in_quotes = True
|
|
778
|
+
quote_char = char
|
|
779
|
+
elif char == quote_char and in_quotes:
|
|
780
|
+
in_quotes = False
|
|
781
|
+
quote_char = ""
|
|
782
|
+
elif not in_quotes:
|
|
783
|
+
if char == '[':
|
|
784
|
+
paren_count += 1
|
|
785
|
+
elif char == ']':
|
|
786
|
+
paren_count -= 1
|
|
787
|
+
elif char == ',' and paren_count == 0:
|
|
788
|
+
items.append(current_item.strip())
|
|
789
|
+
current_item = ""
|
|
790
|
+
continue
|
|
791
|
+
|
|
792
|
+
current_item += char
|
|
793
|
+
|
|
794
|
+
if current_item.strip():
|
|
795
|
+
items.append(current_item.strip())
|
|
796
|
+
|
|
797
|
+
# Process each item
|
|
798
|
+
for item in items:
|
|
799
|
+
if '=>' in item:
|
|
800
|
+
key, value = item.split('=>', 1)
|
|
801
|
+
key = key.strip().strip("'\"")
|
|
802
|
+
value = value.strip().strip("'\"")
|
|
803
|
+
|
|
804
|
+
# Remove $ prefix from state variable
|
|
805
|
+
if value.startswith('$'):
|
|
806
|
+
state_key = value[1:]
|
|
807
|
+
else:
|
|
808
|
+
state_key = value
|
|
809
|
+
|
|
810
|
+
if key == '#children':
|
|
811
|
+
# Special key #children
|
|
812
|
+
result.append(f'{SPA_STATECHANGE_PREFIX}{state_key}="#children"')
|
|
813
|
+
elif key == '#content':
|
|
814
|
+
# Special key #content
|
|
815
|
+
result.append(f'{SPA_STATECHANGE_PREFIX}{state_key}="#content"')
|
|
816
|
+
else:
|
|
817
|
+
# Regular attribute
|
|
818
|
+
result.append(f'{SPA_STATECHANGE_PREFIX}{state_key}="{key}"')
|
|
819
|
+
|
|
820
|
+
return ' '.join(result)
|
|
821
|
+
|
|
822
|
+
# Handle @subscribe directive with new approach
|
|
823
|
+
def replace_subscribe_directive(match):
|
|
824
|
+
full_match = match.group(0)
|
|
825
|
+
# Extract the content inside parentheses
|
|
826
|
+
paren_match = re.search(r'@subscribe\s*\((.*)\)', full_match, re.IGNORECASE)
|
|
827
|
+
if not paren_match:
|
|
828
|
+
return full_match
|
|
829
|
+
|
|
830
|
+
content = paren_match.group(1).strip()
|
|
831
|
+
|
|
832
|
+
# Case 1: Single parameter - @subscribe($stateKey)
|
|
833
|
+
single_match = re.match(r'^\$?(\w+)$', content)
|
|
834
|
+
if single_match:
|
|
835
|
+
state_key = single_match.group(1)
|
|
836
|
+
return f'${{this.__subscribe({{\"#all\": [\"{state_key}\"]}})}}'
|
|
837
|
+
|
|
838
|
+
# Case 2: Two parameters - @subscribe($stateKey, 'attrKey') or @subscribe($stateKey, "#children")
|
|
839
|
+
two_params_match = re.match(r'^\$?(\w+)\s*,\s*[\'"]([^\'"]*)[\'"]$', content)
|
|
840
|
+
if two_params_match:
|
|
841
|
+
state_key = two_params_match.group(1)
|
|
842
|
+
attr_key = two_params_match.group(2)
|
|
843
|
+
return f'${{this.__subscribe({{\"{attr_key}\": [\"{state_key}\"]}})}}'
|
|
844
|
+
|
|
845
|
+
# Case 3: Array of state variables - @subscribe([$stateKey, $contentState])
|
|
846
|
+
if content.startswith('[') and content.endswith(']'):
|
|
847
|
+
array_content = content[1:-1].strip()
|
|
848
|
+
# Check if it contains => (key-value pairs) or just state variables
|
|
849
|
+
if '=>' in array_content:
|
|
850
|
+
# Case 4: Array with key-value pairs - @subscribe(['attrKey' => $stateKey, ...])
|
|
851
|
+
return process_subscribe_array_keyvalue(array_content)
|
|
852
|
+
else:
|
|
853
|
+
# Case 3: Array of state variables - @subscribe([$stateKey, $contentState])
|
|
854
|
+
state_keys = parse_state_array(array_content)
|
|
855
|
+
return f'${{this.__subscribe({{\"#all\": {json.dumps(state_keys)}}})}}'
|
|
856
|
+
|
|
857
|
+
# Case 5: Array with second parameter - @subscribe([$stateKey, $contentState], "#children")
|
|
858
|
+
array_with_attr_match = re.match(r'^\[([^\]]+)\]\s*,\s*[\'"]([^\'"]*)[\'"]$', content)
|
|
859
|
+
if array_with_attr_match:
|
|
860
|
+
array_content = array_with_attr_match.group(1).strip()
|
|
861
|
+
attr_key = array_with_attr_match.group(2)
|
|
862
|
+
state_keys = parse_state_array(array_content)
|
|
863
|
+
return f'${{this.__subscribe({{\"{attr_key}\": {json.dumps(state_keys)}}})}}'
|
|
864
|
+
|
|
865
|
+
return full_match
|
|
866
|
+
|
|
867
|
+
def parse_state_array(array_content):
|
|
868
|
+
"""Parse array content and extract state keys"""
|
|
869
|
+
state_keys = []
|
|
870
|
+
items = []
|
|
871
|
+
current_item = ""
|
|
872
|
+
in_quotes = False
|
|
873
|
+
quote_char = ""
|
|
874
|
+
paren_count = 0
|
|
875
|
+
|
|
876
|
+
for char in array_content:
|
|
877
|
+
if (char == '"' or char == "'") and not in_quotes:
|
|
878
|
+
in_quotes = True
|
|
879
|
+
quote_char = char
|
|
880
|
+
current_item += char
|
|
881
|
+
elif char == quote_char and in_quotes:
|
|
882
|
+
in_quotes = False
|
|
883
|
+
quote_char = ""
|
|
884
|
+
current_item += char
|
|
885
|
+
elif char == '[' and not in_quotes:
|
|
886
|
+
paren_count += 1
|
|
887
|
+
current_item += char
|
|
888
|
+
elif char == ']' and not in_quotes:
|
|
889
|
+
paren_count -= 1
|
|
890
|
+
current_item += char
|
|
891
|
+
elif char == ',' and not in_quotes and paren_count == 0:
|
|
892
|
+
if current_item.strip():
|
|
893
|
+
items.append(current_item.strip())
|
|
894
|
+
current_item = ""
|
|
895
|
+
else:
|
|
896
|
+
current_item += char
|
|
897
|
+
|
|
898
|
+
if current_item.strip():
|
|
899
|
+
items.append(current_item.strip())
|
|
900
|
+
|
|
901
|
+
# Process each item
|
|
902
|
+
for item in items:
|
|
903
|
+
item = item.strip()
|
|
904
|
+
# Remove $ prefix from state variable
|
|
905
|
+
if item.startswith('$'):
|
|
906
|
+
state_key = item[1:]
|
|
907
|
+
else:
|
|
908
|
+
state_key = item
|
|
909
|
+
state_keys.append(state_key)
|
|
910
|
+
|
|
911
|
+
return state_keys
|
|
912
|
+
|
|
913
|
+
def process_subscribe_array_keyvalue(array_content):
|
|
914
|
+
"""Process array with key-value pairs"""
|
|
915
|
+
result = {}
|
|
916
|
+
|
|
917
|
+
# Split by comma but respect quotes and brackets
|
|
918
|
+
items = []
|
|
919
|
+
current_item = ""
|
|
920
|
+
in_quotes = False
|
|
921
|
+
quote_char = ""
|
|
922
|
+
paren_count = 0
|
|
923
|
+
|
|
924
|
+
for char in array_content:
|
|
925
|
+
if (char == '"' or char == "'") and not in_quotes:
|
|
926
|
+
in_quotes = True
|
|
927
|
+
quote_char = char
|
|
928
|
+
current_item += char
|
|
929
|
+
elif char == quote_char and in_quotes:
|
|
930
|
+
in_quotes = False
|
|
931
|
+
quote_char = ""
|
|
932
|
+
current_item += char
|
|
933
|
+
elif char == '[' and not in_quotes:
|
|
934
|
+
paren_count += 1
|
|
935
|
+
current_item += char
|
|
936
|
+
elif char == ']' and not in_quotes:
|
|
937
|
+
paren_count -= 1
|
|
938
|
+
current_item += char
|
|
939
|
+
elif char == ',' and not in_quotes and paren_count == 0:
|
|
940
|
+
if current_item.strip():
|
|
941
|
+
items.append(current_item.strip())
|
|
942
|
+
current_item = ""
|
|
943
|
+
else:
|
|
944
|
+
current_item += char
|
|
945
|
+
|
|
946
|
+
if current_item.strip():
|
|
947
|
+
items.append(current_item.strip())
|
|
948
|
+
|
|
949
|
+
# Process each key-value pair
|
|
950
|
+
for item in items:
|
|
951
|
+
item = item.strip()
|
|
952
|
+
if '=>' in item:
|
|
953
|
+
key, value = item.split('=>', 1)
|
|
954
|
+
key = key.strip().strip('"\'')
|
|
955
|
+
value = value.strip()
|
|
956
|
+
|
|
957
|
+
# Check if value is an array
|
|
958
|
+
if value.startswith('[') and value.endswith(']'):
|
|
959
|
+
# Array of state variables
|
|
960
|
+
array_content = value[1:-1].strip()
|
|
961
|
+
state_keys = parse_state_array(array_content)
|
|
962
|
+
result[key] = state_keys
|
|
963
|
+
else:
|
|
964
|
+
# Single state variable
|
|
965
|
+
if value.startswith('$'):
|
|
966
|
+
state_key = value[1:]
|
|
967
|
+
else:
|
|
968
|
+
state_key = value
|
|
969
|
+
result[key] = [state_key]
|
|
970
|
+
|
|
971
|
+
return f'${{this.__subscribe({json.dumps(result)})}}'
|
|
972
|
+
|
|
973
|
+
# Apply the new subscribe directive processing
|
|
974
|
+
processed_line = re.sub(r'@subscribe\s*\([^)]*\)', replace_subscribe_directive, processed_line, flags=re.IGNORECASE)
|
|
975
|
+
|
|
976
|
+
# Handle @wrap/@wrapAttr/@wrapattr directive
|
|
977
|
+
def replace_wrap_directive(match):
|
|
978
|
+
return '${this.wrapattr()}'
|
|
979
|
+
|
|
980
|
+
# Only match @wrap directives that are in HTML tag attributes (not in text content)
|
|
981
|
+
# Look for patterns like: <tag @wrap class="..."> or <tag @wrap>
|
|
982
|
+
processed_line = re.sub(r'<([^>]*?)\s@(?:wrap|wrapAttr|wrapattr)\s*(?:\([^)]*\))?\s*([^>]*?)>', r'<\1 \2 ${this.wrapattr()}>', processed_line, flags=re.IGNORECASE)
|
|
983
|
+
|
|
984
|
+
# Safety: some earlier/legacy passes may leave a stray 'Attr()' token
|
|
985
|
+
# immediately before the inserted ${this.wrapattr()} (e.g. "Attr() ${this.wrapattr()}").
|
|
986
|
+
# Remove leftover standalone "Attr()" occurrences to avoid duplicated output.
|
|
987
|
+
try:
|
|
988
|
+
processed_line = re.sub(r"\bAttr\(\)\s*", '', processed_line)
|
|
989
|
+
except Exception:
|
|
990
|
+
pass
|
|
991
|
+
|
|
992
|
+
# Merge multiple on-yield-attr attributes into one
|
|
993
|
+
def merge_yield_attr_attributes(line):
|
|
994
|
+
"""Merge multiple on-yield-attr attributes into a single one"""
|
|
995
|
+
import re
|
|
996
|
+
|
|
997
|
+
# Find all on-yield-attr attributes
|
|
998
|
+
yield_attr_pattern = r'on-yield-attr="([^"]*)"'
|
|
999
|
+
matches = list(re.finditer(yield_attr_pattern, line))
|
|
1000
|
+
|
|
1001
|
+
if len(matches) <= 1:
|
|
1002
|
+
return line
|
|
1003
|
+
|
|
1004
|
+
# Collect all attribute mappings
|
|
1005
|
+
all_attrs = []
|
|
1006
|
+
for match in matches:
|
|
1007
|
+
attrs = match.group(1).split(',')
|
|
1008
|
+
all_attrs.extend([attr.strip() for attr in attrs if attr.strip()])
|
|
1009
|
+
|
|
1010
|
+
# Remove duplicates while preserving order
|
|
1011
|
+
seen = set()
|
|
1012
|
+
unique_attrs = []
|
|
1013
|
+
for attr in all_attrs:
|
|
1014
|
+
if attr not in seen:
|
|
1015
|
+
seen.add(attr)
|
|
1016
|
+
unique_attrs.append(attr)
|
|
1017
|
+
|
|
1018
|
+
# Replace all on-yield-attr with single one
|
|
1019
|
+
result = line
|
|
1020
|
+
for match in reversed(matches): # Process in reverse order
|
|
1021
|
+
result = result[:match.start()] + '' + result[match.end():]
|
|
1022
|
+
|
|
1023
|
+
# Add single merged on-yield-attr
|
|
1024
|
+
merged_attr = f'on-yield-attr="{",".join(unique_attrs)}"'
|
|
1025
|
+
insert_pos = result.find('>')
|
|
1026
|
+
if insert_pos != -1:
|
|
1027
|
+
result = result[:insert_pos] + ' ' + merged_attr + result[insert_pos:]
|
|
1028
|
+
|
|
1029
|
+
return result
|
|
1030
|
+
|
|
1031
|
+
processed_line = merge_yield_attr_attributes(processed_line)
|
|
1032
|
+
|
|
1033
|
+
# Handle @viewId directive
|
|
1034
|
+
processed_line = re.sub(r'@viewId', "${" + JS_FUNCTION_PREFIX + ".generateViewId()}", processed_line)
|
|
1035
|
+
|
|
1036
|
+
# Handle {!! ... !!} (unescaped output)
|
|
1037
|
+
def replace_unescaped(match):
|
|
1038
|
+
expr = match.group(1).strip()
|
|
1039
|
+
js_expr = php_to_js(expr)
|
|
1040
|
+
|
|
1041
|
+
# Check if we're inside an HTML tag (not inside attribute value quotes)
|
|
1042
|
+
# Look for pattern: < ... {!! ... !!} ... > where {!! !!} is not inside "..." or '...'
|
|
1043
|
+
full_line = processed_line
|
|
1044
|
+
pos = match.start()
|
|
1045
|
+
|
|
1046
|
+
# Find nearest < before pos
|
|
1047
|
+
tag_start = full_line.rfind('<', 0, pos)
|
|
1048
|
+
if tag_start != -1:
|
|
1049
|
+
# Find nearest > after pos
|
|
1050
|
+
tag_end = full_line.find('>', pos)
|
|
1051
|
+
if tag_end != -1:
|
|
1052
|
+
# Check if there's a closing > between tag_start and pos
|
|
1053
|
+
intermediate_close = full_line.rfind('>', tag_start, pos)
|
|
1054
|
+
if intermediate_close == -1:
|
|
1055
|
+
# We might be inside a tag - check if we're inside quotes
|
|
1056
|
+
tag_content = full_line[tag_start:pos]
|
|
1057
|
+
# Count unescaped quotes
|
|
1058
|
+
in_double = tag_content.count('"') % 2 == 1
|
|
1059
|
+
in_single = tag_content.count("'") % 2 == 1
|
|
1060
|
+
|
|
1061
|
+
if not in_double and not in_single:
|
|
1062
|
+
# Inside tag, outside quotes - use simple interpolation
|
|
1063
|
+
return '${' + js_expr + '}'
|
|
1064
|
+
|
|
1065
|
+
return '${' + js_expr + '}'
|
|
1066
|
+
processed_line = re.sub(r'{\!!\s*(.*?)\s*!!}', replace_unescaped, processed_line)
|
|
1067
|
+
|
|
1068
|
+
# Handle {{ ... }} (escaped output)
|
|
1069
|
+
def replace_echo(match):
|
|
1070
|
+
expr = match.group(1).strip()
|
|
1071
|
+
js_expr = php_to_js(expr)
|
|
1072
|
+
|
|
1073
|
+
# Check if we're inside an HTML tag (not inside attribute value quotes)
|
|
1074
|
+
full_line = processed_line
|
|
1075
|
+
pos = match.start()
|
|
1076
|
+
|
|
1077
|
+
# Find nearest < before pos
|
|
1078
|
+
tag_start = full_line.rfind('<', 0, pos)
|
|
1079
|
+
if tag_start != -1:
|
|
1080
|
+
# Find nearest > after pos
|
|
1081
|
+
tag_end = full_line.find('>', pos)
|
|
1082
|
+
if tag_end != -1:
|
|
1083
|
+
# Check if there's a closing > between tag_start and pos
|
|
1084
|
+
intermediate_close = full_line.rfind('>', tag_start, pos)
|
|
1085
|
+
if intermediate_close == -1:
|
|
1086
|
+
# We might be inside a tag - check if we're inside quotes
|
|
1087
|
+
tag_content = full_line[tag_start:pos]
|
|
1088
|
+
# Count unescaped quotes to determine if we're in a string
|
|
1089
|
+
in_double = tag_content.count('"') % 2 == 1
|
|
1090
|
+
in_single = tag_content.count("'") % 2 == 1
|
|
1091
|
+
|
|
1092
|
+
if not in_double and not in_single:
|
|
1093
|
+
# Inside tag, outside attribute value quotes
|
|
1094
|
+
# Use simple escaped interpolation
|
|
1095
|
+
return "${" + APP_VIEW_NAMESPACE + ".escString(" + js_expr + ")}"
|
|
1096
|
+
|
|
1097
|
+
# Check if this is a complex structure (array/object) that shouldn't be escaped
|
|
1098
|
+
if self._is_complex_structure(js_expr):
|
|
1099
|
+
return "${" + js_expr + "}"
|
|
1100
|
+
else:
|
|
1101
|
+
return "${" + JS_FUNCTION_PREFIX + ".escString(" + js_expr + ")}"
|
|
1102
|
+
processed_line = re.sub(r'{{\s*(.*?)\s*}}', replace_echo, processed_line)
|
|
1103
|
+
|
|
1104
|
+
# Handle { ... } (simple variable output)
|
|
1105
|
+
def replace_simple_var(match):
|
|
1106
|
+
expr = match.group(1).strip()
|
|
1107
|
+
js_expr = php_to_js(expr)
|
|
1108
|
+
return "${" + js_expr + "}"
|
|
1109
|
+
processed_line = re.sub(r'{\s*\$(\w+)\s*}', replace_simple_var, processed_line)
|
|
1110
|
+
|
|
1111
|
+
# Handle {{ $var }} syntax - convert to ${App.View.escString(var)}
|
|
1112
|
+
def replace_php_variable(match):
|
|
1113
|
+
var_name = match.group(1).strip()
|
|
1114
|
+
# Remove $ prefix if present
|
|
1115
|
+
if var_name.startswith('$'):
|
|
1116
|
+
var_name = var_name[1:]
|
|
1117
|
+
return f'${{{APP_VIEW_NAMESPACE}.escString({var_name})}}'
|
|
1118
|
+
|
|
1119
|
+
processed_line = re.sub(r'\{\{\s*\$(\w+)\s*\}\}', replace_php_variable, processed_line)
|
|
1120
|
+
|
|
1121
|
+
# Handle @useState directive - remove from template (already processed in main_compiler.py)
|
|
1122
|
+
processed_line = re.sub(r'@useState\s*\([^)]*\)', '', processed_line, flags=re.IGNORECASE)
|
|
1123
|
+
|
|
1124
|
+
return processed_line
|
|
1125
|
+
|
|
1126
|
+
def _parse_wrap_expression(self, expression):
|
|
1127
|
+
"""Parse @wrap($tag, $attributes) expression"""
|
|
1128
|
+
# Find comma separator
|
|
1129
|
+
comma_pos = -1
|
|
1130
|
+
in_quote = False
|
|
1131
|
+
quote_char = None
|
|
1132
|
+
|
|
1133
|
+
for i in range(len(expression)):
|
|
1134
|
+
char = expression[i]
|
|
1135
|
+
|
|
1136
|
+
if (char == '"' or char == "'") and (i == 0 or expression[i-1] != '\\'):
|
|
1137
|
+
if not in_quote:
|
|
1138
|
+
in_quote = True
|
|
1139
|
+
quote_char = char
|
|
1140
|
+
elif char == quote_char:
|
|
1141
|
+
in_quote = False
|
|
1142
|
+
quote_char = None
|
|
1143
|
+
|
|
1144
|
+
if not in_quote and char == ',':
|
|
1145
|
+
comma_pos = i
|
|
1146
|
+
break
|
|
1147
|
+
|
|
1148
|
+
if comma_pos == -1:
|
|
1149
|
+
# Only tag, no attributes
|
|
1150
|
+
tag = expression.strip().strip('\'"')
|
|
1151
|
+
return {'tag': tag, 'attributes': {}}
|
|
1152
|
+
|
|
1153
|
+
# Both tag and attributes
|
|
1154
|
+
tag_part = expression[:comma_pos].strip().strip('\'"')
|
|
1155
|
+
attributes_part = expression[comma_pos + 1:].strip()
|
|
1156
|
+
|
|
1157
|
+
return {'tag': tag_part, 'attributes': self._parse_wrap_attributes(attributes_part)}
|
|
1158
|
+
|
|
1159
|
+
def _parse_wrap_attributes(self, attributes_str):
|
|
1160
|
+
"""Parse attributes array from string"""
|
|
1161
|
+
attributes_str = attributes_str.strip()
|
|
1162
|
+
|
|
1163
|
+
# Remove brackets
|
|
1164
|
+
if attributes_str.startswith('[') and attributes_str.endswith(']'):
|
|
1165
|
+
attributes_str = attributes_str[1:-1]
|
|
1166
|
+
|
|
1167
|
+
if not attributes_str:
|
|
1168
|
+
return {}
|
|
1169
|
+
|
|
1170
|
+
# Use regex to parse key-value pairs
|
|
1171
|
+
attributes = {}
|
|
1172
|
+
|
|
1173
|
+
# Pattern to match 'key' => 'value' or 'key' => value
|
|
1174
|
+
pattern = r"['\"]?([^'\"]+)['\"]?\s*=>\s*(.*?)(?=,\s*['\"]?[^'\"]+['\"]?\s*=>|$)"
|
|
1175
|
+
matches = re.findall(pattern, attributes_str)
|
|
1176
|
+
|
|
1177
|
+
for key, value in matches:
|
|
1178
|
+
key = key.strip()
|
|
1179
|
+
value = value.strip().strip('\'"')
|
|
1180
|
+
|
|
1181
|
+
# Handle follow parameter specially
|
|
1182
|
+
if key == 'follow':
|
|
1183
|
+
if value == 'false':
|
|
1184
|
+
attributes[key] = False
|
|
1185
|
+
elif value.startswith('[') and value.endswith(']'):
|
|
1186
|
+
# Array of variables
|
|
1187
|
+
array_content = value[1:-1]
|
|
1188
|
+
variables = [v.strip().strip('\'"') for v in array_content.split(',')]
|
|
1189
|
+
attributes[key] = variables
|
|
1190
|
+
else:
|
|
1191
|
+
# Single variable
|
|
1192
|
+
attributes[key] = value
|
|
1193
|
+
elif key == 'subscribe':
|
|
1194
|
+
# Handle subscribe parameter similar to follow, with boolean support
|
|
1195
|
+
if value == 'false':
|
|
1196
|
+
attributes[key] = False
|
|
1197
|
+
elif value == 'true':
|
|
1198
|
+
attributes[key] = True
|
|
1199
|
+
elif value.startswith('[') and value.endswith(']'):
|
|
1200
|
+
array_content = value[1:-1]
|
|
1201
|
+
variables = [v.strip().strip('\'"') for v in array_content.split(',')]
|
|
1202
|
+
attributes[key] = variables
|
|
1203
|
+
else:
|
|
1204
|
+
attributes[key] = value
|
|
1205
|
+
else:
|
|
1206
|
+
attributes[key] = value
|
|
1207
|
+
|
|
1208
|
+
return attributes
|
|
1209
|
+
|
|
1210
|
+
def _process_subscribe_value(self, subscribe_str):
|
|
1211
|
+
"""Process subscribe value to extract variable names
|
|
1212
|
+
Input: "[$statekey]" or "[$user, $posts]" or "false" or "$key"
|
|
1213
|
+
Output: ["statekey"] or ["user", "posts"] or False or ["key"]
|
|
1214
|
+
"""
|
|
1215
|
+
subscribe_str = str(subscribe_str).strip()
|
|
1216
|
+
|
|
1217
|
+
# Strip outer quotes if present
|
|
1218
|
+
if (subscribe_str.startswith('"') and subscribe_str.endswith('"')) or \
|
|
1219
|
+
(subscribe_str.startswith("'") and subscribe_str.endswith("'")):
|
|
1220
|
+
subscribe_str = subscribe_str[1:-1].strip()
|
|
1221
|
+
|
|
1222
|
+
# Handle boolean values
|
|
1223
|
+
if subscribe_str.lower() == 'false' or subscribe_str == 'False':
|
|
1224
|
+
return False
|
|
1225
|
+
if subscribe_str.lower() == 'true' or subscribe_str == 'True':
|
|
1226
|
+
return True
|
|
1227
|
+
|
|
1228
|
+
# Handle array syntax: [$var1, $var2, ...] or ["var1", "var2", ...]
|
|
1229
|
+
if subscribe_str.startswith('[') and subscribe_str.endswith(']'):
|
|
1230
|
+
# Remove brackets
|
|
1231
|
+
inner = subscribe_str[1:-1].strip()
|
|
1232
|
+
if not inner:
|
|
1233
|
+
return []
|
|
1234
|
+
|
|
1235
|
+
# Split by comma and extract variable names
|
|
1236
|
+
variables = []
|
|
1237
|
+
for var in inner.split(','):
|
|
1238
|
+
var = var.strip()
|
|
1239
|
+
# Remove quotes if present
|
|
1240
|
+
if (var.startswith('"') and var.endswith('"')) or \
|
|
1241
|
+
(var.startswith("'") and var.endswith("'")):
|
|
1242
|
+
var = var[1:-1].strip()
|
|
1243
|
+
# Remove $ if present
|
|
1244
|
+
var = var.lstrip('$')
|
|
1245
|
+
if var:
|
|
1246
|
+
variables.append(var)
|
|
1247
|
+
return variables
|
|
1248
|
+
|
|
1249
|
+
# Handle single variable: $var
|
|
1250
|
+
if subscribe_str.startswith('$'):
|
|
1251
|
+
return [subscribe_str[1:]]
|
|
1252
|
+
|
|
1253
|
+
# Already processed or literal
|
|
1254
|
+
return subscribe_str
|
|
1255
|
+
|
|
1256
|
+
def _parse_template_parameters(self, params_str):
|
|
1257
|
+
"""Parse template parameters from various formats:
|
|
1258
|
+
- Positional: $tag = '...', $subscribe = [...], $attr1 = '...', ...
|
|
1259
|
+
- Named: tag: '...', subscribe: [...], attr1: '...', ...
|
|
1260
|
+
- Array: ['tag' => '...', 'subscribe' => [...], ...]
|
|
1261
|
+
- First param as tag: 'section', $subscribe = [...]
|
|
1262
|
+
"""
|
|
1263
|
+
params_str = params_str.strip()
|
|
1264
|
+
|
|
1265
|
+
# Check if it's array syntax
|
|
1266
|
+
if params_str.startswith('[') and params_str.endswith(']'):
|
|
1267
|
+
return self._parse_wrap_attributes(params_str)
|
|
1268
|
+
|
|
1269
|
+
# Check if it's named parameter syntax (contains colons)
|
|
1270
|
+
if self._is_named_parameter_syntax(params_str):
|
|
1271
|
+
return self._parse_named_parameters(params_str)
|
|
1272
|
+
|
|
1273
|
+
# Parse as positional parameters with defaults
|
|
1274
|
+
return self._parse_positional_parameters(params_str)
|
|
1275
|
+
|
|
1276
|
+
def _is_named_parameter_syntax(self, params_str):
|
|
1277
|
+
"""Check if expression uses named parameter syntax (key: value)"""
|
|
1278
|
+
in_quote = False
|
|
1279
|
+
quote_char = None
|
|
1280
|
+
bracket_depth = 0
|
|
1281
|
+
|
|
1282
|
+
for i, char in enumerate(params_str):
|
|
1283
|
+
# Handle quotes
|
|
1284
|
+
if char in ['"', "'"] and (i == 0 or params_str[i-1] != '\\'):
|
|
1285
|
+
if not in_quote:
|
|
1286
|
+
in_quote = True
|
|
1287
|
+
quote_char = char
|
|
1288
|
+
elif char == quote_char:
|
|
1289
|
+
in_quote = False
|
|
1290
|
+
quote_char = None
|
|
1291
|
+
|
|
1292
|
+
# Handle brackets
|
|
1293
|
+
if not in_quote:
|
|
1294
|
+
if char == '[':
|
|
1295
|
+
bracket_depth += 1
|
|
1296
|
+
elif char == ']':
|
|
1297
|
+
bracket_depth -= 1
|
|
1298
|
+
|
|
1299
|
+
# Check for colon (not inside quotes or brackets, not part of ::)
|
|
1300
|
+
if not in_quote and bracket_depth == 0 and char == ':':
|
|
1301
|
+
if (i + 1 >= len(params_str) or params_str[i + 1] != ':') and \
|
|
1302
|
+
(i == 0 or params_str[i - 1] != ':'):
|
|
1303
|
+
return True
|
|
1304
|
+
|
|
1305
|
+
return False
|
|
1306
|
+
|
|
1307
|
+
def _parse_named_parameters(self, params_str):
|
|
1308
|
+
"""Parse named parameters: key: value, key2: value2, ..."""
|
|
1309
|
+
return self._parse_key_value_pairs(params_str, ':')
|
|
1310
|
+
|
|
1311
|
+
def _parse_positional_parameters(self, params_str):
|
|
1312
|
+
"""Parse positional parameters: $tag = '...', $subscribe = [...], ..."""
|
|
1313
|
+
attributes = {}
|
|
1314
|
+
parts = self._split_params_by_comma(params_str)
|
|
1315
|
+
|
|
1316
|
+
for part in parts:
|
|
1317
|
+
part = part.strip()
|
|
1318
|
+
|
|
1319
|
+
# Check if it's an assignment: $varName = value or varName = value
|
|
1320
|
+
match = re.match(r'^\s*\$?(\w+)\s*=\s*(.+)$', part, re.DOTALL)
|
|
1321
|
+
if match:
|
|
1322
|
+
key = match.group(1)
|
|
1323
|
+
value = match.group(2).strip()
|
|
1324
|
+
attributes[key] = value
|
|
1325
|
+
else:
|
|
1326
|
+
# If no assignment, treat as 'tag' parameter
|
|
1327
|
+
if 'tag' not in attributes:
|
|
1328
|
+
# Remove quotes if present
|
|
1329
|
+
attributes['tag'] = part.strip('\'"')
|
|
1330
|
+
|
|
1331
|
+
return attributes
|
|
1332
|
+
|
|
1333
|
+
def _parse_key_value_pairs(self, params_str, separator):
|
|
1334
|
+
"""Parse key-value pairs with given separator (=> or :)"""
|
|
1335
|
+
attributes = {}
|
|
1336
|
+
parts = self._split_params_by_comma(params_str)
|
|
1337
|
+
|
|
1338
|
+
for part in parts:
|
|
1339
|
+
part = part.strip()
|
|
1340
|
+
|
|
1341
|
+
# Find separator position (not inside quotes or brackets)
|
|
1342
|
+
sep_pos = self._find_separator_position(part, separator)
|
|
1343
|
+
|
|
1344
|
+
if sep_pos is not None:
|
|
1345
|
+
key = part[:sep_pos].strip().strip('\'"').lstrip('$')
|
|
1346
|
+
value = part[sep_pos + len(separator):].strip()
|
|
1347
|
+
attributes[key] = value
|
|
1348
|
+
|
|
1349
|
+
return attributes
|
|
1350
|
+
|
|
1351
|
+
def _find_separator_position(self, expression, separator):
|
|
1352
|
+
"""Find separator position outside quotes and brackets"""
|
|
1353
|
+
in_quote = False
|
|
1354
|
+
quote_char = None
|
|
1355
|
+
bracket_depth = 0
|
|
1356
|
+
sep_len = len(separator)
|
|
1357
|
+
|
|
1358
|
+
for i, char in enumerate(expression):
|
|
1359
|
+
# Handle quotes
|
|
1360
|
+
if char in ['"', "'"] and (i == 0 or expression[i-1] != '\\'):
|
|
1361
|
+
if not in_quote:
|
|
1362
|
+
in_quote = True
|
|
1363
|
+
quote_char = char
|
|
1364
|
+
elif char == quote_char:
|
|
1365
|
+
in_quote = False
|
|
1366
|
+
quote_char = None
|
|
1367
|
+
|
|
1368
|
+
# Handle brackets
|
|
1369
|
+
if not in_quote:
|
|
1370
|
+
if char == '[':
|
|
1371
|
+
bracket_depth += 1
|
|
1372
|
+
elif char == ']':
|
|
1373
|
+
bracket_depth -= 1
|
|
1374
|
+
|
|
1375
|
+
# Check for separator
|
|
1376
|
+
if not in_quote and bracket_depth == 0:
|
|
1377
|
+
if expression[i:i+sep_len] == separator:
|
|
1378
|
+
# For ':', make sure it's not '::'
|
|
1379
|
+
if separator == ':':
|
|
1380
|
+
not_double_colon = (i + 1 >= len(expression) or expression[i + 1] != ':') and \
|
|
1381
|
+
(i == 0 or expression[i - 1] != ':')
|
|
1382
|
+
if not_double_colon:
|
|
1383
|
+
return i
|
|
1384
|
+
else:
|
|
1385
|
+
return i
|
|
1386
|
+
|
|
1387
|
+
return None
|
|
1388
|
+
|
|
1389
|
+
def _split_params_by_comma(self, expression):
|
|
1390
|
+
"""Split expression by comma (respecting quotes, brackets, and parentheses)"""
|
|
1391
|
+
parts = []
|
|
1392
|
+
current = ''
|
|
1393
|
+
in_quote = False
|
|
1394
|
+
quote_char = None
|
|
1395
|
+
bracket_depth = 0
|
|
1396
|
+
paren_depth = 0
|
|
1397
|
+
|
|
1398
|
+
for i, char in enumerate(expression):
|
|
1399
|
+
# Handle quotes
|
|
1400
|
+
if char in ['"', "'"] and (i == 0 or expression[i-1] != '\\'):
|
|
1401
|
+
if not in_quote:
|
|
1402
|
+
in_quote = True
|
|
1403
|
+
quote_char = char
|
|
1404
|
+
elif char == quote_char:
|
|
1405
|
+
in_quote = False
|
|
1406
|
+
quote_char = None
|
|
1407
|
+
|
|
1408
|
+
# Handle brackets and parentheses
|
|
1409
|
+
if not in_quote:
|
|
1410
|
+
if char == '[':
|
|
1411
|
+
bracket_depth += 1
|
|
1412
|
+
elif char == ']':
|
|
1413
|
+
bracket_depth -= 1
|
|
1414
|
+
elif char == '(':
|
|
1415
|
+
paren_depth += 1
|
|
1416
|
+
elif char == ')':
|
|
1417
|
+
paren_depth -= 1
|
|
1418
|
+
|
|
1419
|
+
# Split on comma only if not inside quotes, brackets, or parentheses
|
|
1420
|
+
if not in_quote and bracket_depth == 0 and paren_depth == 0 and char == ',':
|
|
1421
|
+
if current.strip():
|
|
1422
|
+
parts.append(current)
|
|
1423
|
+
current = ''
|
|
1424
|
+
else:
|
|
1425
|
+
current += char
|
|
1426
|
+
|
|
1427
|
+
# Add the last part
|
|
1428
|
+
if current.strip():
|
|
1429
|
+
parts.append(current)
|
|
1430
|
+
|
|
1431
|
+
return parts
|
|
1432
|
+
|
|
1433
|
+
def _generate_wrapper_config(self, attributes, tag=None):
|
|
1434
|
+
"""Generate wrapperConfig object"""
|
|
1435
|
+
config_parts = ['enable: true']
|
|
1436
|
+
|
|
1437
|
+
# Always add tag field (null if not provided)
|
|
1438
|
+
if tag:
|
|
1439
|
+
# Strip outer quotes from tag if present
|
|
1440
|
+
tag_value = tag.strip().strip('\'"')
|
|
1441
|
+
config_parts.append(f'tag: "{tag_value}"')
|
|
1442
|
+
else:
|
|
1443
|
+
config_parts.append('tag: null')
|
|
1444
|
+
|
|
1445
|
+
# Handle follow/subscribe parameter (treat 'subscribe' as alias of 'follow')
|
|
1446
|
+
# Prefer explicit 'follow' if present; otherwise use 'subscribe'. If both present and are lists, merge unique.
|
|
1447
|
+
follow_val = None
|
|
1448
|
+
raw_follow = attributes.pop('follow', None)
|
|
1449
|
+
raw_subscribe = attributes.pop('subscribe', None)
|
|
1450
|
+
|
|
1451
|
+
if raw_follow is not None and raw_subscribe is not None:
|
|
1452
|
+
# Both provided: normalize and merge if lists
|
|
1453
|
+
if isinstance(raw_follow, list) and isinstance(raw_subscribe, list):
|
|
1454
|
+
merged = []
|
|
1455
|
+
for item in raw_follow + raw_subscribe:
|
|
1456
|
+
if isinstance(item, str) and item.startswith('$'):
|
|
1457
|
+
key = item[1:]
|
|
1458
|
+
else:
|
|
1459
|
+
key = str(item)
|
|
1460
|
+
if key not in merged:
|
|
1461
|
+
merged.append(key)
|
|
1462
|
+
follow_val = merged
|
|
1463
|
+
else:
|
|
1464
|
+
# If either is boolean false, prefer false; else prefer raw_follow
|
|
1465
|
+
if raw_follow == 'false' or raw_follow is False or raw_subscribe == 'false' or raw_subscribe is False:
|
|
1466
|
+
follow_val = False
|
|
1467
|
+
else:
|
|
1468
|
+
follow_val = raw_follow
|
|
1469
|
+
elif raw_follow is not None:
|
|
1470
|
+
follow_val = raw_follow
|
|
1471
|
+
elif raw_subscribe is not None:
|
|
1472
|
+
follow_val = raw_subscribe
|
|
1473
|
+
|
|
1474
|
+
if follow_val is not None:
|
|
1475
|
+
# Only emit `subscribe` in output; keep backward-compatible interpretation
|
|
1476
|
+
if follow_val == 'false' or follow_val is False:
|
|
1477
|
+
config_parts.append('subscribe: false')
|
|
1478
|
+
elif follow_val == 'true' or follow_val is True:
|
|
1479
|
+
config_parts.append('subscribe: true')
|
|
1480
|
+
elif isinstance(follow_val, str):
|
|
1481
|
+
# Single variable
|
|
1482
|
+
if follow_val.startswith('$'):
|
|
1483
|
+
follow_key = follow_val[1:]
|
|
1484
|
+
else:
|
|
1485
|
+
follow_key = follow_val
|
|
1486
|
+
config_parts.append(f'subscribe: ["{follow_key}"]')
|
|
1487
|
+
elif isinstance(follow_val, list):
|
|
1488
|
+
# Array of variables
|
|
1489
|
+
processed_follow = []
|
|
1490
|
+
for item in follow_val:
|
|
1491
|
+
if isinstance(item, str) and item.startswith('$'):
|
|
1492
|
+
processed_follow.append(f'"{item[1:]}"')
|
|
1493
|
+
else:
|
|
1494
|
+
processed_follow.append(f'"{item}"')
|
|
1495
|
+
config_parts.append(f'subscribe: [{", ".join(processed_follow)}]')
|
|
1496
|
+
|
|
1497
|
+
# Handle other attributes
|
|
1498
|
+
if attributes:
|
|
1499
|
+
# Strip quotes from attribute values for proper JSON conversion
|
|
1500
|
+
cleaned_attrs = {}
|
|
1501
|
+
for key, value in attributes.items():
|
|
1502
|
+
value_str = str(value).strip()
|
|
1503
|
+
# If value is a simple quoted string, strip the quotes
|
|
1504
|
+
if (value_str.startswith('"') and value_str.endswith('"')) or \
|
|
1505
|
+
(value_str.startswith("'") and value_str.endswith("'")):
|
|
1506
|
+
cleaned_attrs[key] = value_str[1:-1]
|
|
1507
|
+
else:
|
|
1508
|
+
cleaned_attrs[key] = value
|
|
1509
|
+
|
|
1510
|
+
attrs_js = convert_php_array_to_json(str(cleaned_attrs))
|
|
1511
|
+
attrs_js = re.sub(r'\$(\w+)', r'\1', attrs_js)
|
|
1512
|
+
config_parts.append(f'attributes: {attrs_js}')
|
|
1513
|
+
else:
|
|
1514
|
+
config_parts.append('attributes: {}')
|
|
1515
|
+
|
|
1516
|
+
return f'__WRAPPER_CONFIG__ = {{ {", ".join(config_parts)} }};'
|
|
1517
|
+
|
|
1518
|
+
def _is_complex_structure(self, expr):
|
|
1519
|
+
"""Check if expression is a complex structure (array/object) that shouldn't be escaped"""
|
|
1520
|
+
expr = expr.strip()
|
|
1521
|
+
|
|
1522
|
+
# Check for array syntax
|
|
1523
|
+
if expr.startswith('[') and expr.endswith(']'):
|
|
1524
|
+
return True
|
|
1525
|
+
|
|
1526
|
+
# Check for object syntax
|
|
1527
|
+
if expr.startswith('{') and expr.endswith('}'):
|
|
1528
|
+
return True
|
|
1529
|
+
|
|
1530
|
+
# Check for nested structures (more sophisticated)
|
|
1531
|
+
if '[' in expr and ']' in expr and '=>' in expr:
|
|
1532
|
+
return True
|
|
1533
|
+
|
|
1534
|
+
if '{' in expr and '}' in expr and ':' in expr:
|
|
1535
|
+
return True
|
|
1536
|
+
|
|
1537
|
+
return False
|
|
1538
|
+
|
|
1539
|
+
def process_serverside_directive(self, line):
|
|
1540
|
+
"""Process @serverside/@serverSide directive and aliases"""
|
|
1541
|
+
serverside_aliases = [
|
|
1542
|
+
'@serverside', '@serverSide', '@ssr', '@SSR', '@useSSR', '@useSsr'
|
|
1543
|
+
]
|
|
1544
|
+
|
|
1545
|
+
if any(line.startswith(alias) for alias in serverside_aliases):
|
|
1546
|
+
return 'skip_until_@endserverside'
|
|
1547
|
+
return False
|
|
1548
|
+
|
|
1549
|
+
def process_clientside_directive(self, line):
|
|
1550
|
+
"""Process @clientside/@endclientside directive and aliases"""
|
|
1551
|
+
clientside_aliases = [
|
|
1552
|
+
'@clientside', '@clientSide', '@csr', '@CSR', '@useCSR', '@useCsr'
|
|
1553
|
+
]
|
|
1554
|
+
|
|
1555
|
+
if any(line.startswith(alias) for alias in clientside_aliases):
|
|
1556
|
+
return 'remove_directive_markers_until_@endclientside'
|
|
1557
|
+
return False
|