onelaraveljs 1.0.0 → 1.1.3

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 (121) hide show
  1. package/README.md +1 -1
  2. package/bin/onejs-build.js +32 -0
  3. package/index.js +3 -1
  4. package/package.json +11 -3
  5. package/scripts/README-template-compiler.md +133 -0
  6. package/scripts/README.md +61 -0
  7. package/scripts/__pycache__/build.cpython-314.pyc +0 -0
  8. package/scripts/__pycache__/compile.cpython-313.pyc +0 -0
  9. package/scripts/__pycache__/compile.cpython-314.pyc +0 -0
  10. package/scripts/build.py +574 -0
  11. package/scripts/check-system-errors.php +214 -0
  12. package/scripts/compile.py +101 -0
  13. package/scripts/compiler/README_CONFIG.md +196 -0
  14. package/scripts/compiler/__init__.py +18 -0
  15. package/scripts/compiler/__pycache__/__init__.cpython-313.pyc +0 -0
  16. package/scripts/compiler/__pycache__/__init__.cpython-314.pyc +0 -0
  17. package/scripts/compiler/__pycache__/binding_directive_service.cpython-314.pyc +0 -0
  18. package/scripts/compiler/__pycache__/class_binding_handler.cpython-314.pyc +0 -0
  19. package/scripts/compiler/__pycache__/compiler_utils.cpython-313.pyc +0 -0
  20. package/scripts/compiler/__pycache__/compiler_utils.cpython-314.pyc +0 -0
  21. package/scripts/compiler/__pycache__/conditional_handlers.cpython-313.pyc +0 -0
  22. package/scripts/compiler/__pycache__/conditional_handlers.cpython-314.pyc +0 -0
  23. package/scripts/compiler/__pycache__/config.cpython-313.pyc +0 -0
  24. package/scripts/compiler/__pycache__/config.cpython-314.pyc +0 -0
  25. package/scripts/compiler/__pycache__/declaration_tracker.cpython-314.pyc +0 -0
  26. package/scripts/compiler/__pycache__/directive_processors.cpython-313.pyc +0 -0
  27. package/scripts/compiler/__pycache__/directive_processors.cpython-314.pyc +0 -0
  28. package/scripts/compiler/__pycache__/echo_processor.cpython-314.pyc +0 -0
  29. package/scripts/compiler/__pycache__/event_directive_processor.cpython-313.pyc +0 -0
  30. package/scripts/compiler/__pycache__/event_directive_processor.cpython-314.pyc +0 -0
  31. package/scripts/compiler/__pycache__/function_generators.cpython-313.pyc +0 -0
  32. package/scripts/compiler/__pycache__/function_generators.cpython-314.pyc +0 -0
  33. package/scripts/compiler/__pycache__/loop_handlers.cpython-313.pyc +0 -0
  34. package/scripts/compiler/__pycache__/loop_handlers.cpython-314.pyc +0 -0
  35. package/scripts/compiler/__pycache__/main_compiler.cpython-313.pyc +0 -0
  36. package/scripts/compiler/__pycache__/main_compiler.cpython-314.pyc +0 -0
  37. package/scripts/compiler/__pycache__/parsers.cpython-313.pyc +0 -0
  38. package/scripts/compiler/__pycache__/parsers.cpython-314.pyc +0 -0
  39. package/scripts/compiler/__pycache__/php_converter.cpython-313.pyc +0 -0
  40. package/scripts/compiler/__pycache__/php_converter.cpython-314.pyc +0 -0
  41. package/scripts/compiler/__pycache__/php_js_converter.cpython-313.pyc +0 -0
  42. package/scripts/compiler/__pycache__/php_js_converter.cpython-314.pyc +0 -0
  43. package/scripts/compiler/__pycache__/register_parser.cpython-313.pyc +0 -0
  44. package/scripts/compiler/__pycache__/register_parser.cpython-314.pyc +0 -0
  45. package/scripts/compiler/__pycache__/section_handlers.cpython-313.pyc +0 -0
  46. package/scripts/compiler/__pycache__/section_handlers.cpython-314.pyc +0 -0
  47. package/scripts/compiler/__pycache__/show_directive_handler.cpython-314.pyc +0 -0
  48. package/scripts/compiler/__pycache__/style_directive_handler.cpython-314.pyc +0 -0
  49. package/scripts/compiler/__pycache__/template_analyzer.cpython-313.pyc +0 -0
  50. package/scripts/compiler/__pycache__/template_analyzer.cpython-314.pyc +0 -0
  51. package/scripts/compiler/__pycache__/template_processor.cpython-313.pyc +0 -0
  52. package/scripts/compiler/__pycache__/template_processor.cpython-314.pyc +0 -0
  53. package/scripts/compiler/__pycache__/template_processors.cpython-313.pyc +0 -0
  54. package/scripts/compiler/__pycache__/template_processors.cpython-314.pyc +0 -0
  55. package/scripts/compiler/__pycache__/utils.cpython-313.pyc +0 -0
  56. package/scripts/compiler/__pycache__/utils.cpython-314.pyc +0 -0
  57. package/scripts/compiler/__pycache__/wrapper_parser.cpython-313.pyc +0 -0
  58. package/scripts/compiler/__pycache__/wrapper_parser.cpython-314.pyc +0 -0
  59. package/scripts/compiler/binding_directive_service.py +103 -0
  60. package/scripts/compiler/class_binding_handler.py +347 -0
  61. package/scripts/compiler/cli.py +34 -0
  62. package/scripts/compiler/code_generator.py +141 -0
  63. package/scripts/compiler/compiler.config.json +36 -0
  64. package/scripts/compiler/compiler_utils.py +55 -0
  65. package/scripts/compiler/conditional_handlers.py +252 -0
  66. package/scripts/compiler/config.py +107 -0
  67. package/scripts/compiler/declaration_tracker.py +420 -0
  68. package/scripts/compiler/directive_processors.py +603 -0
  69. package/scripts/compiler/echo_processor.py +667 -0
  70. package/scripts/compiler/event_directive_processor.py +1099 -0
  71. package/scripts/compiler/fetch_parser.py +49 -0
  72. package/scripts/compiler/function_generators.py +310 -0
  73. package/scripts/compiler/loop_handlers.py +224 -0
  74. package/scripts/compiler/main_compiler.py +1763 -0
  75. package/scripts/compiler/parsers.py +1418 -0
  76. package/scripts/compiler/php_converter.py +470 -0
  77. package/scripts/compiler/php_js_converter.py +603 -0
  78. package/scripts/compiler/register_parser.py +480 -0
  79. package/scripts/compiler/section_handlers.py +122 -0
  80. package/scripts/compiler/show_directive_handler.py +85 -0
  81. package/scripts/compiler/style_directive_handler.py +169 -0
  82. package/scripts/compiler/template_analyzer.py +162 -0
  83. package/scripts/compiler/template_processor.py +1167 -0
  84. package/scripts/compiler/template_processors.py +1557 -0
  85. package/scripts/compiler/test_compiler.py +69 -0
  86. package/scripts/compiler/utils.py +54 -0
  87. package/scripts/compiler/variables_analyzer.py +135 -0
  88. package/scripts/compiler/view_identifier_generator.py +278 -0
  89. package/scripts/compiler/wrapper_parser.py +78 -0
  90. package/scripts/dev-context.js +311 -0
  91. package/scripts/dev.js +109 -0
  92. package/scripts/generate-assets-order.js +208 -0
  93. package/scripts/migrate-namespace.php +146 -0
  94. package/scripts/node/MIGRATION.md +190 -0
  95. package/scripts/node/README.md +269 -0
  96. package/scripts/node/build.js +208 -0
  97. package/scripts/node/compiler/compiler-utils.js +38 -0
  98. package/scripts/node/compiler/conditional-handlers.js +45 -0
  99. package/scripts/node/compiler/config.js +178 -0
  100. package/scripts/node/compiler/directive-processors.js +51 -0
  101. package/scripts/node/compiler/event-directive-processor.js +182 -0
  102. package/scripts/node/compiler/function-generators.js +239 -0
  103. package/scripts/node/compiler/loop-handlers.js +45 -0
  104. package/scripts/node/compiler/main-compiler.js +236 -0
  105. package/scripts/node/compiler/parsers.js +358 -0
  106. package/scripts/node/compiler/php-converter.js +227 -0
  107. package/scripts/node/compiler/register-parser.js +32 -0
  108. package/scripts/node/compiler/section-handlers.js +46 -0
  109. package/scripts/node/compiler/template-analyzer.js +50 -0
  110. package/scripts/node/compiler/template-processor.js +371 -0
  111. package/scripts/node/compiler/template-processors.js +219 -0
  112. package/scripts/node/compiler/utils.js +203 -0
  113. package/scripts/node/compiler/wrapper-parser.js +25 -0
  114. package/scripts/node/package.json +24 -0
  115. package/scripts/node/test-compiler.js +52 -0
  116. package/scripts/node-run.cjs +28 -0
  117. package/scripts/standardize-directories.php +92 -0
  118. package/src/core/ViewManager.js +4 -4
  119. package/templates/view.module.js +2 -0
  120. package/templates/view.tpl-raw.js +13 -0
  121. package/templates/wraper.js +71 -0
@@ -0,0 +1,667 @@
1
+ """
2
+ Echo Processor - Xử lý thông minh {{ }} và {!! !!}
3
+ Phân biệt context: content, attribute value, tag body
4
+ """
5
+
6
+ import re
7
+ from php_js_converter import php_to_js_advanced
8
+ from config import APP_VIEW_NAMESPACE
9
+
10
+ class EchoProcessor:
11
+ def __init__(self, state_variables=None):
12
+ """
13
+ Initialize echo processor with state variables
14
+
15
+ Args:
16
+ state_variables (set): Set of variable names from useState, let, const
17
+ """
18
+ self.state_variables = state_variables or set()
19
+ self.reactive_counter = 0
20
+
21
+ def process_echo_expressions(self, template_content):
22
+ """
23
+ Main entry point - process all {{ }} and {!! !!} expressions
24
+
25
+ Returns:
26
+ str: Processed template content
27
+ """
28
+ # First, protect expressions inside @verbatim placeholders
29
+ # They should not be processed
30
+
31
+ # Process in order:
32
+ # 1. Attributes with {{ }} or {!! !!}
33
+ template_content = self._process_echo_in_attributes(template_content)
34
+
35
+ # 2. Content {{ }} and {!! !!}
36
+ template_content = self._process_echo_in_content(template_content)
37
+
38
+ return template_content
39
+
40
+ def _process_echo_in_attributes(self, content):
41
+ """
42
+ Process {{ }} and {!! !!} inside HTML attribute values
43
+ Also process @checked(...) and @selected(...) directives
44
+ Merge with @attr if needed
45
+ """
46
+ # Instead of simple regex, use manual parsing to handle complex attributes
47
+ # Pattern to find opening tags
48
+ result = []
49
+ pos = 0
50
+
51
+ while pos < len(content):
52
+ # Find next <
53
+ lt_pos = content.find('<', pos)
54
+ if lt_pos == -1:
55
+ result.append(content[pos:])
56
+ break
57
+
58
+ # Add content before <
59
+ result.append(content[pos:lt_pos])
60
+
61
+ # Check if this is a tag
62
+ if lt_pos + 1 >= len(content):
63
+ result.append(content[lt_pos:])
64
+ break
65
+
66
+ # Get tag name
67
+ tag_start = lt_pos + 1
68
+ tag_name_match = re.match(r'([a-zA-Z][a-zA-Z0-9]*)', content[tag_start:])
69
+
70
+ if not tag_name_match:
71
+ # Not a valid tag, skip
72
+ result.append('<')
73
+ pos = lt_pos + 1
74
+ continue
75
+
76
+ tag_name = tag_name_match.group(1)
77
+ attr_start = tag_start + len(tag_name)
78
+
79
+ # Find the end of tag, handling nested brackets and quotes
80
+ gt_pos = self._find_tag_end(content, attr_start)
81
+
82
+ if gt_pos == -1:
83
+ # No closing >, treat as text
84
+ result.append(content[lt_pos:])
85
+ break
86
+
87
+ # Check if self-closing
88
+ self_closing = ''
89
+ if gt_pos > 0 and content[gt_pos - 1] == '/':
90
+ self_closing = '/'
91
+ actual_end = gt_pos - 1
92
+ else:
93
+ actual_end = gt_pos
94
+
95
+ # Extract attributes
96
+ attributes_str = content[attr_start:actual_end]
97
+
98
+ # Process this tag
99
+ processed_tag = self._process_single_tag(tag_name, attributes_str, self_closing)
100
+ result.append(processed_tag)
101
+
102
+ pos = gt_pos + 1
103
+
104
+ return ''.join(result)
105
+
106
+ def _find_tag_end(self, content, start_pos):
107
+ """
108
+ Find the closing > of a tag, handling nested brackets, quotes, and arrays
109
+ """
110
+ pos = start_pos
111
+ in_quote = False
112
+ quote_char = None
113
+ paren_depth = 0
114
+ bracket_depth = 0
115
+
116
+ while pos < len(content):
117
+ ch = content[pos]
118
+
119
+ # Handle quotes
120
+ if ch in ('"', "'") and (pos == 0 or content[pos - 1] != '\\'):
121
+ if not in_quote:
122
+ in_quote = True
123
+ quote_char = ch
124
+ elif ch == quote_char:
125
+ in_quote = False
126
+ quote_char = None
127
+
128
+ # Outside quotes
129
+ elif not in_quote:
130
+ if ch == '(':
131
+ paren_depth += 1
132
+ elif ch == ')':
133
+ paren_depth -= 1
134
+ elif ch == '[':
135
+ bracket_depth += 1
136
+ elif ch == ']':
137
+ bracket_depth -= 1
138
+ elif ch == '>' and paren_depth == 0 and bracket_depth == 0:
139
+ return pos
140
+
141
+ pos += 1
142
+
143
+ return -1
144
+
145
+ def _process_single_tag(self, tag_name, attributes_str, self_closing):
146
+ """
147
+ Process a single tag's attributes
148
+ """
149
+ # Process @checked(...) and @selected(...) first
150
+ checked_selected_attrs = {}
151
+
152
+ # Process @checked(...)
153
+ def extract_checked(m):
154
+ expr = m.group(1).strip()
155
+ js_expr = php_to_js_advanced(expr)
156
+ variables = self._extract_variables(expr)
157
+ state_vars_used = variables & self.state_variables
158
+
159
+ if state_vars_used:
160
+ checked_selected_attrs['checked'] = {
161
+ 'expressions': [{'type': 'checked', 'php': expr, 'js': js_expr, 'vars': variables}],
162
+ 'state_vars': list(state_vars_used),
163
+ 'original_value': expr
164
+ }
165
+ return '' # Remove from attributes string
166
+ else:
167
+ # Static evaluation
168
+ return f'${{({js_expr}) ? " checked" : ""}}'
169
+
170
+ attributes_str = re.sub(r'@checked\s*\(\s*(.*?)\s*\)', extract_checked, attributes_str, flags=re.DOTALL)
171
+
172
+ # Process @selected(...)
173
+ def extract_selected(m):
174
+ expr = m.group(1).strip()
175
+ js_expr = php_to_js_advanced(expr)
176
+ variables = self._extract_variables(expr)
177
+ state_vars_used = variables & self.state_variables
178
+
179
+ if state_vars_used:
180
+ checked_selected_attrs['selected'] = {
181
+ 'expressions': [{'type': 'selected', 'php': expr, 'js': js_expr, 'vars': variables}],
182
+ 'state_vars': list(state_vars_used),
183
+ 'original_value': expr
184
+ }
185
+ return '' # Remove from attributes string
186
+ else:
187
+ # Static evaluation
188
+ return f'${{({js_expr}) ? " selected" : ""}}'
189
+
190
+ attributes_str = re.sub(r'@selected\s*\(\s*(.*?)\s*\)', extract_selected, attributes_str, flags=re.DOTALL)
191
+
192
+ # Find all attributes with {{ }} or {!! !!}
193
+ echo_attrs = {}
194
+ has_echo = False or bool(checked_selected_attrs)
195
+
196
+ # Pattern for attr="{{...}}" or attr="{!!...!!}"
197
+ attr_pattern = r'([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*["\']([^"\']*(?:\{\{[^}]*\}\}|{!![^!]*!!})[^"\']*)["\']'
198
+
199
+ def extract_echo_attr(attr_match):
200
+ nonlocal has_echo
201
+ attr_name = attr_match.group(1)
202
+ attr_value = attr_match.group(2)
203
+
204
+ # Check if has {{ }} or {!! !!}
205
+ if '{{' in attr_value or '{!!' in attr_value:
206
+ has_echo = True
207
+
208
+ # Extract all expressions in this attribute value
209
+ expressions = []
210
+ used_vars = set()
211
+
212
+ # Find {{ }} expressions
213
+ for echo_match in re.finditer(r'\{\{([^}]+)\}\}', attr_value):
214
+ expr = echo_match.group(1).strip()
215
+ js_expr = php_to_js_advanced(expr)
216
+ variables = self._extract_variables(expr)
217
+ used_vars.update(variables)
218
+ expressions.append({
219
+ 'type': 'escaped',
220
+ 'php': expr,
221
+ 'js': js_expr,
222
+ 'vars': variables
223
+ })
224
+
225
+ # Find {!! !!} expressions
226
+ for raw_match in re.finditer(r'{!!([^!]+)!!}', attr_value):
227
+ expr = raw_match.group(1).strip()
228
+ js_expr = php_to_js_advanced(expr)
229
+ variables = self._extract_variables(expr)
230
+ used_vars.update(variables)
231
+ expressions.append({
232
+ 'type': 'unescaped',
233
+ 'php': expr,
234
+ 'js': js_expr,
235
+ 'vars': variables
236
+ })
237
+
238
+ # Check if uses state variables
239
+ state_vars_used = used_vars & self.state_variables
240
+
241
+ if state_vars_used:
242
+ # Need reactive handling
243
+ echo_attrs[attr_name] = {
244
+ 'expressions': expressions,
245
+ 'state_vars': list(state_vars_used),
246
+ 'original_value': attr_value
247
+ }
248
+ # Remove this attribute from string
249
+ return ''
250
+ else:
251
+ # Static - process inline
252
+ processed_value = attr_value
253
+ for expr_info in expressions:
254
+ if expr_info['type'] == 'escaped':
255
+ replacement = f"${{{APP_VIEW_NAMESPACE}.escString({expr_info['js']})}}"
256
+ else:
257
+ replacement = f"${{{expr_info['js']}}}"
258
+
259
+ # Replace in original value
260
+ if expr_info['type'] == 'escaped':
261
+ processed_value = processed_value.replace(f"{{{{{expr_info['php']}}}}}", replacement)
262
+ else:
263
+ processed_value = processed_value.replace(f"{{!!{expr_info['php']}!!}}", replacement)
264
+
265
+ return f'{attr_name}="{processed_value}"'
266
+
267
+ return attr_match.group(0)
268
+
269
+ # Process attributes
270
+ new_attributes_str = re.sub(attr_pattern, extract_echo_attr, attributes_str)
271
+
272
+ # Merge checked/selected attrs with echo_attrs
273
+ if checked_selected_attrs:
274
+ echo_attrs.update(checked_selected_attrs)
275
+
276
+ if has_echo and echo_attrs:
277
+ # Check if already has @attr directive
278
+ existing_attr_match = re.search(r'@attr\s*\(', new_attributes_str)
279
+
280
+ if existing_attr_match:
281
+ # Need to merge with existing @attr
282
+ # Extract the existing @attr parameters
283
+ from utils import extract_balanced_parentheses
284
+ start_pos = existing_attr_match.end() - 1
285
+ existing_params, end_pos = extract_balanced_parentheses(new_attributes_str, start_pos)
286
+
287
+ if existing_params is not None:
288
+ # Parse existing @attr parameters and merge
289
+ merged_attrs = self._merge_attr_directives(existing_params, echo_attrs)
290
+
291
+ # Replace the old @attr with merged one
292
+ new_attr_directive = f"${{this.__attr({merged_attrs})}}"
293
+ new_attributes_str = (
294
+ new_attributes_str[:existing_attr_match.start()] +
295
+ new_attr_directive +
296
+ new_attributes_str[end_pos:]
297
+ )
298
+ else:
299
+ # No existing @attr, just add new one
300
+ attr_directive = self._generate_attr_directive(echo_attrs)
301
+ new_attributes_str = new_attributes_str + ' ' + attr_directive
302
+
303
+ # Clean up multiple spaces
304
+ new_attributes_str = re.sub(r'\s+', ' ', new_attributes_str)
305
+ new_attributes_str = new_attributes_str.strip()
306
+
307
+ # Add space before attributes if not empty
308
+ if new_attributes_str:
309
+ new_attributes_str = ' ' + new_attributes_str
310
+
311
+ return f'<{tag_name}{new_attributes_str}{self_closing}>'
312
+
313
+ def _process_echo_in_content(self, content):
314
+ """
315
+ Process {{ }} and {!! !!} in content (not in attributes)
316
+ """
317
+ # Skip if inside attribute values (already processed)
318
+ # This is for content like: <div>{{ $name }}</div>
319
+
320
+ def replace_escaped_echo(match):
321
+ expr = match.group(1).strip()
322
+ js_expr = php_to_js_advanced(expr)
323
+ variables = self._extract_variables(expr)
324
+
325
+ # Check if uses state variables
326
+ state_vars_used = variables & self.state_variables
327
+
328
+ # Check if we're inside an HTML tag (as a standalone attribute, not attribute value)
329
+ # Example: <input type="checkbox" {{ $checked ? 'checked' : '' }}>
330
+ pos = match.start()
331
+ tag_start = content.rfind('<', 0, pos)
332
+
333
+ if tag_start != -1:
334
+ tag_end = content.find('>', pos)
335
+ if tag_end != -1:
336
+ # Check if there's a > between tag_start and pos
337
+ intermediate_close = content.rfind('>', tag_start, pos)
338
+ if intermediate_close == -1:
339
+ # We're inside a tag - check if we're inside quotes
340
+ tag_content = content[tag_start:pos]
341
+ in_double = tag_content.count('"') % 2 == 1
342
+ in_single = tag_content.count("'") % 2 == 1
343
+
344
+ if not in_double and not in_single:
345
+ # Inside tag, outside quotes - this is a standalone attribute
346
+ # Use simple interpolation without __outputEscaped wrapper
347
+ return f"${{{APP_VIEW_NAMESPACE}.escString({js_expr})}}"
348
+
349
+ if state_vars_used:
350
+ # Reactive output with escaped HTML
351
+ state_vars_list = list(state_vars_used)
352
+ # Use new __reactive method for better performance
353
+ return f"${{this.__reactive(`{self._generate_reactive_id()}`, {state_vars_list}, (__rc__) => {js_expr}, {{type: 'output', escapeHTML: true}})}}"
354
+ else:
355
+ # Static output
356
+ return f"${{{APP_VIEW_NAMESPACE}.escString({js_expr})}}"
357
+
358
+ def replace_unescaped_echo(match):
359
+ expr = match.group(1).strip()
360
+ js_expr = php_to_js_advanced(expr)
361
+ variables = self._extract_variables(expr)
362
+
363
+ # Check if uses state variables
364
+ state_vars_used = variables & self.state_variables
365
+
366
+ # Check if we're inside an HTML tag
367
+ pos = match.start()
368
+ tag_start = content.rfind('<', 0, pos)
369
+
370
+ if tag_start != -1:
371
+ tag_end = content.find('>', pos)
372
+ if tag_end != -1:
373
+ intermediate_close = content.rfind('>', tag_start, pos)
374
+ if intermediate_close == -1:
375
+ tag_content = content[tag_start:pos]
376
+ in_double = tag_content.count('"') % 2 == 1
377
+ in_single = tag_content.count("'") % 2 == 1
378
+
379
+ if not in_double and not in_single:
380
+ # Inside tag, outside quotes - use simple interpolation
381
+ return f"${{{js_expr}}}"
382
+
383
+ if state_vars_used:
384
+ # Reactive output (unescaped) using new __reactive method
385
+ state_vars_list = list(state_vars_used)
386
+ return f"${{this.__reactive(`{self._generate_reactive_id()}`, {state_vars_list}, (__rc__) => {js_expr}, {{type: 'output', escapeHTML: false}})}}"
387
+ else:
388
+ # Static output
389
+ return f"${{{js_expr}}}"
390
+
391
+ # Process {!! !!} first (to avoid confusion with {{ }})
392
+ content = re.sub(r'{!!([^!]+)!!}', replace_unescaped_echo, content)
393
+
394
+ # Process {{ }}
395
+ content = re.sub(r'\{\{([^}]+)\}\}', replace_escaped_echo, content)
396
+
397
+ return content
398
+
399
+ def _extract_variables(self, php_expr):
400
+ """
401
+ Extract variable names from PHP expression
402
+ Returns set of variable names (without $ prefix)
403
+ """
404
+ variables = set()
405
+
406
+ # Find all $variableName patterns
407
+ var_pattern = r'\$([a-zA-Z_][a-zA-Z0-9_]*)'
408
+ matches = re.findall(var_pattern, php_expr)
409
+
410
+ for var_name in matches:
411
+ variables.add(var_name)
412
+
413
+ return variables
414
+
415
+ def _generate_attr_directive(self, echo_attrs):
416
+ """
417
+ Generate @attr directive from echo_attrs
418
+
419
+ Args:
420
+ echo_attrs: dict of {attr_name: {expressions, state_vars, original_value}}
421
+
422
+ Returns:
423
+ str: @attr directive string
424
+ """
425
+ # Build attribute object
426
+ attrs_obj = {}
427
+
428
+ for attr_name, attr_info in echo_attrs.items():
429
+ # Check if this is @checked or @selected
430
+ is_boolean_attr = (
431
+ len(attr_info['expressions']) == 1 and
432
+ attr_info['expressions'][0]['type'] in ['checked', 'selected']
433
+ )
434
+
435
+ if is_boolean_attr:
436
+ # For @checked/@selected, generate: () => expr ? true : false
437
+ expr_info = attr_info['expressions'][0]
438
+ render_func = f"() => ({expr_info['js']}) ? true : false"
439
+
440
+ attrs_obj[attr_name] = {
441
+ 'states': attr_info['state_vars'],
442
+ 'render': render_func
443
+ }
444
+ else:
445
+ # Combine all expressions in this attribute
446
+ combined_expr = attr_info['original_value']
447
+
448
+ # Convert to JS
449
+ js_expr = combined_expr
450
+ for expr_info in attr_info['expressions']:
451
+ if expr_info['type'] == 'escaped':
452
+ # For attributes, don't use __outputEscaped, just use variable directly
453
+ replacement = expr_info['js']
454
+ # Don't use exact match with expr_info['php'] because it's stripped
455
+ # Use regex to match {{ spaces $var spaces }}
456
+ js_expr = re.sub(r'\{\{\s*' + re.escape(expr_info['php']) + r'\s*\}\}', f"${{({replacement})}}", js_expr)
457
+ else:
458
+ replacement = expr_info['js']
459
+ # Same for unescaped
460
+ js_expr = re.sub(r'\{!!\s*' + re.escape(expr_info['php']) + r'\s*!!\}', f"${{({replacement})}}", js_expr)
461
+
462
+ # Simplify if expression is just a single variable
463
+ # Check if js_expr is exactly "${(varname)}" → simplify to just "varname"
464
+ single_expr_match = re.match(r'^\$\{\(([^)]+)\)\}$', js_expr)
465
+
466
+ if single_expr_match:
467
+ # Single expression, no template literal needed
468
+ render_func = f"() => {single_expr_match.group(1)}"
469
+ else:
470
+ # Complex expression or multiple expressions, use template literal
471
+ render_func = f"() => `{js_expr}`"
472
+
473
+ attrs_obj[attr_name] = {
474
+ 'states': attr_info['state_vars'],
475
+ 'render': render_func
476
+ }
477
+
478
+ # Convert to @attr format
479
+ # ${this.__attr({"attr1": {states:["a"], render: () => a}, ...})}
480
+ attr_parts = []
481
+ for attr_name, attr_config in attrs_obj.items():
482
+ states_str = str(attr_config['states']).replace("'", '"')
483
+ render_str = attr_config['render']
484
+ attr_parts.append(f'"{attr_name}": {{states: {states_str}, render: {render_str}}}')
485
+
486
+ attr_obj_str = '{' + ', '.join(attr_parts) + '}'
487
+
488
+ return f"${{this.__attr({attr_obj_str})}}"
489
+
490
+ def _merge_attr_directives(self, existing_params, echo_attrs):
491
+ """
492
+ Merge existing @attr parameters with echo_attrs
493
+
494
+ Args:
495
+ existing_params (str): Existing @attr parameters, e.g., "'data-count', $count" or "['attr' => $val]"
496
+ echo_attrs (dict): New attributes from {{ }} expressions
497
+
498
+ Returns:
499
+ str: Merged attribute object string
500
+ """
501
+ merged_attrs = {}
502
+
503
+ # Parse existing @attr parameters
504
+ # Handle two formats:
505
+ # 1. Simple format: @attr('attr-name', $value)
506
+ # 2. Array format: @attr(['attr-name' => $value, ...])
507
+
508
+ existing_params = existing_params.strip()
509
+
510
+ if existing_params.startswith('[') and existing_params.endswith(']'):
511
+ # Array format: ['attr' => value, ...]
512
+ # Parse array elements
513
+ array_content = existing_params[1:-1].strip()
514
+ # Simple split by comma (should handle quotes properly)
515
+ pairs = self._split_attr_pairs(array_content)
516
+
517
+ for pair in pairs:
518
+ if '=>' in pair:
519
+ parts = pair.split('=>', 1)
520
+ attr_name = parts[0].strip().strip('"').strip("'")
521
+ attr_value = parts[1].strip()
522
+
523
+ # Extract variables and convert to JS
524
+ variables = self._extract_variables(attr_value)
525
+ state_vars_used = variables & self.state_variables
526
+
527
+ if state_vars_used:
528
+ from php_js_converter import php_to_js_advanced
529
+ js_expr = php_to_js_advanced(attr_value)
530
+ merged_attrs[attr_name] = {
531
+ 'states': list(state_vars_used),
532
+ 'render': f"() => {js_expr}"
533
+ }
534
+ else:
535
+ # Simple format: 'attr-name', $value
536
+ parts = self._split_attr_pairs(existing_params)
537
+ if len(parts) >= 2:
538
+ attr_name = parts[0].strip().strip('"').strip("'")
539
+ attr_value = ','.join(parts[1:]).strip()
540
+
541
+ # Extract variables and convert to JS
542
+ variables = self._extract_variables(attr_value)
543
+ state_vars_used = variables & self.state_variables
544
+
545
+ if state_vars_used:
546
+ from php_js_converter import php_to_js_advanced
547
+ js_expr = php_to_js_advanced(attr_value)
548
+ merged_attrs[attr_name] = {
549
+ 'states': list(state_vars_used),
550
+ 'render': f"() => {js_expr}"
551
+ }
552
+
553
+ # Add echo_attrs (from {{ }} expressions or @checked/@selected)
554
+ for attr_name, attr_info in echo_attrs.items():
555
+ # Check if this is @checked or @selected
556
+ is_boolean_attr = (
557
+ len(attr_info['expressions']) == 1 and
558
+ attr_info['expressions'][0]['type'] in ['checked', 'selected']
559
+ )
560
+
561
+ if is_boolean_attr:
562
+ # For @checked/@selected, generate: () => expr ? true : false
563
+ expr_info = attr_info['expressions'][0]
564
+ render_func = f"() => ({expr_info['js']}) ? true : false"
565
+
566
+ merged_attrs[attr_name] = {
567
+ 'states': attr_info['state_vars'],
568
+ 'render': render_func
569
+ }
570
+ else:
571
+ # Build JS expression for regular attributes
572
+ combined_expr = attr_info['original_value']
573
+ js_expr = combined_expr
574
+
575
+ for expr_info in attr_info['expressions']:
576
+ if expr_info['type'] == 'escaped':
577
+ replacement = expr_info['js']
578
+ js_expr = re.sub(r'\{\{\s*' + re.escape(expr_info['php']) + r'\s*\}\}', f"${{({replacement})}}", js_expr)
579
+ else:
580
+ replacement = expr_info['js']
581
+ js_expr = re.sub(r'\{!!\s*' + re.escape(expr_info['php']) + r'\s*!!\}', f"${{({replacement})}}", js_expr)
582
+
583
+ # Simplify if expression is just a single variable
584
+ single_expr_match = re.match(r'^\$\{\(([^)]+)\)\}$', js_expr)
585
+
586
+ if single_expr_match:
587
+ # Single expression, no template literal needed
588
+ render_func = f"() => {single_expr_match.group(1)}"
589
+ else:
590
+ # Complex expression or multiple expressions, use template literal
591
+ render_func = f"() => `{js_expr}`"
592
+
593
+ merged_attrs[attr_name] = {
594
+ 'states': attr_info['state_vars'],
595
+ 'render': render_func
596
+ }
597
+
598
+ # Convert to JSON-like string
599
+ attr_parts = []
600
+ for attr_name, attr_config in merged_attrs.items():
601
+ states_str = str(attr_config['states']).replace("'", '"')
602
+ render_str = attr_config['render']
603
+ attr_parts.append(f'"{attr_name}": {{states: {states_str}, render: {render_str}}}')
604
+
605
+ return '{' + ', '.join(attr_parts) + '}'
606
+
607
+ def _split_attr_pairs(self, content):
608
+ """
609
+ Split attribute pairs by comma, respecting quotes and parentheses
610
+ """
611
+ pairs = []
612
+ current = ''
613
+ paren_depth = 0
614
+ bracket_depth = 0
615
+ in_quotes = False
616
+ quote_char = None
617
+
618
+ for i, char in enumerate(content):
619
+ if not in_quotes:
620
+ if char in ['"', "'"]:
621
+ in_quotes = True
622
+ quote_char = char
623
+ current += char
624
+ elif char == '(':
625
+ paren_depth += 1
626
+ current += char
627
+ elif char == ')':
628
+ paren_depth -= 1
629
+ current += char
630
+ elif char == '[':
631
+ bracket_depth += 1
632
+ current += char
633
+ elif char == ']':
634
+ bracket_depth -= 1
635
+ current += char
636
+ elif char == ',' and paren_depth == 0 and bracket_depth == 0:
637
+ if current.strip():
638
+ pairs.append(current.strip())
639
+ current = ''
640
+ else:
641
+ current += char
642
+ else:
643
+ current += char
644
+ if char == quote_char:
645
+ # Check if escaped
646
+ if i > 0 and content[i-1] != '\\':
647
+ in_quotes = False
648
+
649
+ if current.strip():
650
+ pairs.append(current.strip())
651
+
652
+ return pairs
653
+
654
+ def _is_complex_structure(self, js_expr):
655
+ """
656
+ Check if expression is complex structure (array/object)
657
+ If yes, don't escape
658
+ """
659
+ # Simple heuristic - check for brackets
660
+ return '{' in js_expr or '[' in js_expr
661
+
662
+ def _generate_reactive_id(self):
663
+ """
664
+ Generate unique reactive component ID
665
+ """
666
+ self.reactive_counter += 1
667
+ return f"rc-{{{{__VIEW_ID__}}}}-{self.reactive_counter}"