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,1418 @@
1
+ """
2
+ Parsers cho các directive (@extends, @vars, @fetch, @onInit)
3
+ """
4
+
5
+ import re
6
+ import json
7
+ from utils import extract_balanced_parentheses
8
+ from php_converter import php_to_js, convert_php_array_to_json, convert_php_array_with_php_r
9
+
10
+ class DirectiveParsers:
11
+ def __init__(self):
12
+ pass
13
+
14
+ def _remove_script_tags(self, blade_code):
15
+ """Loại bỏ JavaScript code trong <script> tags để tránh xử lý useState trong JS"""
16
+ # Loại bỏ tất cả content trong <script> tags
17
+ filtered_code = re.sub(r'<script[^>]*>.*?</script>', '', blade_code, flags=re.DOTALL | re.IGNORECASE)
18
+ return filtered_code
19
+
20
+ def _remove_verbatim_blocks(self, blade_code):
21
+ """Loại bỏ @verbatim...@endverbatim blocks để tránh xử lý directives bên trong"""
22
+ # Loại bỏ tất cả content trong @verbatim blocks
23
+ filtered_code = re.sub(r'@verbatim\s*.*?\s*@endverbatim', '', blade_code, flags=re.DOTALL | re.IGNORECASE)
24
+ return filtered_code
25
+
26
+ def parse_extends(self, blade_code):
27
+ """Parse @extends directive"""
28
+ extends_match = re.search(r'@extends\s*\(\s*([^)]+)\s*\)', blade_code, re.DOTALL)
29
+ if not extends_match:
30
+ return None, None, None
31
+
32
+ extends_content = extends_match.group(1).strip()
33
+
34
+ # Check if there's a comma (indicating data parameter)
35
+ comma_pos = extends_content.find(',')
36
+ if comma_pos != -1:
37
+ view_expr = extends_content[:comma_pos].strip()
38
+ data_expr = extends_content[comma_pos + 1:].strip()
39
+ extends_data = self._convert_extends_data(data_expr)
40
+ else:
41
+ view_expr = extends_content
42
+ extends_data = None
43
+
44
+ # Check if it's a simple string literal (no variables or function calls)
45
+ if self._is_simple_string_literal(view_expr):
46
+ extended_view = view_expr[1:-1] # Remove quotes
47
+ extends_expression = None
48
+ else:
49
+ # Complex expression - convert to JavaScript
50
+ extends_expression = self._convert_extends_expression(view_expr)
51
+ extended_view = None
52
+
53
+ return extended_view, extends_expression, extends_data
54
+
55
+ def parse_vars(self, blade_code):
56
+ """Parse @vars directive - improved to handle complex arrays like Event directive"""
57
+ # Loại bỏ @verbatim blocks để tránh parse directives bên trong
58
+ blade_code = self._remove_verbatim_blocks(blade_code)
59
+ vars_match = re.search(r'@vars\s*\(\s*(.*?)\s*\)', blade_code, re.DOTALL)
60
+ if not vars_match:
61
+ return ''
62
+
63
+ vars_content = vars_match.group(1)
64
+ var_parts = []
65
+
66
+ # Special handling for object destructuring syntax {var1, var2}
67
+ if vars_content.strip().startswith('{') and vars_content.strip().endswith('}'):
68
+ # Extract content inside braces
69
+ inner_content = vars_content.strip()[1:-1]
70
+ # Split by comma at level 0
71
+ parts = self._split_vars_content_correct(inner_content)
72
+ else:
73
+ # Use improved splitting logic (same as Event directive)
74
+ parts = self._split_vars_content_correct(vars_content)
75
+
76
+ for var in parts:
77
+ var = var.strip()
78
+ if '=' in var:
79
+ equals_pos = self._find_first_equals(var)
80
+ if equals_pos != -1:
81
+ var_name = var[:equals_pos].strip().lstrip('$')
82
+ var_value = var[equals_pos + 1:].strip()
83
+ # Convert PHP array syntax to JavaScript
84
+ var_value = self._convert_php_to_js(var_value)
85
+ var_parts.append(f"{var_name} = {var_value}")
86
+ else:
87
+ var_name = var.strip().lstrip('$')
88
+ var_parts.append(var_name)
89
+ else:
90
+ var_name = var.strip().lstrip('$')
91
+ var_parts.append(var_name)
92
+
93
+ return "let {" + ', '.join(var_parts) + "} = $$$DATA$$$ || {};"
94
+
95
+ def parse_let_directives(self, blade_code):
96
+ """Parse @let directives - chỉ xử lý Blade directives, không xử lý JavaScript code"""
97
+ # Loại bỏ JavaScript code trong <script> tags trước khi parse
98
+ blade_code_filtered = self._remove_script_tags(blade_code)
99
+ # Loại bỏ @verbatim blocks để tránh parse directives bên trong
100
+ blade_code_filtered = self._remove_verbatim_blocks(blade_code_filtered)
101
+
102
+ # Sử dụng balanced parentheses để parse đúng
103
+ let_matches = []
104
+ pattern = r'@let\s*\('
105
+ for match in re.finditer(pattern, blade_code_filtered):
106
+ start_pos = match.end() - 1
107
+ content, _ = extract_balanced_parentheses(blade_code_filtered, start_pos)
108
+ if content is not None and content.strip():
109
+ let_matches.append(content.strip())
110
+
111
+ if not let_matches:
112
+ return ''
113
+
114
+ let_declarations = []
115
+ for expression in let_matches:
116
+ # Split expression by comma để xử lý mixed declarations
117
+ parts = self._split_assignments(expression)
118
+
119
+ for part in parts:
120
+ part = part.strip()
121
+ if not part:
122
+ continue
123
+
124
+ # Loại bỏ dấu $ ở đầu part nếu có
125
+ part = re.sub(r'^\s*\$\s*', '', part)
126
+
127
+ # Xử lý destructuring syntax đặc biệt
128
+ if '[' in part and ']' in part and '=' in part:
129
+ # Đây là destructuring assignment
130
+ js_expression = self._convert_destructuring_assignment(part)
131
+ if js_expression:
132
+ # Thay const bằng let
133
+ js_expression = js_expression.replace('const ', 'let ')
134
+ let_declarations.append(js_expression)
135
+ continue
136
+ elif '{' in part and '}' in part and '=' in part:
137
+ # Object destructuring
138
+ js_expression = self._convert_destructuring_assignment(part)
139
+ if js_expression:
140
+ js_expression = js_expression.replace('const ', 'let ')
141
+ let_declarations.append(js_expression)
142
+ continue
143
+
144
+ # Convert PHP to JavaScript với xử lý mảng bằng php -r
145
+ js_expression = self._convert_php_expression_with_arrays(part)
146
+
147
+ # Loại bỏ dấu $ từ biến (nếu có) - cải thiện regex
148
+ js_expression = re.sub(r'\$(\w+)', r'\1', js_expression)
149
+
150
+ # Loại bỏ dấu $ ở đầu assignment nếu có
151
+ if '=' in js_expression:
152
+ parts = js_expression.split('=', 1)
153
+ if len(parts) == 2:
154
+ left_part = parts[0].strip()
155
+ right_part = parts[1].strip()
156
+ # Loại bỏ $ ở đầu left part
157
+ left_part = re.sub(r'^\s*\$\s*', '', left_part)
158
+ js_expression = f'{left_part} = {right_part}'
159
+
160
+ # Thêm prefix 'let ' và dấu ; nếu chưa có
161
+ if not js_expression.startswith('let '):
162
+ js_expression = 'let ' + js_expression
163
+ if not js_expression.endswith(';'):
164
+ js_expression += ';'
165
+ let_declarations.append(js_expression)
166
+
167
+ return '\n'.join(let_declarations)
168
+
169
+ def _convert_php_expression_with_arrays(self, expression):
170
+ """Convert PHP expression to JavaScript with array handling using php -r"""
171
+ # Sử dụng regex đơn giản để tìm và replace arrays
172
+ def replace_array(match):
173
+ array_expr = match.group(0)
174
+ # Sử dụng php -r để convert array
175
+ json_result = convert_php_array_with_php_r(array_expr)
176
+ if json_result is not None:
177
+ return json_result
178
+ # Fallback to old method
179
+ return self._convert_php_array_legacy(array_expr)
180
+
181
+ # Sử dụng php -r trực tiếp cho toàn bộ expression nếu có array
182
+ import re
183
+ if '[' in expression and ']' in expression:
184
+ # Thử sử dụng php -r cho toàn bộ expression
185
+ json_result = convert_php_array_with_php_r(expression)
186
+ if json_result is not None:
187
+ expression = json_result
188
+ else:
189
+ # Fallback to convert_php_array_to_json
190
+ expression = convert_php_array_to_json(expression)
191
+
192
+ # Convert remaining PHP to JavaScript
193
+ result = php_to_js(expression)
194
+
195
+ # Loại bỏ dấu $ từ biến (nếu có)
196
+ result = re.sub(r'\$(\w+)', r'\1', result)
197
+
198
+ return result
199
+
200
+ def _split_assignments(self, expression):
201
+ """Split expression by comma, respecting parentheses and brackets"""
202
+ assignments = []
203
+ current = ''
204
+ balance = 0
205
+ in_string = False
206
+ string_char = ''
207
+
208
+ for char in expression:
209
+ if char in ['"', "'"]:
210
+ if in_string and char == string_char:
211
+ in_string = False
212
+ elif not in_string:
213
+ in_string = True
214
+ string_char = char
215
+ elif not in_string:
216
+ if char in ['(', '[', '{']:
217
+ balance += 1
218
+ elif char in [')', ']', '}']:
219
+ balance -= 1
220
+ elif char == ',' and balance == 0:
221
+ assignments.append(current.strip())
222
+ current = ''
223
+ continue
224
+
225
+ current += char
226
+
227
+ if current.strip():
228
+ assignments.append(current.strip())
229
+
230
+ return assignments
231
+
232
+ def parse_const_directives(self, blade_code):
233
+ """Parse @const directives - chỉ xử lý Blade directives, không xử lý JavaScript code"""
234
+ # Loại bỏ JavaScript code trong <script> tags trước khi parse
235
+ blade_code_filtered = self._remove_script_tags(blade_code)
236
+ # Loại bỏ @verbatim blocks để tránh parse directives bên trong
237
+ blade_code_filtered = self._remove_verbatim_blocks(blade_code_filtered)
238
+
239
+ # Sử dụng balanced parentheses để parse đúng
240
+ const_matches = []
241
+ pattern = r'@const\s*\('
242
+ for match in re.finditer(pattern, blade_code_filtered):
243
+ start_pos = match.end() - 1
244
+ content, _ = extract_balanced_parentheses(blade_code_filtered, start_pos)
245
+ if content is not None and content.strip():
246
+ const_matches.append(content.strip())
247
+
248
+ if not const_matches:
249
+ return ''
250
+
251
+ const_declarations = []
252
+ for expression in const_matches:
253
+ # Xử lý destructuring syntax đặc biệt
254
+ if '[' in expression and ']' in expression and '=' in expression:
255
+ # Đây là destructuring assignment
256
+ js_expression = self._convert_destructuring_assignment(expression)
257
+ if js_expression:
258
+ const_declarations.append(js_expression)
259
+ continue
260
+
261
+ # Convert PHP to JavaScript với xử lý mảng bằng php -r
262
+ js_expression = self._convert_php_expression_with_arrays(expression)
263
+
264
+ # Split by comma to handle multiple assignments
265
+ assignments = self._split_assignments(js_expression)
266
+
267
+ for assignment in assignments:
268
+ assignment = assignment.strip()
269
+ if assignment:
270
+ # Thêm prefix 'const ' và dấu ; nếu chưa có
271
+ if not assignment.startswith('const '):
272
+ assignment = 'const ' + assignment
273
+ if not assignment.endswith(';'):
274
+ assignment += ';'
275
+ const_declarations.append(assignment)
276
+
277
+ return '\n'.join(const_declarations)
278
+
279
+ def _convert_destructuring_assignment(self, expression):
280
+ """Convert destructuring assignment like [a, b] = something() or {a, b} = something()"""
281
+ # Tìm dấu = đầu tiên
282
+ equals_pos = expression.find('=')
283
+ if equals_pos == -1:
284
+ return None
285
+
286
+ # Lấy phần bên trái (destructuring pattern) và bên phải (value)
287
+ left_part = expression[:equals_pos].strip()
288
+ right_part = expression[equals_pos + 1:].strip()
289
+
290
+ # Xử lý left part: [$userState, $setUserState] -> [userState, setUserState]
291
+ if left_part.startswith('[') and left_part.endswith(']'):
292
+ # Array destructuring
293
+ inner_content = left_part[1:-1].strip()
294
+ # Split by comma và loại bỏ $ từ mỗi biến
295
+ variables = []
296
+ for var in inner_content.split(','):
297
+ var = var.strip().lstrip('$')
298
+ variables.append(var)
299
+
300
+ left_part_js = '[' + ', '.join(variables) + ']'
301
+ elif left_part.startswith('{') and left_part.endswith('}'):
302
+ # Object destructuring
303
+ inner_content = left_part[1:-1].strip()
304
+ # Split by comma và loại bỏ $ từ mỗi biến
305
+ variables = []
306
+ for var in inner_content.split(','):
307
+ var = var.strip().lstrip('$')
308
+ variables.append(var)
309
+
310
+ left_part_js = '{' + ', '.join(variables) + '}'
311
+ else:
312
+ left_part_js = left_part
313
+
314
+ # Convert right part với xử lý mảng
315
+ right_part_js = self._convert_php_expression_with_arrays(right_part)
316
+
317
+ return f'const {left_part_js} = {right_part_js};'
318
+
319
+ def parse_usestate_directives(self, blade_code):
320
+ """Parse @useState directives - chỉ xử lý Blade directives, không xử lý JavaScript code"""
321
+ # Loại bỏ JavaScript code trong <script> tags trước khi parse
322
+ blade_code_filtered = self._remove_script_tags(blade_code)
323
+ # Loại bỏ @verbatim blocks để tránh parse directives bên trong
324
+ blade_code_filtered = self._remove_verbatim_blocks(blade_code_filtered)
325
+
326
+ usestate_matches = re.findall(r'@useState\s*\(\s*(.*?)\s*\)', blade_code_filtered, re.DOTALL)
327
+ if not usestate_matches:
328
+ return ''
329
+
330
+ usestate_declarations = []
331
+ for match in usestate_matches:
332
+ expression = match.strip()
333
+ # Parse 3 parameters: value, stateName, setStateName
334
+ params = self._parse_usestate_params(expression)
335
+ if len(params) >= 2:
336
+ value = params[0]
337
+ state_name = params[1] if len(params) > 1 else 'state'
338
+ set_state_name = params[2] if len(params) > 2 else 'setState'
339
+ # Convert PHP to JavaScript với xử lý mảng bằng php -r
340
+ value_js = self._convert_php_expression_with_arrays(value)
341
+ state_name_js = php_to_js(state_name)
342
+ set_state_name_js = php_to_js(set_state_name)
343
+ usestate_declarations.append(f'const [{state_name_js}, {set_state_name_js}] = useState({value_js});')
344
+
345
+ return '\n'.join(usestate_declarations)
346
+
347
+ def _parse_usestate_params(self, expression):
348
+ """Parse @useState parameters with proper array handling"""
349
+ params = []
350
+ current = ''
351
+ paren_count = 0
352
+ bracket_count = 0
353
+ in_quotes = False
354
+ quote_char = ''
355
+ i = 0
356
+
357
+ while i < len(expression):
358
+ char = expression[i]
359
+
360
+ if not in_quotes:
361
+ if char in ['"', "'"]:
362
+ in_quotes = True
363
+ quote_char = char
364
+ current += char
365
+ elif char == '(':
366
+ paren_count += 1
367
+ current += char
368
+ elif char == ')':
369
+ paren_count -= 1
370
+ current += char
371
+ elif char == '[':
372
+ bracket_count += 1
373
+ current += char
374
+ elif char == ']':
375
+ bracket_count -= 1
376
+ current += char
377
+ elif char == ',' and paren_count == 0 and bracket_count == 0:
378
+ params.append(current.strip())
379
+ current = ''
380
+ else:
381
+ current += char
382
+ else:
383
+ current += char
384
+ if char == quote_char:
385
+ # Check if it's escaped
386
+ escape_count = 0
387
+ j = i - 1
388
+ while j >= 0 and expression[j] == '\\':
389
+ escape_count += 1
390
+ j -= 1
391
+
392
+ # If even number of backslashes, quote is not escaped
393
+ if escape_count % 2 == 0:
394
+ in_quotes = False
395
+
396
+ i += 1
397
+
398
+ if current.strip():
399
+ params.append(current.strip())
400
+
401
+ return params
402
+
403
+ def _split_fetch_parameters(self, content):
404
+ """Split fetch parameters by comma, respecting parentheses, brackets and quotes"""
405
+ params = []
406
+ current = ''
407
+ paren_count = 0
408
+ bracket_count = 0
409
+ in_quotes = False
410
+ quote_char = ''
411
+
412
+ i = 0
413
+ while i < len(content):
414
+ char = content[i]
415
+
416
+ if not in_quotes:
417
+ if char in ['"', "'"]:
418
+ in_quotes = True
419
+ quote_char = char
420
+ current += char
421
+ elif char == '(':
422
+ paren_count += 1
423
+ current += char
424
+ elif char == ')':
425
+ paren_count -= 1
426
+ current += char
427
+ elif char == '[':
428
+ bracket_count += 1
429
+ current += char
430
+ elif char == ']':
431
+ bracket_count -= 1
432
+ current += char
433
+ elif char == ',' and paren_count == 0 and bracket_count == 0:
434
+ # Comma at level 0 - parameter separator
435
+ params.append(current.strip())
436
+ current = ''
437
+ i += 1
438
+ continue
439
+ else:
440
+ current += char
441
+ else:
442
+ current += char
443
+ if char == quote_char and (i == 0 or content[i-1] != '\\'):
444
+ in_quotes = False
445
+ quote_char = ''
446
+
447
+ i += 1
448
+
449
+ # Add last parameter
450
+ if current.strip():
451
+ params.append(current.strip())
452
+
453
+ return params
454
+
455
+ def _parse_php_array_to_js_object(self, php_array_str):
456
+ """Parse PHP array string to JavaScript object"""
457
+ try:
458
+ # Convert PHP array syntax to JavaScript object syntax
459
+ js_str = php_array_str
460
+ # Remove $ prefix from variables
461
+ js_str = re.sub(r'(?<!")\$(\w+)(?!")', r'\1', js_str)
462
+ # Convert array syntax to object syntax
463
+ js_str = re.sub(r'\[', '{', js_str)
464
+ js_str = re.sub(r'\]', '}', js_str)
465
+ js_str = re.sub(r'\s*=>\s*', ': ', js_str)
466
+ js_str = re.sub(r'\s+\.\s+', ' + ', js_str)
467
+
468
+ # Handle single quotes to double quotes for JSON
469
+ js_str = re.sub(r"'([^']*)'", r'"\1"', js_str)
470
+
471
+ # Parse the JavaScript object
472
+ import json
473
+ return json.loads(js_str)
474
+ except Exception as e:
475
+ print(f"Error parsing {php_array_str}: {e}")
476
+ return {}
477
+
478
+ def parse_fetch(self, blade_code):
479
+ """Parse @fetch directive - supports @fetch(url, data, headers) and @fetch([config])"""
480
+ fetch_match = re.search(r'@fetch\s*\(', blade_code)
481
+ if not fetch_match:
482
+ return None
483
+
484
+ start_pos = fetch_match.end() - 1
485
+ fetch_content, _ = extract_balanced_parentheses(blade_code, start_pos)
486
+ if fetch_content:
487
+ fetch_content = fetch_content.strip()
488
+
489
+ # Check if it's multiple parameters: @fetch(url, data, headers)
490
+ if ',' in fetch_content and not fetch_content.startswith('['):
491
+ # Split by comma that's not inside parentheses or quotes
492
+ parts = self._split_fetch_parameters(fetch_content)
493
+
494
+ if len(parts) >= 1:
495
+ # Parse URL (first parameter)
496
+ url_part = parts[0].strip()
497
+ if url_part.startswith("'") and url_part.endswith("'"):
498
+ url = f"`{url_part[1:-1]}`"
499
+ elif url_part.startswith('"') and url_part.endswith('"'):
500
+ url = f"`{url_part[1:-1]}`"
501
+ elif '(' in url_part and ')' in url_part:
502
+ # Function call like route('api.posts')
503
+ js_url = php_to_js(url_part)
504
+ url = f"`${{{js_url}}}`"
505
+ else:
506
+ url = f"`${{{url_part}}}`"
507
+
508
+ # Parse data (second parameter, optional)
509
+ data = {}
510
+ if len(parts) >= 2:
511
+ data_part = parts[1].strip()
512
+ if data_part.startswith('[') and data_part.endswith(']'):
513
+ data = self._parse_php_array_to_js_object(data_part)
514
+
515
+ # Parse headers (third parameter, optional)
516
+ headers = {}
517
+ if len(parts) >= 3:
518
+ headers_part = parts[2].strip()
519
+ if headers_part.startswith('[') and headers_part.endswith(']'):
520
+ headers = self._parse_php_array_to_js_object(headers_part)
521
+
522
+ return {
523
+ 'url': url,
524
+ 'method': 'GET',
525
+ 'data': data,
526
+ 'headers': headers
527
+ }
528
+
529
+ # Simple URL fetch
530
+ elif fetch_content.startswith("'") and fetch_content.endswith("'"):
531
+ url_content = fetch_content[1:-1]
532
+ return {'url': f"`{url_content}`", 'method': 'GET'}
533
+ elif fetch_content.startswith('"') and fetch_content.endswith('"'):
534
+ url_content = fetch_content[1:-1]
535
+ return {'url': f"`{url_content}`", 'method': 'GET'}
536
+
537
+ # Function call fetch (e.g., route('web.users'))
538
+ elif '(' in fetch_content and ')' in fetch_content and not fetch_content.startswith('['):
539
+ js_url = php_to_js(fetch_content)
540
+ return {'url': f"`${{{js_url}}}`", 'method': 'GET'}
541
+
542
+ # Array configuration fetch
543
+ elif fetch_content.startswith('[') and fetch_content.endswith(']'):
544
+ try:
545
+ # Handle multiple array parameters (e.g., [config], [data], [headers])
546
+ if ',' in fetch_content and fetch_content.count('[') > 1:
547
+ # Split by comma that's not inside brackets
548
+ parts = self._split_fetch_parameters(fetch_content)
549
+
550
+ # Parse first part as main config
551
+ config = {}
552
+ if parts:
553
+ config = self._parse_php_array_to_js_object(parts[0])
554
+
555
+ # Parse additional data parameter (second parameter)
556
+ if len(parts) >= 2:
557
+ additional_data = self._parse_php_array_to_js_object(parts[1])
558
+ if 'data' in config:
559
+ config['data'].update(additional_data)
560
+ else:
561
+ config['data'] = additional_data
562
+
563
+ # Parse additional headers parameter (third parameter)
564
+ if len(parts) >= 3:
565
+ additional_headers = self._parse_php_array_to_js_object(parts[2])
566
+ if 'headers' in config:
567
+ config['headers'].update(additional_headers)
568
+ else:
569
+ config['headers'] = additional_headers
570
+
571
+ # Ensure required fields
572
+ if 'url' not in config:
573
+ config['url'] = ''
574
+ if 'method' not in config:
575
+ config['method'] = 'GET'
576
+ if 'data' not in config:
577
+ config['data'] = {}
578
+ if 'headers' not in config:
579
+ config['headers'] = {}
580
+
581
+ # Convert values to template strings if needed
582
+ if isinstance(config['url'], str) and not config['url'].startswith('`'):
583
+ config['url'] = f"`{config['url']}`"
584
+
585
+ return config
586
+ else:
587
+ # Single array parameter
588
+ config = self._parse_php_array_to_js_object(fetch_content)
589
+
590
+ # Ensure required fields
591
+ if 'url' not in config:
592
+ config['url'] = ''
593
+ if 'method' not in config:
594
+ config['method'] = 'GET'
595
+ if 'data' not in config:
596
+ config['data'] = {}
597
+ if 'headers' not in config:
598
+ config['headers'] = {}
599
+
600
+ # Convert values to template strings if needed
601
+ if isinstance(config['url'], str) and not config['url'].startswith('`'):
602
+ config['url'] = f"`{config['url']}`"
603
+
604
+ return config
605
+ except Exception as e:
606
+ # Fallback to simple URL if parsing fails
607
+ print(f"Fetch parsing error: {e}")
608
+ return {'url': "`" + fetch_content + "`", 'method': 'GET'}
609
+
610
+ return None
611
+
612
+ def parse_init(self, blade_code):
613
+ """Parse @onInit directives"""
614
+ init_functions = []
615
+ css_content = []
616
+
617
+ # Find all @onInit blocks - support both @onInit(...) and @onInit...@endonInit formats
618
+ # Find @onInit blocks by finding @onInit( and matching the closing )
619
+ init_matches = []
620
+ start_pattern = re.compile(r'@oninit\s*\(', re.IGNORECASE)
621
+
622
+ for match in start_pattern.finditer(blade_code):
623
+ start_pos = match.end()
624
+ # Find the matching closing parenthesis
625
+ paren_count = 1
626
+ pos = start_pos
627
+
628
+ while pos < len(blade_code) and paren_count > 0:
629
+ if blade_code[pos] == '(':
630
+ paren_count += 1
631
+ elif blade_code[pos] == ')':
632
+ paren_count -= 1
633
+ pos += 1
634
+
635
+ if paren_count == 0:
636
+ # Found matching closing parenthesis
637
+ content = blade_code[start_pos:pos-1]
638
+ # Create a mock match object
639
+ class MockMatch:
640
+ def __init__(self, content):
641
+ self._content = content
642
+ def group(self, n):
643
+ if n == 1:
644
+ return self._content
645
+ return None
646
+
647
+ init_matches.append(MockMatch(content))
648
+
649
+ for match in init_matches:
650
+ init_content = match.group(1).strip()
651
+
652
+ # Extract script content from <script> tags
653
+ script_content = []
654
+ in_script = False
655
+
656
+ # Extract CSS content from <style> tags
657
+ style_matches = re.finditer(r'<style[^>]*>(.*?)</style>', init_content, re.DOTALL | re.IGNORECASE)
658
+ for style_match in style_matches:
659
+ css_content.append(style_match.group(1).strip())
660
+
661
+ # Check if content has <script> tags
662
+ has_script_tags = '<script' in init_content.lower()
663
+
664
+ if has_script_tags:
665
+ # Process content with <script> tags
666
+ for line in init_content.split('\n'):
667
+ if '<script' in line.lower():
668
+ in_script = True
669
+ script_start = line.lower().find('<script')
670
+ script_tag_end = line.find('>', script_start)
671
+ if script_tag_end != -1:
672
+ script_content.append(line[script_tag_end + 1:])
673
+ elif '</script>' in line.lower():
674
+ in_script = False
675
+ script_end = line.lower().find('</script>')
676
+ if script_end > 0:
677
+ script_content.append(line[:script_end])
678
+ elif in_script:
679
+ script_content.append(line)
680
+ else:
681
+ # Process content directly without <script> tags
682
+ script_content.append(init_content)
683
+
684
+ if script_content:
685
+ init_code = '\n'.join(script_content).strip()
686
+ if init_code:
687
+ init_functions.append(init_code)
688
+
689
+ return init_functions, css_content
690
+
691
+ def _convert_extends_data(self, data_expr):
692
+ """Convert extends data expression"""
693
+ extends_data = convert_php_array_to_json(data_expr)
694
+ extends_data = re.sub(r'(?<!")\$(\w+)(?!")', r'\1', extends_data)
695
+ extends_data = re.sub(r'\[', '{', extends_data)
696
+ extends_data = re.sub(r'\]', '}', extends_data)
697
+ extends_data = re.sub(r'\s*=>\s*', ': ', extends_data)
698
+ extends_data = re.sub(r'\s+\.\s+', ' + ', extends_data)
699
+ return extends_data
700
+
701
+ def _split_vars_content(self, content):
702
+ """Split vars content respecting parentheses and brackets"""
703
+ parts = []
704
+ current = ''
705
+ paren_count = 0
706
+ bracket_count = 0
707
+ in_quotes = False
708
+ quote_char = ''
709
+ i = 0
710
+
711
+ while i < len(content):
712
+ char = content[i]
713
+
714
+ if (char == '"' or char == "'") and not in_quotes:
715
+ in_quotes = True
716
+ quote_char = char
717
+ elif char == quote_char and in_quotes:
718
+ in_quotes = False
719
+ quote_char = ''
720
+ elif not in_quotes:
721
+ if char == '(':
722
+ paren_count += 1
723
+ elif char == ')':
724
+ paren_count -= 1
725
+ elif char == '[':
726
+ bracket_count += 1
727
+ elif char == ']':
728
+ bracket_count -= 1
729
+ elif char == ',' and paren_count == 0 and bracket_count == 0:
730
+ parts.append(current.strip())
731
+ current = ''
732
+ i += 1
733
+ continue
734
+
735
+ current += char
736
+ i += 1
737
+
738
+ if current.strip():
739
+ parts.append(current.strip())
740
+
741
+ return parts
742
+
743
+ def _split_vars_content_improved(self, content):
744
+ """
745
+ Split vars content with improved logic (same as Event directive)
746
+ Handles complex nested arrays and objects properly
747
+ """
748
+ parts = []
749
+ current = ''
750
+ paren_count = 0
751
+ bracket_count = 0
752
+ in_quotes = False
753
+ quote_char = ''
754
+
755
+ i = 0
756
+ while i < len(content):
757
+ char = content[i]
758
+
759
+ if not in_quotes:
760
+ if char in ['"', "'"]:
761
+ in_quotes = True
762
+ quote_char = char
763
+ current += char
764
+ elif char == '(':
765
+ paren_count += 1
766
+ current += char
767
+ elif char == ')':
768
+ paren_count -= 1
769
+ current += char
770
+ elif char == '[':
771
+ bracket_count += 1
772
+ current += char
773
+ elif char == ']':
774
+ bracket_count -= 1
775
+ current += char
776
+ elif char == ',' and paren_count == 0 and bracket_count == 0:
777
+ # Comma at level 0 - variable separator
778
+ parts.append(current.strip())
779
+ current = ''
780
+ i += 1
781
+ continue
782
+ else:
783
+ current += char
784
+ else:
785
+ current += char
786
+ if char == quote_char and (i == 0 or content[i-1] != '\\'):
787
+ in_quotes = False
788
+ quote_char = ''
789
+
790
+ i += 1
791
+
792
+ # Add last variable
793
+ if current.strip():
794
+ parts.append(current.strip())
795
+
796
+ return parts
797
+
798
+ def _split_vars_content_fixed(self, content):
799
+ """
800
+ Split vars content with fixed logic - properly handles nested arrays
801
+ """
802
+ parts = []
803
+ current = ''
804
+ paren_count = 0
805
+ bracket_count = 0
806
+ in_quotes = False
807
+ quote_char = ''
808
+
809
+ i = 0
810
+ while i < len(content):
811
+ char = content[i]
812
+
813
+ if not in_quotes:
814
+ if char in ['"', "'"]:
815
+ in_quotes = True
816
+ quote_char = char
817
+ current += char
818
+ elif char == '(':
819
+ paren_count += 1
820
+ current += char
821
+ elif char == ')':
822
+ paren_count -= 1
823
+ current += char
824
+ elif char == '[':
825
+ bracket_count += 1
826
+ current += char
827
+ elif char == ']':
828
+ bracket_count -= 1
829
+ current += char
830
+ elif char == ',' and paren_count == 0 and bracket_count == 0:
831
+ # Comma at level 0 - variable separator
832
+ parts.append(current.strip())
833
+ current = ''
834
+ i += 1
835
+ continue
836
+ else:
837
+ current += char
838
+ else:
839
+ current += char
840
+ if char == quote_char and (i == 0 or content[i-1] != '\\'):
841
+ in_quotes = False
842
+ quote_char = ''
843
+
844
+ i += 1
845
+
846
+ # Add last variable
847
+ if current.strip():
848
+ parts.append(current.strip())
849
+
850
+ return parts
851
+
852
+ def _split_vars_content_correct(self, content):
853
+ """
854
+ Split vars content with correct logic - handles nested arrays properly
855
+ """
856
+ parts = []
857
+ current = ''
858
+ paren_count = 0
859
+ bracket_count = 0
860
+ brace_count = 0
861
+ in_quotes = False
862
+ quote_char = ''
863
+
864
+ i = 0
865
+ while i < len(content):
866
+ char = content[i]
867
+
868
+ if not in_quotes:
869
+ if char in ['"', "'"]:
870
+ in_quotes = True
871
+ quote_char = char
872
+ current += char
873
+ elif char == '(':
874
+ paren_count += 1
875
+ current += char
876
+ elif char == ')':
877
+ paren_count -= 1
878
+ current += char
879
+ elif char == '[':
880
+ bracket_count += 1
881
+ current += char
882
+ elif char == ']':
883
+ bracket_count -= 1
884
+ current += char
885
+ elif char == '{':
886
+ brace_count += 1
887
+ current += char
888
+ elif char == '}':
889
+ brace_count -= 1
890
+ current += char
891
+ elif char == ',' and paren_count == 0 and bracket_count == 0 and brace_count == 0:
892
+ # Comma at level 0 - variable separator
893
+ parts.append(current.strip())
894
+ current = ''
895
+ i += 1
896
+ continue
897
+ else:
898
+ current += char
899
+ else:
900
+ current += char
901
+ if char == quote_char and (i == 0 or content[i-1] != '\\'):
902
+ in_quotes = False
903
+ quote_char = ''
904
+
905
+ i += 1
906
+
907
+ # Add last variable
908
+ if current.strip():
909
+ parts.append(current.strip())
910
+
911
+ return parts
912
+
913
+ def _convert_php_to_js(self, php_value):
914
+ """Convert PHP array syntax to JavaScript object/array syntax using php -r"""
915
+ php_value = php_value.strip()
916
+
917
+ # Handle string literals
918
+ if (php_value.startswith("'") and php_value.endswith("'")) or \
919
+ (php_value.startswith('"') and php_value.endswith('"')):
920
+ return php_value
921
+
922
+ # Handle numbers
923
+ if php_value.isdigit() or (php_value.startswith('-') and php_value[1:].isdigit()):
924
+ return php_value
925
+
926
+ # Handle booleans
927
+ if php_value.lower() in ['true', 'false']:
928
+ return php_value.lower()
929
+
930
+ # Handle null
931
+ if php_value.lower() == 'null':
932
+ return 'null'
933
+
934
+ # Handle arrays - sử dụng php -r để convert
935
+ if php_value.startswith('[') and php_value.endswith(']'):
936
+ # Sử dụng php -r để convert array
937
+ json_result = convert_php_array_with_php_r(php_value)
938
+ if json_result is not None:
939
+ return json_result
940
+
941
+ # Fallback to old method if php -r fails
942
+ return self._convert_php_array_legacy(php_value)
943
+
944
+ # Default: return as is
945
+ return php_value
946
+
947
+ def _convert_php_array_legacy(self, php_value):
948
+ """Legacy method for converting PHP arrays (fallback)"""
949
+ # Remove outer brackets
950
+ inner_content = php_value[1:-1].strip()
951
+ if not inner_content:
952
+ return '[]' # Empty array, not object
953
+
954
+ # Check if this is an associative array (has =>) or indexed array
955
+ has_arrow_operator = '=>' in inner_content
956
+
957
+ if has_arrow_operator:
958
+ # Associative array - convert to JavaScript object
959
+ js_content = inner_content
960
+ # Replace => with :
961
+ js_content = re.sub(r'\s*=>\s*', ': ', js_content)
962
+ # Replace single quotes with double quotes for keys
963
+ js_content = re.sub(r"'([^']+)'\s*:", r'"\1":', js_content)
964
+ # Replace single quotes with double quotes for string values
965
+ js_content = re.sub(r":\s*'([^']+)'", r': "\1"', js_content)
966
+
967
+ return '{' + js_content + '}'
968
+ else:
969
+ # Indexed array - keep as JavaScript array
970
+ js_content = inner_content
971
+ # Replace single quotes with double quotes for string values
972
+ js_content = re.sub(r"'([^']+)'", r'"\1"', js_content)
973
+
974
+ return '[' + js_content + ']'
975
+
976
+ def _find_first_equals(self, var):
977
+ """Find first = that's not inside quotes or brackets"""
978
+ paren_count = 0
979
+ bracket_count = 0
980
+ in_quotes = False
981
+ quote_char = ''
982
+
983
+ for i, char in enumerate(var):
984
+ if (char == '"' or char == "'") and not in_quotes:
985
+ in_quotes = True
986
+ quote_char = char
987
+ elif char == quote_char and in_quotes:
988
+ in_quotes = False
989
+ quote_char = ''
990
+ elif not in_quotes:
991
+ if char == '(':
992
+ paren_count += 1
993
+ elif char == ')':
994
+ paren_count -= 1
995
+ elif char == '[':
996
+ bracket_count += 1
997
+ elif char == ']':
998
+ bracket_count -= 1
999
+ elif char == '=' and paren_count == 0 and bracket_count == 0:
1000
+ return i
1001
+
1002
+ return -1
1003
+
1004
+ def parse_register(self, blade_code):
1005
+ """Parse @register directive hoặc aliases (@setup, @script)
1006
+
1007
+ NOTE: This method should NOT find @register inside @verbatim blocks
1008
+ because @verbatim blocks are already replaced with placeholders in main_compiler.py
1009
+ before this method is called. This ensures @verbatim has absolute priority.
1010
+ """
1011
+ # Skip verbatim placeholders to ensure we don't process anything inside them
1012
+ # (Even though they should already be replaced, this is a safety check)
1013
+ blade_code_filtered = re.sub(r'__VERBATIM_BLOCK_\d+__', '', blade_code)
1014
+
1015
+ # Tìm @register trước (có thể có hoặc không có parameters)
1016
+ register_match = re.search(r'@register(?:\s*\([^)]*\))?(.*?)@endregister', blade_code_filtered, re.DOTALL | re.IGNORECASE)
1017
+ if register_match:
1018
+ return register_match.group(1).strip()
1019
+
1020
+ # Tìm @setup (alias của @register, có thể có hoặc không có parameters)
1021
+ setup_match = re.search(r'@setup(?:\s*\([^)]*\))?(.*?)@endsetup', blade_code_filtered, re.DOTALL | re.IGNORECASE)
1022
+ if setup_match:
1023
+ return setup_match.group(1).strip()
1024
+
1025
+ # Tìm @script (xử lý như @register)
1026
+ script_match = re.search(r'@script\s*(?:\([^)]*\))?(.*?)@endscript', blade_code_filtered, re.DOTALL | re.IGNORECASE)
1027
+ if script_match:
1028
+ return script_match.group(1).strip()
1029
+
1030
+ return None
1031
+
1032
+ def parse_view_type(self, blade_code):
1033
+ """Parse @viewType/@viewtype directive"""
1034
+ # Match both @viewType and @viewtype (case insensitive)
1035
+ viewtype_match = re.search(r'@viewtype\s*\(', blade_code, re.IGNORECASE)
1036
+ if not viewtype_match:
1037
+ return None
1038
+
1039
+ start_pos = viewtype_match.end() - 1
1040
+ viewtype_content, _ = extract_balanced_parentheses(blade_code, start_pos)
1041
+ if not viewtype_content:
1042
+ return None
1043
+
1044
+ viewtype_content = viewtype_content.strip()
1045
+
1046
+ # Extract the parameter value
1047
+ param_value = self._extract_viewtype_parameter(viewtype_content)
1048
+ if not param_value:
1049
+ return None
1050
+
1051
+ # Normalize and determine view type
1052
+ view_type = self._normalize_view_type(param_value)
1053
+
1054
+ return {
1055
+ 'viewType': view_type,
1056
+ 'originalValue': param_value
1057
+ }
1058
+
1059
+ def _extract_viewtype_parameter(self, content):
1060
+ """Extract parameter value from @viewType directive"""
1061
+ content = content.strip()
1062
+
1063
+ # Handle string literals (single or double quotes)
1064
+ if (content.startswith("'") and content.endswith("'")) or (content.startswith('"') and content.endswith('"')):
1065
+ return content[1:-1] # Remove quotes
1066
+
1067
+ # Handle variables or expressions - convert to string
1068
+ if content.startswith('$'):
1069
+ # PHP variable - convert to JS
1070
+ return php_to_js(content)
1071
+
1072
+ # Handle function calls
1073
+ if '(' in content and ')' in content:
1074
+ return php_to_js(content)
1075
+
1076
+ # Handle arrays or other complex expressions
1077
+ if content.startswith('[') or content.startswith('{'):
1078
+ try:
1079
+ # Try to parse as JSON-like structure
1080
+ parsed = json.loads(content)
1081
+ if isinstance(parsed, (list, dict)):
1082
+ return str(parsed)
1083
+ except:
1084
+ pass
1085
+
1086
+ # Handle numbers, booleans, etc.
1087
+ if content.lower() in ['true', 'false', 'null']:
1088
+ return content.lower()
1089
+
1090
+ # Try to parse as number
1091
+ try:
1092
+ float(content)
1093
+ return content
1094
+ except:
1095
+ pass
1096
+
1097
+ # Default: treat as string
1098
+ return content
1099
+
1100
+ def _normalize_view_type(self, value):
1101
+ """Normalize view type value to standard types"""
1102
+ if not value:
1103
+ return 'view'
1104
+
1105
+ # Convert to lowercase string for comparison
1106
+ value_str = str(value).lower().strip()
1107
+
1108
+ # HTML Document types
1109
+ html_doc_types = [
1110
+ 'html', 'document', 'html-document', 'htmldocument',
1111
+ 'fullpage', 'finalhtml', 'webpage'
1112
+ ]
1113
+ if value_str in html_doc_types:
1114
+ return 'html-document'
1115
+
1116
+ # Layout types
1117
+ layout_types = [
1118
+ 'layout', 'view-layout', 'view/layout'
1119
+ ]
1120
+ if value_str in layout_types:
1121
+ return 'layout'
1122
+
1123
+ # View types
1124
+ view_types = [
1125
+ 'view', 'page', 'viewpage', 'view-page', 'view/page'
1126
+ ]
1127
+ if value_str in view_types:
1128
+ return 'view'
1129
+
1130
+ # Template types
1131
+ template_types = [
1132
+ 'template', 'view-template', 'temp', 'tpl', 'viewtpl', 'view/template'
1133
+ ]
1134
+ if value_str in template_types:
1135
+ return 'template'
1136
+
1137
+ # Component types
1138
+ component_types = [
1139
+ 'component', 'compunent' # Handle typo
1140
+ ]
1141
+ if value_str in component_types:
1142
+ return 'component'
1143
+
1144
+ # Default fallback
1145
+ return 'view'
1146
+
1147
+ def _validate_block_directives(self, blade_code):
1148
+ """Validate that @block directives have matching @endblock/@endBlock directives"""
1149
+ # Remove @verbatim blocks to avoid checking directives inside them
1150
+ blade_code_filtered = self._remove_verbatim_blocks(blade_code)
1151
+
1152
+ # Check for nested blocks and validate order
1153
+ # We'll use a simple stack-based approach to check nesting
1154
+ lines = blade_code_filtered.split('\n')
1155
+ stack = []
1156
+
1157
+ for line_num, line in enumerate(lines, 1):
1158
+ # Check for @block
1159
+ block_match = re.search(r'@block\s*\(', line, re.IGNORECASE)
1160
+ if block_match:
1161
+ # Extract block name if possible
1162
+ block_name_match = re.search(r'@block\s*\(\s*[\'"]([^\'"]*)[\'"]', line, re.IGNORECASE)
1163
+ block_name = block_name_match.group(1) if block_name_match else f"block_{len(stack)}"
1164
+ stack.append(('block', block_name, line_num))
1165
+
1166
+ # Check for @endblock/@endBlock
1167
+ endblock_match = re.search(r'@endblock\b|@endBlock\b', line, re.IGNORECASE)
1168
+ if endblock_match:
1169
+ if not stack:
1170
+ error_msg = f"Lỗi tại dòng {line_num}: Tìm thấy @endblock/@endBlock nhưng không có @block tương ứng"
1171
+ raise ValueError(error_msg)
1172
+ stack.pop()
1173
+
1174
+ # Check if there are unclosed blocks
1175
+ if stack:
1176
+ unclosed_blocks = [f"'{name}' (dòng {line_num})" for _, name, line_num in stack]
1177
+ error_msg = f"Lỗi: Có {len(stack)} block chưa được đóng: {', '.join(unclosed_blocks)}"
1178
+ raise ValueError(error_msg)
1179
+
1180
+ return True
1181
+
1182
+ def parse_block_directives(self, blade_code):
1183
+ """Parse @block directive - now handled as section in template processor"""
1184
+ # Validate block directives before processing
1185
+ self._validate_block_directives(blade_code)
1186
+ # @block is now handled in template_processor.py as a section
1187
+ return blade_code
1188
+
1189
+ def parse_endblock_directives(self, blade_code):
1190
+ """Parse @endblock/@endBlock directive - now handled as section in template processor"""
1191
+ # Validation is done in parse_block_directives
1192
+ # @endblock is now handled in template_processor.py as a section
1193
+ return blade_code
1194
+
1195
+ def parse_useblock_directives(self, blade_code):
1196
+ """Parse @useBlock/@useblock directive with optional defaultValue"""
1197
+ # Pattern to match @useBlock/@useblock with optional defaultValue
1198
+ pattern = r'@(useBlock|useblock)\s*\(\s*([^,)]+)(?:\s*,\s*([^)]+))?\s*\)'
1199
+
1200
+ def replace_useblock(match):
1201
+ name_expr = match.group(2).strip()
1202
+ default_expr = match.group(3).strip() if match.group(3) else None
1203
+
1204
+ # Convert name to JavaScript
1205
+ js_name = self._convert_php_to_js(name_expr)
1206
+ # Remove $ prefix from variables
1207
+ js_name = re.sub(r'\$(\w+)', r'\1', js_name)
1208
+
1209
+ # Convert defaultValue to JavaScript if provided
1210
+ if default_expr:
1211
+ js_default = self._convert_php_to_js(default_expr)
1212
+ # Remove $ prefix from variables
1213
+ js_default = re.sub(r'\$(\w+)', r'\1', js_default)
1214
+ return f"${{this.useBlock({js_name}, {js_default})}}"
1215
+ else:
1216
+ return f"${{this.useBlock({js_name})}}"
1217
+
1218
+ return re.sub(pattern, replace_useblock, blade_code)
1219
+
1220
+ def parse_onblock_directives(self, blade_code):
1221
+ """Parse @onBlock/@onblock/@onBlockChange directives with subscribeBlock"""
1222
+ # Pattern to match @onBlock/@onblock/@onBlockChange with parameters
1223
+ pattern = r'@(onBlock|onblock|onBlockChange)\s*\(\s*([^)]+)\s*\)'
1224
+
1225
+ def replace_onblock(match):
1226
+ directive = match.group(1)
1227
+ params_expr = match.group(2).strip()
1228
+
1229
+ # Parse parameters - could be string, variable, or array
1230
+ if params_expr.startswith('[') and params_expr.endswith(']'):
1231
+ # Array syntax: ['#children' => 'document.body', 'title' => 'block-title']
1232
+ # Convert PHP array to JavaScript object
1233
+ from php_converter import convert_php_array_to_json
1234
+ js_params = convert_php_array_to_json(params_expr)
1235
+ # Format with proper spacing
1236
+ js_params = re.sub(r'":', r'": ', js_params)
1237
+ js_params = re.sub(r',"', r', "', js_params)
1238
+ return f"${{this.subscribeBlock({js_params})}}"
1239
+ else:
1240
+ # Single parameter: 'title' or $blockName
1241
+ js_param = self._convert_php_to_js(params_expr)
1242
+ # Remove $ prefix from variables
1243
+ js_param = re.sub(r'\$(\w+)', r'\1', js_param)
1244
+ return f"${{this.subscribeBlock({js_param})}}"
1245
+
1246
+ return re.sub(pattern, replace_onblock, blade_code)
1247
+
1248
+ def _parse_block_expression(self, expression):
1249
+ """Parse block expression to extract name and attributes"""
1250
+ # Find first comma outside quotes and brackets
1251
+ comma_pos = self._find_outer_comma(expression)
1252
+
1253
+ if comma_pos == -1:
1254
+ # Only block name, no attributes
1255
+ name = expression.strip()
1256
+ return {'name': self._convert_php_to_js(name), 'attributes': None}
1257
+
1258
+ # Split name and attributes
1259
+ name_part = expression[:comma_pos].strip()
1260
+ attributes_part = expression[comma_pos + 1:].strip()
1261
+
1262
+ name = self._convert_php_to_js(name_part)
1263
+ attributes = self._convert_block_attributes(attributes_part)
1264
+
1265
+ return {'name': name, 'attributes': attributes}
1266
+
1267
+ def _convert_block_attributes(self, attributes_expr):
1268
+ """Convert block attributes to JavaScript"""
1269
+ attributes_expr = attributes_expr.strip()
1270
+
1271
+ # Check if it's array syntax
1272
+ if attributes_expr.startswith('[') and attributes_expr.endswith(']'):
1273
+ # Convert PHP array to JavaScript object
1274
+ return convert_php_array_to_json(attributes_expr)
1275
+ else:
1276
+ # Convert PHP expression to JavaScript
1277
+ return self._convert_php_to_js(attributes_expr)
1278
+
1279
+ def _find_outer_comma(self, expression):
1280
+ """Find first comma outside quotes and brackets"""
1281
+ in_quotes = False
1282
+ quote_char = None
1283
+ bracket_count = 0
1284
+ paren_count = 0
1285
+
1286
+ for i, char in enumerate(expression):
1287
+ if not in_quotes:
1288
+ if char in ['"', "'"]:
1289
+ in_quotes = True
1290
+ quote_char = char
1291
+ elif char == '[':
1292
+ bracket_count += 1
1293
+ elif char == ']':
1294
+ bracket_count -= 1
1295
+ elif char == '(':
1296
+ paren_count += 1
1297
+ elif char == ')':
1298
+ paren_count -= 1
1299
+ elif char == ',' and bracket_count == 0 and paren_count == 0:
1300
+ return i
1301
+ else:
1302
+ if char == quote_char and (i == 0 or expression[i-1] != '\\'):
1303
+ in_quotes = False
1304
+ quote_char = None
1305
+
1306
+ return -1
1307
+
1308
+ def _is_simple_string_literal(self, view_expr):
1309
+ """Check if view_expr is a simple string literal without variables or function calls"""
1310
+ view_expr = view_expr.strip()
1311
+
1312
+ # Must start and end with quotes
1313
+ if not ((view_expr.startswith('"') and view_expr.endswith('"')) or
1314
+ (view_expr.startswith("'") and view_expr.endswith("'"))):
1315
+ return False
1316
+
1317
+ # Check for variables ($) or function calls (parentheses)
1318
+ if '$' in view_expr or '(' in view_expr:
1319
+ return False
1320
+
1321
+ return True
1322
+
1323
+ def _convert_extends_expression(self, view_expr):
1324
+ """Convert @extends expression to JavaScript"""
1325
+ view_expr = view_expr.strip()
1326
+
1327
+ # Handle string with variables: "theme.layout" -> `theme+'.layout'`
1328
+ if view_expr.startswith('"') and view_expr.endswith('"'):
1329
+ inner_content = view_expr[1:-1]
1330
+ # Convert PHP string concatenation to JavaScript
1331
+ js_expr = self._convert_php_string_concat(inner_content)
1332
+ return f"`{js_expr}`"
1333
+
1334
+ # Handle single quotes: 'theme.layout' -> `theme+'.layout'`
1335
+ elif view_expr.startswith("'") and view_expr.endswith("'"):
1336
+ inner_content = view_expr[1:-1]
1337
+ js_expr = self._convert_php_string_concat(inner_content)
1338
+ return f"`{js_expr}`"
1339
+
1340
+ # Handle function calls first: test('.def')
1341
+ elif '(' in view_expr and ')' in view_expr:
1342
+ # Function call - use direct conversion
1343
+ return php_to_js(view_expr)
1344
+
1345
+ # Handle variables and concatenation: $theme.'.layout', $abc.'.def'.$Abxy
1346
+ elif '.' in view_expr and ('$' in view_expr or '"' in view_expr or "'" in view_expr):
1347
+ # Has concatenation - convert to template literal with View.execute
1348
+ js_expr = self._convert_php_string_concat(view_expr)
1349
+ return f"View.execute(() => `{js_expr}`)"
1350
+
1351
+ # Handle simple variables: $theme
1352
+ else:
1353
+ # Check if it contains undefined variables (starts with $ and not in declared vars)
1354
+ if view_expr.startswith('$') and not self._is_variable_declared(view_expr):
1355
+ return f"View.execute(() => {php_to_js(view_expr)})"
1356
+ else:
1357
+ return php_to_js(view_expr)
1358
+
1359
+ def _convert_php_string_concat(self, content):
1360
+ """Convert PHP string concatenation to JavaScript template literal"""
1361
+ # Use regex to find all parts separated by . outside quotes
1362
+ import re
1363
+
1364
+ # Pattern to match: variable, string literal, or function call
1365
+ pattern = r'(\$[a-zA-Z_][a-zA-Z0-9_]*|"[^"]*"|\'[^\']*\'|[a-zA-Z_][a-zA-Z0-9_]*\([^)]*\))'
1366
+ parts = re.findall(pattern, content)
1367
+
1368
+ if not parts:
1369
+ # Fallback: try to split by . and process each part
1370
+ parts = content.split('.')
1371
+
1372
+ # Convert parts to JavaScript
1373
+ js_parts = []
1374
+ for part in parts:
1375
+ part = part.strip()
1376
+ if part.startswith('"') and part.endswith('"'):
1377
+ # String literal
1378
+ js_parts.append(f'"{part[1:-1]}"')
1379
+ elif part.startswith("'") and part.endswith("'"):
1380
+ # String literal
1381
+ js_parts.append(f"'{part[1:-1]}'")
1382
+ elif part.startswith('$'):
1383
+ # Variable
1384
+ var_name = part[1:]
1385
+ js_parts.append(f"{var_name}")
1386
+ elif '(' in part and ')' in part:
1387
+ # Function call
1388
+ js_parts.append(f"{php_to_js(part)}")
1389
+ else:
1390
+ # Other expression
1391
+ js_parts.append(f"{php_to_js(part)}")
1392
+
1393
+ # Wrap the entire expression in template literal
1394
+ return f"${{{'+'.join(js_parts)}}}"
1395
+
1396
+ def _is_variable_declared(self, view_expr):
1397
+ """Check if a variable is declared in the template"""
1398
+ # Extract variable name from $variable
1399
+ if view_expr.startswith('$'):
1400
+ var_name = view_expr[1:].split('.')[0] # Get first part before any dots
1401
+
1402
+ # Check if variable is declared in @vars, @let, @const, or @useState
1403
+ # This is a simple check - in a real implementation, you'd need to track
1404
+ # all declared variables from the template
1405
+ declared_vars = self._get_declared_variables()
1406
+ return var_name in declared_vars
1407
+
1408
+ return True # Non-variable expressions are considered "declared"
1409
+
1410
+ def _get_declared_variables(self):
1411
+ """Get list of declared variables from the template"""
1412
+ # This is a simplified version - in practice, you'd need to track
1413
+ # variables declared in @vars, @let, @const, @useState directives
1414
+ # For now, return common variables that are typically declared
1415
+ return {
1416
+ 'user', 'userState', 'setUserState', 'isEditModalOpen', 'setIsEditModalOpen',
1417
+ 'posts', 'setPosts', 'loading', 'setLoading', 'data', 'config'
1418
+ }