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.
Files changed (119) hide show
  1. package/README.md +1 -1
  2. package/bin/onejs-build.js +32 -0
  3. package/package.json +11 -3
  4. package/scripts/README-template-compiler.md +133 -0
  5. package/scripts/README.md +61 -0
  6. package/scripts/__pycache__/build.cpython-314.pyc +0 -0
  7. package/scripts/__pycache__/compile.cpython-313.pyc +0 -0
  8. package/scripts/__pycache__/compile.cpython-314.pyc +0 -0
  9. package/scripts/build.py +573 -0
  10. package/scripts/check-system-errors.php +214 -0
  11. package/scripts/compile.py +101 -0
  12. package/scripts/compiler/README_CONFIG.md +196 -0
  13. package/scripts/compiler/__init__.py +18 -0
  14. package/scripts/compiler/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/scripts/compiler/__pycache__/__init__.cpython-314.pyc +0 -0
  16. package/scripts/compiler/__pycache__/binding_directive_service.cpython-314.pyc +0 -0
  17. package/scripts/compiler/__pycache__/class_binding_handler.cpython-314.pyc +0 -0
  18. package/scripts/compiler/__pycache__/compiler_utils.cpython-313.pyc +0 -0
  19. package/scripts/compiler/__pycache__/compiler_utils.cpython-314.pyc +0 -0
  20. package/scripts/compiler/__pycache__/conditional_handlers.cpython-313.pyc +0 -0
  21. package/scripts/compiler/__pycache__/conditional_handlers.cpython-314.pyc +0 -0
  22. package/scripts/compiler/__pycache__/config.cpython-313.pyc +0 -0
  23. package/scripts/compiler/__pycache__/config.cpython-314.pyc +0 -0
  24. package/scripts/compiler/__pycache__/declaration_tracker.cpython-314.pyc +0 -0
  25. package/scripts/compiler/__pycache__/directive_processors.cpython-313.pyc +0 -0
  26. package/scripts/compiler/__pycache__/directive_processors.cpython-314.pyc +0 -0
  27. package/scripts/compiler/__pycache__/echo_processor.cpython-314.pyc +0 -0
  28. package/scripts/compiler/__pycache__/event_directive_processor.cpython-313.pyc +0 -0
  29. package/scripts/compiler/__pycache__/event_directive_processor.cpython-314.pyc +0 -0
  30. package/scripts/compiler/__pycache__/function_generators.cpython-313.pyc +0 -0
  31. package/scripts/compiler/__pycache__/function_generators.cpython-314.pyc +0 -0
  32. package/scripts/compiler/__pycache__/loop_handlers.cpython-313.pyc +0 -0
  33. package/scripts/compiler/__pycache__/loop_handlers.cpython-314.pyc +0 -0
  34. package/scripts/compiler/__pycache__/main_compiler.cpython-313.pyc +0 -0
  35. package/scripts/compiler/__pycache__/main_compiler.cpython-314.pyc +0 -0
  36. package/scripts/compiler/__pycache__/parsers.cpython-313.pyc +0 -0
  37. package/scripts/compiler/__pycache__/parsers.cpython-314.pyc +0 -0
  38. package/scripts/compiler/__pycache__/php_converter.cpython-313.pyc +0 -0
  39. package/scripts/compiler/__pycache__/php_converter.cpython-314.pyc +0 -0
  40. package/scripts/compiler/__pycache__/php_js_converter.cpython-313.pyc +0 -0
  41. package/scripts/compiler/__pycache__/php_js_converter.cpython-314.pyc +0 -0
  42. package/scripts/compiler/__pycache__/register_parser.cpython-313.pyc +0 -0
  43. package/scripts/compiler/__pycache__/register_parser.cpython-314.pyc +0 -0
  44. package/scripts/compiler/__pycache__/section_handlers.cpython-313.pyc +0 -0
  45. package/scripts/compiler/__pycache__/section_handlers.cpython-314.pyc +0 -0
  46. package/scripts/compiler/__pycache__/show_directive_handler.cpython-314.pyc +0 -0
  47. package/scripts/compiler/__pycache__/style_directive_handler.cpython-314.pyc +0 -0
  48. package/scripts/compiler/__pycache__/template_analyzer.cpython-313.pyc +0 -0
  49. package/scripts/compiler/__pycache__/template_analyzer.cpython-314.pyc +0 -0
  50. package/scripts/compiler/__pycache__/template_processor.cpython-313.pyc +0 -0
  51. package/scripts/compiler/__pycache__/template_processor.cpython-314.pyc +0 -0
  52. package/scripts/compiler/__pycache__/template_processors.cpython-313.pyc +0 -0
  53. package/scripts/compiler/__pycache__/template_processors.cpython-314.pyc +0 -0
  54. package/scripts/compiler/__pycache__/utils.cpython-313.pyc +0 -0
  55. package/scripts/compiler/__pycache__/utils.cpython-314.pyc +0 -0
  56. package/scripts/compiler/__pycache__/wrapper_parser.cpython-313.pyc +0 -0
  57. package/scripts/compiler/__pycache__/wrapper_parser.cpython-314.pyc +0 -0
  58. package/scripts/compiler/binding_directive_service.py +103 -0
  59. package/scripts/compiler/class_binding_handler.py +347 -0
  60. package/scripts/compiler/cli.py +34 -0
  61. package/scripts/compiler/code_generator.py +141 -0
  62. package/scripts/compiler/compiler.config.json +36 -0
  63. package/scripts/compiler/compiler_utils.py +55 -0
  64. package/scripts/compiler/conditional_handlers.py +252 -0
  65. package/scripts/compiler/config.py +107 -0
  66. package/scripts/compiler/declaration_tracker.py +420 -0
  67. package/scripts/compiler/directive_processors.py +603 -0
  68. package/scripts/compiler/echo_processor.py +667 -0
  69. package/scripts/compiler/event_directive_processor.py +1099 -0
  70. package/scripts/compiler/fetch_parser.py +49 -0
  71. package/scripts/compiler/function_generators.py +310 -0
  72. package/scripts/compiler/loop_handlers.py +224 -0
  73. package/scripts/compiler/main_compiler.py +1763 -0
  74. package/scripts/compiler/parsers.py +1418 -0
  75. package/scripts/compiler/php_converter.py +470 -0
  76. package/scripts/compiler/php_js_converter.py +603 -0
  77. package/scripts/compiler/register_parser.py +480 -0
  78. package/scripts/compiler/section_handlers.py +122 -0
  79. package/scripts/compiler/show_directive_handler.py +85 -0
  80. package/scripts/compiler/style_directive_handler.py +169 -0
  81. package/scripts/compiler/template_analyzer.py +162 -0
  82. package/scripts/compiler/template_processor.py +1167 -0
  83. package/scripts/compiler/template_processors.py +1557 -0
  84. package/scripts/compiler/test_compiler.py +69 -0
  85. package/scripts/compiler/utils.py +54 -0
  86. package/scripts/compiler/variables_analyzer.py +135 -0
  87. package/scripts/compiler/view_identifier_generator.py +278 -0
  88. package/scripts/compiler/wrapper_parser.py +78 -0
  89. package/scripts/dev-context.js +311 -0
  90. package/scripts/dev.js +109 -0
  91. package/scripts/generate-assets-order.js +200 -0
  92. package/scripts/migrate-namespace.php +146 -0
  93. package/scripts/node/MIGRATION.md +190 -0
  94. package/scripts/node/README.md +269 -0
  95. package/scripts/node/build.js +208 -0
  96. package/scripts/node/compiler/compiler-utils.js +38 -0
  97. package/scripts/node/compiler/conditional-handlers.js +45 -0
  98. package/scripts/node/compiler/config.js +178 -0
  99. package/scripts/node/compiler/directive-processors.js +51 -0
  100. package/scripts/node/compiler/event-directive-processor.js +182 -0
  101. package/scripts/node/compiler/function-generators.js +239 -0
  102. package/scripts/node/compiler/loop-handlers.js +45 -0
  103. package/scripts/node/compiler/main-compiler.js +236 -0
  104. package/scripts/node/compiler/parsers.js +358 -0
  105. package/scripts/node/compiler/php-converter.js +227 -0
  106. package/scripts/node/compiler/register-parser.js +32 -0
  107. package/scripts/node/compiler/section-handlers.js +46 -0
  108. package/scripts/node/compiler/template-analyzer.js +50 -0
  109. package/scripts/node/compiler/template-processor.js +371 -0
  110. package/scripts/node/compiler/template-processors.js +219 -0
  111. package/scripts/node/compiler/utils.js +203 -0
  112. package/scripts/node/compiler/wrapper-parser.js +25 -0
  113. package/scripts/node/package.json +24 -0
  114. package/scripts/node/test-compiler.js +52 -0
  115. package/scripts/node-run.cjs +28 -0
  116. package/scripts/standardize-directories.php +92 -0
  117. package/templates/view.module.js +2 -0
  118. package/templates/view.tpl-raw.js +13 -0
  119. 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