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,1167 @@
1
+ """
2
+ Template processor chính xử lý template content
3
+ """
4
+
5
+ import re
6
+ from utils import extract_balanced_parentheses
7
+ from conditional_handlers import ConditionalHandlers
8
+ from loop_handlers import LoopHandlers
9
+ from section_handlers import SectionHandlers
10
+ from template_processors import TemplateProcessors
11
+ from directive_processors import DirectiveProcessor
12
+ from event_directive_processor import EventDirectiveProcessor
13
+ from echo_processor import EchoProcessor
14
+ from class_binding_handler import ClassBindingHandler
15
+
16
+ class TemplateProcessor:
17
+ def __init__(self, usestate_variables=None):
18
+ self.state_variables = usestate_variables or set()
19
+ self.watch_counter = 0 # Counter for generating unique watch IDs
20
+ self.conditional_handlers = ConditionalHandlers(self.state_variables, self)
21
+ self.loop_handlers = LoopHandlers(self.state_variables, self)
22
+ self.section_handlers = SectionHandlers()
23
+ self.template_processors = TemplateProcessors()
24
+ self.directive_processors = DirectiveProcessor()
25
+ self.event_processor = EventDirectiveProcessor(self.state_variables)
26
+ self.echo_processor = EchoProcessor(self.state_variables)
27
+ self.class_binding_handler = ClassBindingHandler(self.state_variables)
28
+
29
+ def _is_attribute_directive(self, line):
30
+ """
31
+ Detect if directive is in attribute context (inside tag attributes)
32
+ Example: <div @if(condition)> - attribute directive (return True)
33
+ Example: <div>@if(condition) - block directive (return False)
34
+ """
35
+ # Find position of directive
36
+ directive_patterns = [r'@if\b', r'@foreach\b', r'@for\b', r'@while\b', r'@switch\b']
37
+ directive_pos = -1
38
+ for pattern in directive_patterns:
39
+ match = re.search(pattern, line)
40
+ if match:
41
+ directive_pos = match.start()
42
+ break
43
+
44
+ if directive_pos == -1:
45
+ return False
46
+
47
+ # Find the LAST opening < before the directive
48
+ before_directive = line[:directive_pos]
49
+ last_open_tag = before_directive.rfind('<')
50
+
51
+ if last_open_tag == -1:
52
+ return False
53
+
54
+ # Check if there's a closing > between last_open_tag and directive
55
+ between = line[last_open_tag:directive_pos]
56
+ if '>' in between:
57
+ # There's a tag closing between last < and directive
58
+ # So directive is NOT in attributes
59
+ return False
60
+
61
+ # Directive is between < and (potential) >, so it's inline
62
+ return True
63
+
64
+ def _process_inline_directive(self, line):
65
+ """
66
+ Process inline directives in HTML attributes
67
+ Example: <div @if($count > 0) data-test="active" @endif>
68
+ Becomes: <div ${this.__execute(() => { if(count > 0){ return `data-test="active"`; } return ''; })}>
69
+ """
70
+ from php_converter import php_to_js
71
+
72
+ # Pattern for @if...@endif inline
73
+ if_pattern = r'@if\s*\((.*?)\)\s*(.*?)\s*@endif'
74
+ def replace_inline_if(match):
75
+ condition_php = match.group(1).strip()
76
+ content = match.group(2).strip()
77
+ condition_js = php_to_js(condition_php)
78
+
79
+ # Compile to expression without __watch (inline directives are not reactive)
80
+ return f"${{this.__execute(() => {{ if({condition_js}){{ return `{content}`; }} return ''; }})}}"
81
+
82
+ line = re.sub(if_pattern, replace_inline_if, line)
83
+
84
+ # Pattern for @foreach...@endforeach inline
85
+ # Match: @foreach($items as $item) content @endforeach
86
+ # Also match: @foreach($key => $value as $item) content @endforeach
87
+ foreach_pattern = r'@foreach\s*\(\s*\\?[$](\w+)\s+as\s+(?:\\?[$](\w+)\s*=>\s*)?\\?[$](\w+)\s*\)\s*(.*?)@endforeach'
88
+ def replace_inline_foreach(match):
89
+ array_var = match.group(1) # items
90
+ key_var = match.group(2) # optional key
91
+ value_var = match.group(3) # item
92
+ content = match.group(4).strip()
93
+
94
+ # Convert PHP variables to JS
95
+ array_js = array_var # No need for php_to_js for simple variable names
96
+
97
+ # Note: Content may have {{ }} expressions - keep them for later echo processing
98
+ # For now, simple join with space
99
+ if key_var:
100
+ return f"${{this.__foreach({array_js}, ({value_var}, {key_var}) => `{content}`).join(' ')}}"
101
+ else:
102
+ return f"${{this.__foreach({array_js}, ({value_var}) => `{content}`).join(' ')}}"
103
+
104
+ line = re.sub(foreach_pattern, replace_inline_foreach, line, flags=re.DOTALL)
105
+
106
+ # Pattern for @for...@endfor inline
107
+ # Match: @for($i = 0; $i < 3; $i++) content @endfor
108
+ for_pattern = r'@for\s*\(\s*\\?[$](\w+)\s*=\s*([^;]+);\s*\\?[$]\1\s*([<>=!]+)\s*([^;]+);\s*\\?[$]\1\s*\+\+\s*\)\s*(.*?)@endfor'
109
+ def replace_inline_for(match):
110
+ var_name = match.group(1)
111
+ start_value_php = match.group(2).strip()
112
+ operator = match.group(3)
113
+ end_value_php = match.group(4).strip()
114
+ content = match.group(5).strip()
115
+
116
+ start_js = php_to_js(start_value_php)
117
+ end_js = php_to_js(end_value_php)
118
+
119
+ # Generate for loop with string concatenation
120
+ return f"${{this.__execute(() => {{ let __output = ''; for(let {var_name} = {start_js}; {var_name} {operator} {end_js}; {var_name}++) {{ __output += `{content}`; }} return __output; }})}}"
121
+
122
+ line = re.sub(for_pattern, replace_inline_for, line, flags=re.DOTALL)
123
+
124
+ # Pattern for @while...@endwhile inline
125
+ # Match: @while($condition) content @endwhile
126
+ while_pattern = r'@while\s*\((.*?)\)\s*(.*?)@endwhile'
127
+ def replace_inline_while(match):
128
+ condition_php = match.group(1).strip()
129
+ content = match.group(2).strip()
130
+ condition_js = php_to_js(condition_php)
131
+
132
+ # Generate while loop (careful with infinite loops!)
133
+ return f"${{this.__execute(() => {{ let __output = ''; let __iterations = 0; while({condition_js} && __iterations < 1000) {{ __output += `{content}`; __iterations++; }} return __output; }})}}"
134
+
135
+ line = re.sub(while_pattern, replace_inline_while, line, flags=re.DOTALL)
136
+
137
+ # Pattern for @switch...@endswitch inline (complex with @case/@break/@default)
138
+ # Match: @switch($var) @case(val1) content1 @break @case(val2) content2 @break @default contentD @endswitch
139
+ switch_pattern = r'@switch\s*\((.*?)\)\s*(.*?)@endswitch'
140
+ def replace_inline_switch(match):
141
+ switch_var_php = match.group(1).strip()
142
+ switch_content = match.group(2)
143
+ switch_var_js = php_to_js(switch_var_php)
144
+
145
+ # Parse @case/@default blocks
146
+ result = f"${{this.__execute(() => {{ let __output = ''; switch({switch_var_js}) {{"
147
+
148
+ # Find all @case directives
149
+ case_pattern = r'@case\s*\((.*?)\)\s*(.*?)(?:@break|@case|@default|$)'
150
+ for case_match in re.finditer(case_pattern, switch_content):
151
+ case_value_php = case_match.group(1).strip()
152
+ case_content = case_match.group(2).strip()
153
+ case_value_js = php_to_js(case_value_php)
154
+ result += f" case {case_value_js}: __output = `{case_content}`; break;"
155
+
156
+ # Find @default
157
+ default_pattern = r'@default\s*(.*?)(?:@endswitch|$)'
158
+ default_match = re.search(default_pattern, switch_content)
159
+ if default_match:
160
+ default_content = default_match.group(1).strip()
161
+ result += f" default: __output = `{default_content}`;"
162
+
163
+ # Close switch and function
164
+ result += " } return __output; })}}"
165
+ return result
166
+
167
+ line = re.sub(switch_pattern, replace_inline_switch, line, flags=re.DOTALL)
168
+
169
+ return line
170
+
171
+ def process_template(self, blade_code):
172
+ """Process template content and extract sections"""
173
+ # NOTE: @verbatim blocks are already processed in main_compiler.py before this step
174
+ # They are already replaced with placeholders (__VERBATIM_BLOCK_N__)
175
+ # So we don't need to process them again here
176
+
177
+ # Remove page/document directives - these are only for PHP compilation, not JS
178
+ # @pageStart, @pageEnd, @pageOpen, @pageClose, @docStart, @docEnd
179
+ blade_code = self._remove_page_directives(blade_code)
180
+
181
+ # Remove already processed directives
182
+ blade_code = re.sub(r'@extends\s*\([^)]*\)', '', blade_code, flags=re.DOTALL)
183
+ blade_code = re.sub(r'@vars\s*\([^)]*\)', '', blade_code, flags=re.DOTALL)
184
+
185
+ # Remove @let directives with balanced parentheses
186
+ let_pattern = r'@let\s*\('
187
+ while re.search(let_pattern, blade_code):
188
+ match = re.search(let_pattern, blade_code)
189
+ if match:
190
+ start_pos = match.end() - 1
191
+ content, end_pos = extract_balanced_parentheses(blade_code, start_pos)
192
+ if content is not None:
193
+ blade_code = blade_code[:match.start()] + blade_code[start_pos + len(content) + 2:]
194
+ else:
195
+ break
196
+
197
+ # Remove @const directives with balanced parentheses
198
+ const_pattern = r'@const\s*\('
199
+ while re.search(const_pattern, blade_code):
200
+ match = re.search(const_pattern, blade_code)
201
+ if match:
202
+ start_pos = match.end() - 1
203
+ content, end_pos = extract_balanced_parentheses(blade_code, start_pos)
204
+ if content is not None:
205
+ blade_code = blade_code[:match.start()] + blade_code[start_pos + len(content) + 2:]
206
+ else:
207
+ break
208
+
209
+ # Remove @useState directives with balanced parentheses
210
+ usestate_pattern = r'@useState\s*\('
211
+ while re.search(usestate_pattern, blade_code):
212
+ match = re.search(usestate_pattern, blade_code)
213
+ if match:
214
+ start_pos = match.end() - 1
215
+ content, end_pos = extract_balanced_parentheses(blade_code, start_pos)
216
+ if content is not None:
217
+ blade_code = blade_code[:match.start()] + blade_code[start_pos + len(content) + 2:]
218
+ else:
219
+ break
220
+ # Remove @fetch directive with balanced parentheses
221
+ fetch_pattern = r'@fetch\s*\('
222
+ while re.search(fetch_pattern, blade_code):
223
+ match = re.search(fetch_pattern, blade_code)
224
+ if match:
225
+ start_pos = match.end() - 1
226
+ fetch_content, end_pos = extract_balanced_parentheses(blade_code, start_pos)
227
+ if fetch_content is not None:
228
+ blade_code = blade_code[:match.start()] + blade_code[start_pos + len(fetch_content) + 2:]
229
+ else:
230
+ break
231
+ blade_code = re.sub(r'@await\s*\([^)]*\)', '', blade_code, flags=re.DOTALL)
232
+
233
+ # Process @include directives (multiline support) BEFORE processing line by line
234
+ blade_code = self._process_multiline_include_directives(blade_code)
235
+
236
+ blade_code = re.sub(r'@oninit.*?@endoninit', '', blade_code, flags=re.DOTALL | re.IGNORECASE)
237
+ # Remove @register directive - với hoặc không có parameters
238
+ blade_code = re.sub(r'@register\s*(?:\([^)]*\))?.*?@endregister', '', blade_code, flags=re.DOTALL | re.IGNORECASE)
239
+
240
+ # Remove @setup directive (alias of @register) - với hoặc không có parameters
241
+ blade_code = re.sub(r'@setup\s*(?:\([^)]*\))?.*?@endsetup', '', blade_code, flags=re.DOTALL | re.IGNORECASE)
242
+
243
+ # Remove @script directive (xử lý như @register)
244
+ blade_code = re.sub(r'@script\s*(?:\([^)]*\))?.*?@endscript', '', blade_code, flags=re.DOTALL | re.IGNORECASE)
245
+
246
+ # Process inline directives in HTML attributes (before echo processing)
247
+ # Example: <div @if($count > 0) data-test="active" @endif>
248
+ lines = blade_code.splitlines()
249
+ processed_lines = []
250
+ for line in lines:
251
+ if self._is_attribute_directive(line):
252
+ line = self._process_inline_directive(line)
253
+ processed_lines.append(line)
254
+ blade_code = '\n'.join(processed_lines)
255
+
256
+ # Process echo expressions {{ }} and {!! !!} with intelligent context-aware handling
257
+ blade_code = self.echo_processor.process_echo_expressions(blade_code)
258
+
259
+ lines = blade_code.splitlines()
260
+ output = []
261
+ sections = []
262
+ stack = []
263
+ skip_until = None
264
+ remove_directive_markers = False
265
+ in_pre_tag = False # Track if we're inside <pre> tags
266
+
267
+ i = 0
268
+ while i < len(lines):
269
+ original_line = lines[i]
270
+
271
+ # Check if we're entering or leaving <pre> tags
272
+ if '<pre>' in original_line or '<pre ' in original_line:
273
+ in_pre_tag = True
274
+ if '</pre>' in original_line:
275
+ in_pre_tag = False
276
+
277
+ # Preserve whitespace inside <pre> tags
278
+ if in_pre_tag:
279
+ line = original_line.rstrip() # Only remove trailing whitespace
280
+ else:
281
+ line = original_line.strip() # Normal processing
282
+
283
+ if not line:
284
+ # Don't add empty lines in loops
285
+ if not (stack and stack[-1][0] in ['for', 'while']):
286
+ output.append('')
287
+ i += 1
288
+ continue
289
+
290
+ # Handle skip modes
291
+ if skip_until:
292
+ if skip_until == '@endserverside':
293
+ # Check for all endserverside aliases
294
+ endserverside_aliases = [
295
+ '@endserverside', '@endServerSide', '@endSSR', '@endSsr',
296
+ '@EndSSR', '@EndSsr', '@endssr'
297
+ ]
298
+ if any(line.startswith(alias) for alias in endserverside_aliases):
299
+ skip_until = None
300
+ remove_directive_markers = False
301
+ # Skip the end directive line
302
+ i += 1
303
+ continue
304
+ elif skip_until == '@endclientside':
305
+ # Check for all endclientside aliases
306
+ endclientside_aliases = [
307
+ '@endclientside', '@endClientSide', '@endcsr', '@endCSR',
308
+ '@endCsr', '@endusecsr', '@endUseCSR', '@endUseCsr'
309
+ ]
310
+ if any(line.startswith(alias) for alias in endclientside_aliases):
311
+ skip_until = None
312
+ remove_directive_markers = False
313
+ # Skip the end directive line
314
+ i += 1
315
+ continue
316
+ elif remove_directive_markers:
317
+ # Process line normally but remove directive markers
318
+ processed_line = self.template_processors.process_template_line(line)
319
+ output.append(processed_line)
320
+ else:
321
+ # Original logic for other skip modes
322
+ if line.startswith(skip_until):
323
+ skip_until = None
324
+ remove_directive_markers = False
325
+ # Skip the end directive line
326
+ i += 1
327
+ continue
328
+ i += 1
329
+ continue
330
+
331
+ # Check for multiline @view/@template directives
332
+ if self._is_incomplete_view_directive(line):
333
+ # Join with next lines until complete
334
+ complete_line, lines_joined = self._join_multiline_view_directive(lines, i)
335
+ if complete_line:
336
+ # Process through template processor (not directive processor)
337
+ processed = self.template_processors.process_template_line(complete_line)
338
+ if processed:
339
+ output.append(processed)
340
+ # Skip the lines that were joined
341
+ i += lines_joined
342
+ continue
343
+
344
+ # Check for multiline event directives
345
+ if self._is_incomplete_event_directive(line):
346
+ # Join with next lines until complete
347
+ complete_line, lines_joined = self._join_multiline_event_directive(lines, i)
348
+ if complete_line:
349
+ processed = self._process_line_directives(complete_line, stack, output, sections)
350
+ if processed:
351
+ output.append(processed)
352
+ # Skip the lines that were joined
353
+ i += lines_joined
354
+ continue
355
+
356
+ # Process directives
357
+ processed = self._process_line_directives(line, stack, output, sections)
358
+ if processed:
359
+ if processed == 'skip_until_@endserverside':
360
+ skip_until = '@endserverside'
361
+ # Skip the @ssr line itself
362
+ i += 1
363
+ continue
364
+ elif processed == 'remove_directive_markers_until_@endclientside':
365
+ skip_until = '@endclientside'
366
+ remove_directive_markers = True
367
+ # Skip the @csr line itself
368
+ i += 1
369
+ continue
370
+ elif processed is not True:
371
+ if stack and stack[-1][0] in ['for', 'while']:
372
+ # Inside a loop, we need to append to the loop's output variable
373
+ loop_type = stack[-1][0]
374
+ output_var = f"__{loop_type}OutputContent__"
375
+
376
+ # Check if the last item in output is an incomplete template string for this loop
377
+ if output and isinstance(output[-1], str) and output[-1].startswith(output_var) and not output[-1].endswith('`;'):
378
+ # Append to the existing template string
379
+ output[-1] = output[-1].rstrip('`') + '\n' + processed + '`;'
380
+ else:
381
+ # Start a new template string
382
+ output.append(f"{output_var} += `{processed}`;")
383
+ else:
384
+ output.append(processed)
385
+ i += 1
386
+ continue
387
+
388
+ # Check if we're inside a php block first
389
+ if stack and stack[-1][0] == 'php':
390
+ # Convert PHP to JavaScript
391
+ if line.strip():
392
+ from php_converter import php_to_js
393
+ js_code = php_to_js(line)
394
+ processed_line = f" {js_code}"
395
+ else:
396
+ # Skip empty lines in php blocks
397
+ i += 1
398
+ continue
399
+ else:
400
+ # Process regular content
401
+ processed_line = self.template_processors.process_template_line(line)
402
+
403
+ # Check if we're inside a loop that needs output variable
404
+ if stack and stack[-1][0] in ['for', 'while']:
405
+ # Skip empty lines in loops completely
406
+ if not processed_line.strip():
407
+ i += 1
408
+ continue
409
+
410
+ # Get loop info first
411
+ loop_type = stack[-1][0]
412
+ output_var = f"__{loop_type}OutputContent__"
413
+
414
+ # Handle @endwhile/@endfor in loops
415
+ if line.startswith('@endwhile') or line.startswith('@endfor'):
416
+ # Process the end directive
417
+ processed = self._process_line_directives(line, stack, output, sections)
418
+ if processed:
419
+ output.append(processed)
420
+ i += 1
421
+ continue
422
+
423
+ # Handle @php ... @endphp as a pair directive in loops
424
+ if line.startswith('@php'):
425
+ # Find the matching @endphp
426
+ php_content = []
427
+ j = i + 1
428
+ while j < len(lines) and not lines[j].strip().startswith('@endphp'):
429
+ if lines[j].strip():
430
+ php_content.append(lines[j].strip())
431
+ j += 1
432
+
433
+ if j < len(lines) and lines[j].strip().startswith('@endphp'):
434
+ # Found matching @endphp, process the entire php block
435
+ if php_content:
436
+ from php_converter import php_to_js
437
+ js_code = '\n'.join([php_to_js(php_line) for php_line in php_content])
438
+ processed_line = f"${{APP.View.execute(() => {{\n {js_code}\n}})}}"
439
+ else:
440
+ processed_line = ""
441
+
442
+ # Skip to after @endphp
443
+ i = j
444
+ else:
445
+ # No matching @endphp found, treat as regular content
446
+ processed_line = self.template_processors.process_template_line(line)
447
+ else:
448
+ # Regular content
449
+ processed_line = self.template_processors.process_template_line(line)
450
+
451
+ # Now append processed_line to the loop's output variable
452
+ # Check if the last item in output is an incomplete template string for this loop
453
+ if output and isinstance(output[-1], str) and output[-1].startswith(output_var) and not output[-1].endswith('`;'):
454
+ # Append to the existing template string
455
+ output[-1] = output[-1].rstrip('`') + '\n' + processed_line + '`;'
456
+ else:
457
+ # Start a new template string
458
+ output.append(f"{output_var} += `{processed_line}`;")
459
+
460
+ i += 1
461
+ continue
462
+
463
+ output.append(processed_line)
464
+
465
+ i += 1
466
+
467
+ # Filter out boolean values and join strings
468
+ template_content = '\n'.join([str(item) for item in output if isinstance(item, str)])
469
+
470
+ # Process @class directive AFTER all other directives have been processed
471
+ template_content = self.class_binding_handler.process_class_directive(template_content)
472
+
473
+ # Replace __INCLUDE_WATCH_PLACEHOLDER__ with actual watch IDs
474
+ # This ensures @include directives processed early get proper sequential watch IDs
475
+ while '__INCLUDE_WATCH_PLACEHOLDER__' in template_content:
476
+ self.watch_counter += 1
477
+ # Replace placeholder (which is already quoted) with watch ID including __VIEW_ID__
478
+ template_content = template_content.replace("'__INCLUDE_WATCH_PLACEHOLDER__'",
479
+ f"`${{__VIEW_ID__}}-watch-{self.watch_counter}`", 1)
480
+
481
+ # NOTE: Verbatim blocks restoration is handled in main_compiler.py
482
+ # after all processing is complete, so we don't restore them here
483
+
484
+ return template_content, sections
485
+
486
+ def _process_event_directives(self, line):
487
+ """Process event directives (@click, @change, @submit, etc.) and aliases (@onClick, @onChange...)"""
488
+ # List of event types to check - comprehensive DOM events
489
+ event_types = [
490
+ # Mouse Events
491
+ 'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout', 'mousemove',
492
+ 'mouseenter', 'mouseleave', 'wheel', 'auxclick',
493
+
494
+ # Keyboard Events
495
+ 'keydown', 'keyup', 'keypress',
496
+
497
+ # Form Events
498
+ 'input', 'change', 'submit', 'reset', 'invalid', 'search',
499
+
500
+ # Focus Events
501
+ 'focus', 'blur', 'focusin', 'focusout',
502
+
503
+ # Selection Events
504
+ 'select', 'selectstart', 'selectionchange',
505
+
506
+ # Touch Events
507
+ 'touchstart', 'touchmove', 'touchend', 'touchcancel',
508
+
509
+ # Drag & Drop Events
510
+ 'dragstart', 'drag', 'dragend', 'dragenter', 'dragleave', 'dragover', 'drop',
511
+
512
+ # Media Events
513
+ 'play', 'pause', 'ended', 'loadstart', 'loadeddata', 'loadedmetadata', 'canplay',
514
+ 'canplaythrough', 'waiting', 'seeking', 'seeked', 'ratechange', 'durationchange',
515
+ 'volumechange', 'suspend', 'stalled', 'progress', 'emptied', 'encrypted', 'wakeup',
516
+
517
+ # Window Events
518
+ 'load', 'unload', 'beforeunload', 'resize', 'scroll', 'orientationchange',
519
+ 'visibilitychange', 'pagehide', 'pageshow', 'popstate', 'hashchange', 'online', 'offline',
520
+
521
+ # Document Events
522
+ 'DOMContentLoaded', 'readystatechange',
523
+
524
+ # Error Events
525
+ 'error', 'abort',
526
+
527
+ # Context Menu
528
+ 'contextmenu',
529
+
530
+ # Animation Events
531
+ 'animationstart', 'animationend', 'animationiteration',
532
+
533
+ # Transition Events
534
+ 'transitionstart', 'transitionend', 'transitionrun', 'transitioncancel',
535
+
536
+ # Pointer Events (Modern browsers)
537
+ 'pointerdown', 'pointerup', 'pointermove', 'pointerover', 'pointerout',
538
+ 'pointerenter', 'pointerleave', 'pointercancel', 'gotpointercapture', 'lostpointercapture',
539
+
540
+ # Fullscreen Events
541
+ 'fullscreenchange', 'fullscreenerror',
542
+
543
+ # Clipboard Events
544
+ 'copy', 'cut', 'paste',
545
+
546
+ # Gamepad Events
547
+ 'gamepadconnected', 'gamepaddisconnected',
548
+
549
+ # Battery Events
550
+ 'batterychargingchange', 'batterylevelchange',
551
+
552
+ # Device Orientation Events
553
+ 'deviceorientation', 'devicemotion', 'devicelight', 'deviceproximity',
554
+
555
+ # WebGL Events
556
+ 'webglcontextlost', 'webglcontextrestored'
557
+ ]
558
+
559
+ result = line
560
+ changed = False
561
+
562
+ # Process all event directives in the line
563
+ for event_type in event_types:
564
+ # Check for @eventType(...) or @onEventType(...) pattern with balanced parentheses
565
+ pattern = rf'@(?:on)?{event_type}\s*\('
566
+
567
+ # Loop to handle multiple occurrences of the same event type
568
+ while True:
569
+ match = re.search(pattern, result, re.IGNORECASE)
570
+ if not match:
571
+ break
572
+
573
+ # Extract content within balanced parentheses
574
+ open_paren_pos = match.end() - 1 # Position of opening parenthesis
575
+ content = self._extract_balanced_content(result, open_paren_pos)
576
+
577
+ if content is not None:
578
+ # Process directive
579
+ event_config = self.event_processor.process_event_directive(event_type, content)
580
+
581
+ # Replace @eventType(...) or @onEventType(...) with event config
582
+ directive_start = match.start()
583
+ # Calculate end position: open_paren_pos + 1 (len of '(') + len(content) + 1 (len of ')')
584
+ directive_end = open_paren_pos + 1 + len(content) + 1
585
+
586
+ result = result[:directive_start] + event_config + result[directive_end:]
587
+ changed = True
588
+ else:
589
+ # If content is None, it means unbalanced parentheses or other issue
590
+ # Break loop to avoid infinite loop
591
+ break
592
+
593
+ return result if changed else None
594
+
595
+ def _is_incomplete_event_directive(self, line):
596
+ """Check if line contains an incomplete event directive"""
597
+ event_types = [
598
+ 'click', 'change', 'submit', 'focus', 'blur', 'input', 'keydown', 'keyup', 'keypress',
599
+ 'mousedown', 'mouseup', 'mouseover', 'mouseout', 'mousemove', 'mouseenter', 'mouseleave',
600
+ 'dblclick', 'contextmenu', 'wheel', 'scroll', 'resize', 'load', 'unload', 'beforeunload',
601
+ 'error', 'abort', 'select', 'selectstart', 'selectionchange'
602
+ ]
603
+
604
+ for event_type in event_types:
605
+ # Check for both @event(...) and @onEvent(...)
606
+ pattern = rf'@(?:on)?{event_type}\s*\('
607
+ if re.search(pattern, line, re.IGNORECASE):
608
+ # Check if parentheses are balanced
609
+ paren_count = 0
610
+ in_quotes = False
611
+ quote_char = ''
612
+
613
+ for char in line:
614
+ if (char == '"' or char == "'") and not in_quotes:
615
+ in_quotes = True
616
+ quote_char = char
617
+ elif char == quote_char and in_quotes:
618
+ in_quotes = False
619
+ quote_char = ''
620
+ elif not in_quotes:
621
+ if char == '(':
622
+ paren_count += 1
623
+ elif char == ')':
624
+ paren_count -= 1
625
+
626
+ # If parentheses are not balanced, it's incomplete
627
+ if paren_count > 0:
628
+ return True
629
+
630
+ return False
631
+
632
+ def _join_multiline_event_directive(self, lines, start_index):
633
+ """Join multiline event directive into a single line"""
634
+ result = lines[start_index].strip()
635
+ lines_joined = 1 # Start with 1 (the first line)
636
+
637
+ for i in range(start_index + 1, len(lines)):
638
+ line = lines[i].strip()
639
+ result += ' ' + line
640
+ lines_joined += 1
641
+
642
+ # Check if directive is now complete
643
+ if self._is_event_directive_complete(result):
644
+ return result, lines_joined
645
+
646
+ return result, lines_joined
647
+
648
+ def _is_event_directive_complete(self, line):
649
+ """Check if event directive is complete (balanced parentheses)"""
650
+ paren_count = 0
651
+ in_quotes = False
652
+ quote_char = ''
653
+
654
+ for char in line:
655
+ if (char == '"' or char == "'") and not in_quotes:
656
+ in_quotes = True
657
+ quote_char = char
658
+ elif char == quote_char and in_quotes:
659
+ in_quotes = False
660
+ quote_char = ''
661
+ elif not in_quotes:
662
+ if char == '(':
663
+ paren_count += 1
664
+ elif char == ')':
665
+ paren_count -= 1
666
+
667
+ return paren_count == 0
668
+
669
+ def _is_incomplete_view_directive(self, line):
670
+ """Check if line contains an incomplete @view/@template directive"""
671
+ line_stripped = line.strip().lower()
672
+
673
+ # Check if line starts with @view or @template
674
+ if line_stripped.startswith('@view(') or line_stripped.startswith('@template('):
675
+ # Check if directive is complete
676
+ return not self._is_event_directive_complete(line)
677
+
678
+ return False
679
+
680
+ def _join_multiline_view_directive(self, lines, start_index):
681
+ """Join multiline @view/@template directive into a single line"""
682
+ result = lines[start_index].strip()
683
+ lines_joined = 1 # Start with 1 (the first line)
684
+
685
+ for i in range(start_index + 1, len(lines)):
686
+ line = lines[i].strip()
687
+ result += ' ' + line
688
+ lines_joined += 1
689
+
690
+ # Check if directive is now complete
691
+ if self._is_event_directive_complete(result):
692
+ return result, lines_joined
693
+
694
+ return result, lines_joined
695
+
696
+ def _extract_balanced_content(self, line, start_pos):
697
+ """Extract content within balanced parentheses"""
698
+ paren_count = 0
699
+ in_quotes = False
700
+ quote_char = ''
701
+
702
+ for i in range(start_pos, len(line)):
703
+ char = line[i]
704
+
705
+ if (char == '"' or char == "'") and not in_quotes:
706
+ in_quotes = True
707
+ quote_char = char
708
+ elif char == quote_char and in_quotes:
709
+ # Check if this is an escaped quote
710
+ if i > 0 and line[i - 1] == '\\':
711
+ continue
712
+ in_quotes = False
713
+ quote_char = ''
714
+ elif not in_quotes:
715
+ if char == '(':
716
+ paren_count += 1
717
+ elif char == ')':
718
+ paren_count -= 1
719
+ if paren_count == 0:
720
+ # Found matching closing parenthesis
721
+ return line[start_pos + 1:i]
722
+
723
+ return None
724
+
725
+ def _process_line_directives(self, line, stack, output, sections):
726
+ """Process Blade directives in a line"""
727
+
728
+ # Handle @class directive (BEFORE event directives to allow @class in same tag)
729
+ if '@class' in line:
730
+ line = self.class_binding_handler.process_class_directive(line)
731
+
732
+ # Handle event directives (@click, @change, @submit, etc.)
733
+ result = self._process_event_directives(line)
734
+ if result:
735
+ # Process {{ $var }} after event directives
736
+ result = self.template_processors.process_template_line(result)
737
+ return result
738
+
739
+ # Handle @serverside/@serverSide
740
+ result = self.template_processors.process_serverside_directive(line)
741
+ if result:
742
+ return result
743
+
744
+ # Handle @clientside
745
+ result = self.template_processors.process_clientside_directive(line)
746
+ if result:
747
+ return result
748
+
749
+ # Handle @auth/@guest
750
+ result = self.directive_processors.process_auth_directive(line)
751
+ if result:
752
+ return result
753
+
754
+ # Handle @endauth/@endguest
755
+ result = self.directive_processors.process_endauth_directive(line)
756
+ if result:
757
+ return result
758
+
759
+ # Handle @can/@cannot
760
+ result = self.directive_processors.process_can_directive(line)
761
+ if result:
762
+ return result
763
+
764
+ # Handle @endcan/@endcannot
765
+ result = self.directive_processors.process_endcan_directive(line)
766
+ if result:
767
+ return result
768
+
769
+ # Handle @csrf
770
+ result = self.directive_processors.process_csrf_directive(line)
771
+ if result:
772
+ return result
773
+
774
+ # Handle @method
775
+ result = self.directive_processors.process_method_directive(line)
776
+ if result:
777
+ return result
778
+
779
+ # Handle @error
780
+ result = self.directive_processors.process_error_directive(line)
781
+ if result:
782
+ return result
783
+
784
+ # Handle @enderror
785
+ result = self.directive_processors.process_enderror_directive(line)
786
+ if result:
787
+ return result
788
+
789
+ # Handle @hasSection
790
+ result = self.directive_processors.process_hassection_directive(line)
791
+ if result:
792
+ return result
793
+
794
+ # Handle @endhassection
795
+ result = self.directive_processors.process_endhassection_directive(line)
796
+ if result:
797
+ return result
798
+
799
+ # Handle @empty
800
+ result = self.directive_processors.process_empty_directive(line, stack, output)
801
+ if result:
802
+ return result
803
+
804
+ # Handle @isset
805
+ result = self.directive_processors.process_isset_directive(line, stack, output)
806
+ if result:
807
+ return result
808
+
809
+ # Handle @unless
810
+ result = self.directive_processors.process_unless_directive(line)
811
+ if result:
812
+ return result
813
+
814
+ # Handle @endunless
815
+ result = self.directive_processors.process_endunless_directive(line)
816
+ if result:
817
+ return result
818
+
819
+ # Handle @endempty
820
+ if line.startswith('@endempty'):
821
+ result = self.directive_processors.process_endempty_directive(stack, output)
822
+ if result:
823
+ return result
824
+
825
+ # Handle @endisset
826
+ if line.startswith('@endisset'):
827
+ result = self.directive_processors.process_endisset_directive(stack, output)
828
+ if result:
829
+ return result
830
+
831
+ # Handle @php
832
+ if line.startswith('@php'):
833
+ # Check if we're inside a loop
834
+ if stack and stack[-1][0] in ['for', 'while']:
835
+ # For @php inside loops, don't process as directive, let it be handled as content
836
+ pass
837
+ else:
838
+ result = self.directive_processors.process_php_directive(line, stack, output)
839
+ if result:
840
+ return result
841
+
842
+ # Handle @endphp
843
+ if line.startswith('@endphp'):
844
+ # Check if we're inside a loop
845
+ if stack and stack[-1][0] in ['for', 'while']:
846
+ # For @endphp inside loops, don't process as directive, let it be handled as content
847
+ pass
848
+ else:
849
+ result = self.directive_processors.process_endphp_directive(stack, output)
850
+ if result:
851
+ return result
852
+
853
+ # Handle @json
854
+ result = self.directive_processors.process_json_directive(line)
855
+ if result:
856
+ return result
857
+
858
+ # Handle @lang
859
+ result = self.directive_processors.process_lang_directive(line)
860
+ if result:
861
+ return result
862
+
863
+
864
+ # Handle @choice
865
+ result = self.directive_processors.process_choice_directive(line)
866
+ if result:
867
+ return result
868
+
869
+ # Handle @exec
870
+ result = self.directive_processors.process_exec_directive(line)
871
+ if result:
872
+ return result
873
+
874
+ # Handle @out
875
+ result = self.directive_processors.process_out_directive(line)
876
+ if result:
877
+ return result
878
+
879
+ # Handle @section
880
+ if line.startswith('@section'):
881
+ return self.section_handlers.process_section_directive(line, stack, output, sections)
882
+
883
+ if line.startswith('@endsection'):
884
+ return self.section_handlers.process_endsection_directive(stack, output, sections)
885
+
886
+ # Handle @block
887
+ if line.startswith('@block'):
888
+ return self.section_handlers.process_block_directive(line, stack, output, sections)
889
+
890
+ if line.startswith('@endblock') or line.startswith('@endBlock'):
891
+ return self.section_handlers.process_endblock_directive(stack, output, sections)
892
+
893
+ # Handle @if/@endif
894
+ if line.startswith('@if'):
895
+ is_attr = self._is_attribute_directive(line)
896
+ return self.conditional_handlers.process_if_directive(line, stack, output, is_attr)
897
+
898
+ if line.startswith('@elseif'):
899
+ return self.conditional_handlers.process_elseif_directive(line, stack, output)
900
+
901
+ if line.startswith('@else'):
902
+ return self.conditional_handlers.process_else_directive(line, stack, output)
903
+
904
+ if line.startswith('@endif'):
905
+ return self.conditional_handlers.process_endif_directive(stack, output)
906
+
907
+ # Handle @foreach
908
+ if line.startswith('@foreach'):
909
+ is_attr = self._is_attribute_directive(line)
910
+ return self.loop_handlers.process_foreach_directive(line, stack, output, is_attr)
911
+
912
+ if line.startswith('@endforeach'):
913
+ return self.loop_handlers.process_endforeach_directive(stack, output)
914
+
915
+ # Handle @for
916
+ if line.startswith('@for'):
917
+ is_attr = self._is_attribute_directive(line)
918
+ return self.loop_handlers.process_for_directive(line, stack, output, is_attr)
919
+
920
+ if line.startswith('@endfor'):
921
+ return self.loop_handlers.process_endfor_directive(stack, output)
922
+
923
+ # Handle @while
924
+ if line.startswith('@while'):
925
+ is_attr = self._is_attribute_directive(line)
926
+ return self.loop_handlers.process_while_directive(line, stack, output, is_attr)
927
+
928
+ if line.startswith('@endwhile'):
929
+ return self.loop_handlers.process_endwhile_directive(stack, output)
930
+
931
+ # Handle @switch
932
+ if line.startswith('@switch'):
933
+ is_attr = self._is_attribute_directive(line)
934
+ return self.conditional_handlers.process_switch_directive(line, stack, output, is_attr)
935
+
936
+ if line.startswith('@case'):
937
+ return self.conditional_handlers.process_case_directive(line, stack, output)
938
+
939
+ if line.startswith('@default'):
940
+ return self.conditional_handlers.process_default_directive(line, stack, output)
941
+
942
+ if line.startswith('@break'):
943
+ return self.conditional_handlers.process_break_directive(line, stack, output)
944
+
945
+ if line.startswith('@endswitch'):
946
+ return self.conditional_handlers.process_endswitch_directive(stack, output)
947
+
948
+ # Handle @register
949
+ if line.startswith('@register'):
950
+ result = self.directive_processors.process_register_directive(line, stack, output)
951
+ if result:
952
+ return result
953
+
954
+ # Handle @endregister
955
+ if line.startswith('@endregister'):
956
+ result = self.directive_processors.process_endregister_directive(stack, output)
957
+ if result:
958
+ return result
959
+
960
+ # Handle @setup (alias của @register)
961
+ if line.startswith('@setup'):
962
+ result = self.directive_processors.process_register_directive(line, stack, output)
963
+ if result:
964
+ return result
965
+
966
+ # Handle @endsetup (alias của @endregister)
967
+ if line.startswith('@endsetup'):
968
+ result = self.directive_processors.process_endregister_directive(stack, output)
969
+ if result:
970
+ return result
971
+
972
+ # Handle @script (xử lý như @register)
973
+ if line.startswith('@script'):
974
+ result = self.directive_processors.process_register_directive(line, stack, output)
975
+ if result:
976
+ return result
977
+
978
+ # Handle @endscript (xử lý như @endregister)
979
+ if line.startswith('@endscript'):
980
+ result = self.directive_processors.process_endregister_directive(stack, output)
981
+ if result:
982
+ return result
983
+
984
+ # Handle @wrapper and @wrap (NOT @view - that's handled as @template style)
985
+ line_lower = line.lower()
986
+ if line_lower.startswith('@wrapper') or line_lower.startswith('@wrap'):
987
+ result = self.directive_processors.process_wrapper_directive(line, stack, output)
988
+ if result:
989
+ return result
990
+
991
+ # Handle @endwrapper and @endwrap (NOT @endview - that's handled with @template style)
992
+ if line_lower.startswith('@endwrapper') or line_lower.startswith('@endwrap'):
993
+ result = self.directive_processors.process_endwrapper_directive(stack, output)
994
+ if result:
995
+ return result
996
+
997
+ return False
998
+
999
+ def _process_multiline_include_directives(self, blade_code):
1000
+ """Process multiline @include directives before line-by-line processing"""
1001
+ from config import APP_VIEW_NAMESPACE
1002
+
1003
+ # Handle @include directive with string literals and variables (multiline arrays/objects)
1004
+ def replace_include_directive(match):
1005
+ view_name = match.group(1).strip()
1006
+ variables = match.group(2).strip() if match.group(2) else '{}'
1007
+
1008
+ # Don't use convert_php_array_to_json as it evals PHP which fails with undefined vars
1009
+ # Instead, just convert PHP array syntax to JS object syntax
1010
+ variables_js = variables
1011
+
1012
+ # Extract state variables used in the include data
1013
+ state_vars_used = set()
1014
+ # Find all $variable references in the variables data
1015
+ var_matches = re.findall(r'\$(\w+)', variables)
1016
+ for var_name in var_matches:
1017
+ if var_name in self.state_variables:
1018
+ state_vars_used.add(var_name)
1019
+
1020
+ # Convert PHP array ['key' => $value] to JS {key: value}
1021
+ # Handle array syntax
1022
+ if variables_js.startswith('[') and variables_js.endswith(']'):
1023
+ variables_js = variables_js[1:-1].strip() # Remove [ ]
1024
+
1025
+ # Convert 'key' => $value patterns to "key": value
1026
+ variables_js = re.sub(r"'([^']+)'\s*=>\s*\$(\w+)", r'"\1": \2', variables_js)
1027
+ variables_js = re.sub(r'"([^"]+)"\s*=>\s*\$(\w+)', r'"\1": \2', variables_js)
1028
+
1029
+ # Wrap in { }
1030
+ variables_js = '{' + variables_js + '}'
1031
+
1032
+ include_call = APP_VIEW_NAMESPACE + ".renderView(this.__include('" + view_name + "', " + variables_js + "))"
1033
+
1034
+ # If state variables are used, wrap with __watch
1035
+ # Use placeholder for watch ID that will be replaced later with actual counter
1036
+ if state_vars_used:
1037
+ watch_keys = list(state_vars_used)
1038
+ return "${this.__watch('__INCLUDE_WATCH_PLACEHOLDER__', " + str(watch_keys) + ", () => " + include_call + ")}"
1039
+
1040
+ return "${" + include_call + "}"
1041
+
1042
+ # Handle @include directive with PHP expressions and variables (multiline)
1043
+ def replace_include_php_directive(match):
1044
+ view_expr = match.group(1).strip()
1045
+ variables = match.group(2).strip() if match.group(2) else '{}'
1046
+
1047
+ # Extract state variables used in the include data
1048
+ state_vars_used = set()
1049
+ # Find all $variable references in the variables data
1050
+ var_matches = re.findall(r'\$(\w+)', variables)
1051
+ for var_name in var_matches:
1052
+ if var_name in self.state_variables:
1053
+ state_vars_used.add(var_name)
1054
+
1055
+ # Don't eval PHP - just parse syntax
1056
+ variables_js = variables
1057
+
1058
+ # Convert PHP array to JS object
1059
+ if variables_js.startswith('[') and variables_js.endswith(']'):
1060
+ variables_js = variables_js[1:-1].strip()
1061
+ # Convert 'key' => $value to "key": value
1062
+ variables_js = re.sub(r"'([^']+)'\s*=>\s*\$(\w+)", r'"\1": \2', variables_js)
1063
+ variables_js = re.sub(r'"([^"]+)"\s*=>\s*\$(\w+)', r'"\1": \2', variables_js)
1064
+ variables_js = '{' + variables_js + '}'
1065
+ else:
1066
+ # Remove $ from simple variables
1067
+ variables_js = re.sub(r'\$(\w+)', r'\1', variables_js)
1068
+
1069
+ # Convert PHP expression to JavaScript
1070
+ from php_converter import php_to_js
1071
+ view_expr_js = php_to_js(view_expr)
1072
+
1073
+ include_call = APP_VIEW_NAMESPACE + ".renderView(this.__include(" + view_expr_js + ", " + variables_js + "))"
1074
+
1075
+ # If state variables are used, wrap with __watch
1076
+ # Use placeholder for watch ID that will be replaced later with actual counter
1077
+ if state_vars_used:
1078
+ watch_keys = list(state_vars_used)
1079
+ return "${this.__watch('__INCLUDE_WATCH_PLACEHOLDER__', " + str(watch_keys) + ", () => " + include_call + ")}"
1080
+
1081
+ return "${" + include_call + "}"
1082
+
1083
+ # Process multiline @include directives with proper patterns
1084
+ # Handle @include with PHP expressions (must be before string literal patterns)
1085
+ blade_code = re.sub(r'@include\s*\(\s*([^,\'"][^)]*?)\s*,\s*(\[[^\]]*\]|\{[^\}]*\}|[^)]*)\s*\)', replace_include_php_directive, blade_code, flags=re.MULTILINE | re.DOTALL)
1086
+
1087
+ # Handle @include with string literals (multiline arrays/objects)
1088
+ blade_code = re.sub(r'@include\s*\(\s*[\'"]([^\'"]*)[\'"]\s*,\s*(\[[^\]]*\]|\{[^\}]*\}|[^)]*)\s*\)', replace_include_directive, blade_code, flags=re.MULTILINE | re.DOTALL)
1089
+
1090
+ # Handle @include without variables
1091
+ def replace_include_no_vars_directive(match):
1092
+ view_expr = match.group(1).strip()
1093
+ from php_converter import php_to_js
1094
+ view_expr_js = php_to_js(view_expr)
1095
+ return "${" + APP_VIEW_NAMESPACE + ".renderView(this.__include(" + view_expr_js + "))}"
1096
+
1097
+ blade_code = re.sub(r'@include\s*\(\s*([^,\'"][^)]*?)\s*\)', replace_include_no_vars_directive, blade_code)
1098
+ blade_code = re.sub(r'@include\s*\(\s*[\'"]([^\'"]*)[\'"]\s*\)', r'${' + APP_VIEW_NAMESPACE + r'.renderView(this.__include("\1", {}))}', blade_code)
1099
+
1100
+ return blade_code
1101
+
1102
+ def _process_verbatim_blocks(self, blade_code):
1103
+ """Process @verbatim...@endverbatim blocks to preserve their content"""
1104
+ # Store verbatim blocks and replace them with placeholders
1105
+ self.verbatim_blocks = {}
1106
+ verbatim_counter = 0
1107
+
1108
+ # Find all @verbatim...@endverbatim blocks
1109
+ verbatim_pattern = r'@verbatim\s*(.*?)\s*@endverbatim'
1110
+
1111
+ def replace_verbatim_block(match):
1112
+ nonlocal verbatim_counter
1113
+ # Extract the content between @verbatim and @endverbatim
1114
+ content = match.group(1)
1115
+ # Store content with a unique placeholder
1116
+ placeholder = f"__VERBATIM_BLOCK_{verbatim_counter}__"
1117
+ self.verbatim_blocks[placeholder] = content
1118
+ verbatim_counter += 1
1119
+ return placeholder
1120
+
1121
+ # Replace all @verbatim blocks with placeholders
1122
+ blade_code = re.sub(verbatim_pattern, replace_verbatim_block, blade_code, flags=re.DOTALL)
1123
+
1124
+ return blade_code
1125
+
1126
+ def _restore_verbatim_blocks(self, processed_content):
1127
+ """Restore verbatim blocks from placeholders"""
1128
+ if hasattr(self, 'verbatim_blocks'):
1129
+ for placeholder, content in self.verbatim_blocks.items():
1130
+ processed_content = processed_content.replace(placeholder, content)
1131
+ return processed_content
1132
+
1133
+ def _remove_page_directives(self, blade_code):
1134
+ """
1135
+ Remove page/document directives from Blade code.
1136
+ These directives are only for PHP compilation (server-side),
1137
+ and should be completely removed when compiling to JavaScript (client-side).
1138
+
1139
+ Directives to remove:
1140
+ - @pageStart, @pageOpen, @docStart
1141
+ - @pageEnd, @pageClose, @docEnd
1142
+ """
1143
+ # Remove all page/document directives (case-insensitive)
1144
+ # These directives don't take parameters, so we just match the directive name
1145
+ # Pattern matches: @directiveName followed by word boundary, optional whitespace, and optional newline
1146
+ directive_patterns = [
1147
+ r'@pageStart\b',
1148
+ r'@pageOpen\b',
1149
+ r'@pageEnd\b',
1150
+ r'@pageClose\b',
1151
+ r'@docStart\b',
1152
+ r'@docEnd\b',
1153
+ ]
1154
+
1155
+ for pattern in directive_patterns:
1156
+ # Remove directive with optional whitespace and newline after it
1157
+ # This handles cases like:
1158
+ # @pageStart
1159
+ # @pageStart\n
1160
+ # @pageStart \n
1161
+ blade_code = re.sub(pattern + r'\s*\n?', '', blade_code, flags=re.IGNORECASE | re.MULTILINE)
1162
+ # Also handle directive at end of line (standalone on a line)
1163
+ blade_code = re.sub(r'^\s*' + pattern + r'\s*$', '', blade_code, flags=re.IGNORECASE | re.MULTILINE)
1164
+
1165
+ return blade_code
1166
+
1167
+