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,1763 @@
1
+ """
2
+ Main compiler class tổng hợp tất cả các module
3
+ """
4
+
5
+ import re
6
+ import json
7
+ from config import JS_FUNCTION_PREFIX, HTML_ATTR_PREFIX
8
+ from parsers import DirectiveParsers
9
+ from template_processor import TemplateProcessor
10
+ from template_analyzer import TemplateAnalyzer
11
+ from function_generators import FunctionGenerators
12
+ from compiler_utils import CompilerUtils
13
+ from wrapper_parser import WrapperParser
14
+ from register_parser import RegisterParser
15
+ from config import ViewConfig
16
+ from declaration_tracker import DeclarationTracker
17
+ from binding_directive_service import BindingDirectiveService
18
+ from style_directive_handler import StyleDirectiveHandler
19
+ from show_directive_handler import ShowDirectiveHandler
20
+
21
+ class BladeCompiler:
22
+ def __init__(self):
23
+ self.parsers = DirectiveParsers()
24
+ self.template_processor = TemplateProcessor()
25
+ self.template_analyzer = TemplateAnalyzer()
26
+ self.function_generators = FunctionGenerators()
27
+ self.compiler_utils = CompilerUtils()
28
+ self.wrapper_parser = WrapperParser()
29
+ self.register_parser = RegisterParser()
30
+ self.declaration_tracker = DeclarationTracker()
31
+ self.binding_directive_service = BindingDirectiveService()
32
+ self.style_directive_handler = StyleDirectiveHandler()
33
+ self.show_directive_handler = ShowDirectiveHandler()
34
+
35
+ def convert_view_path_to_function_name(self, view_path):
36
+ """Convert view path to function name (e.g., web.demo-if -> WebDemoIf)"""
37
+ # Split by dots and hyphens
38
+ parts = re.split(r'[.-]', view_path)
39
+ # Capitalize each part and join
40
+ function_name = ''.join(part.capitalize() for part in parts)
41
+ return function_name
42
+
43
+ def compile_blade_to_js(self, blade_code, view_name):
44
+ """Main compiler function"""
45
+ blade_code = blade_code.strip()
46
+
47
+ # Reset watch counter for this view (each view starts from watch-1)
48
+ self.template_processor.watch_counter = 0
49
+
50
+ # ========================================================================
51
+ # PRIORITY 1 (HIGHEST): Process @verbatim...@endverbatim blocks FIRST
52
+ # ========================================================================
53
+ # @verbatim blocks have ABSOLUTE PRIORITY - all content inside is preserved
54
+ # as-is, NO processing, NO compilation, NO escaping, NO directive parsing
55
+ # This must be done before ANY other processing to ensure complete protection
56
+ verbatim_blocks = {}
57
+ verbatim_counter = 0
58
+
59
+ def protect_verbatim_block(match):
60
+ nonlocal verbatim_counter
61
+ # Store the ENTIRE content between @verbatim and @endverbatim
62
+ # This content will be restored EXACTLY as-is at the end
63
+ content = match.group(1)
64
+ placeholder = f"__VERBATIM_BLOCK_{verbatim_counter}__"
65
+ verbatim_blocks[placeholder] = content
66
+ verbatim_counter += 1
67
+ return placeholder
68
+
69
+ # Match @verbatim...@endverbatim (case insensitive, multiline)
70
+ verbatim_pattern = r'@verbatim\s*(.*?)\s*@endverbatim'
71
+ blade_code = re.sub(verbatim_pattern, protect_verbatim_block, blade_code, flags=re.DOTALL | re.IGNORECASE)
72
+
73
+ # ========================================================================
74
+ # PRIORITY 2: Process @register/@endregister blocks BEFORE escaping backticks
75
+ # ========================================================================
76
+ # @register blocks are processed AFTER @verbatim to ensure @register inside
77
+ # @verbatim is NOT processed. This protects content inside @register blocks
78
+ # from being escaped (they contain raw JavaScript code)
79
+ register_blocks = {}
80
+ register_counter = 0
81
+
82
+ def protect_register_block(match):
83
+ nonlocal register_counter
84
+ # Get full match including @register and @endregister
85
+ # Note: This will NOT match @register inside @verbatim (already replaced)
86
+ full_content = match.group(0)
87
+ placeholder = f"__REGISTER_BLOCK_{register_counter}__"
88
+ register_blocks[placeholder] = full_content
89
+ register_counter += 1
90
+ return placeholder
91
+
92
+ # Match @register with optional parameters and @endregister (case insensitive)
93
+ # Also match aliases: @setup/@endsetup, @script/@endscript
94
+ # IMPORTANT: This will NOT match inside @verbatim blocks (already protected)
95
+ register_pattern = r'@(?:register|setup|script)\s*(?:\([^)]*\))?\s*(.*?)\s*@end(?:register|setup|script)'
96
+ blade_code = re.sub(register_pattern, protect_register_block, blade_code, flags=re.DOTALL | re.IGNORECASE)
97
+
98
+ # Escape backticks in blade code to prevent JavaScript syntax errors
99
+ # This needs to be done AFTER protecting @register blocks to avoid escaping backticks
100
+ # that are part of JavaScript template strings in @register blocks
101
+ escape_str = '@@@@@@@@@@@@@@@@@@@@@@@--------------------------------$$$$$$$$$$$$$$$$$$$$$$$$$$'
102
+ blade_code = blade_code.replace('\\`', escape_str) # Protect already escaped backticks
103
+ blade_code = blade_code.replace('`', '\\`') # Escape all backticks
104
+ blade_code = blade_code.replace(escape_str, '\\`') # Restore protected backticks
105
+
106
+ # Reset parser states to avoid data leakage between views
107
+ if hasattr(self.register_parser, 'reset'):
108
+ self.register_parser.reset()
109
+ # Note: wrapper_parser không reset vì dữ liệu từ wraper.js không thay đổi
110
+
111
+ # Parse wrapper content
112
+ wrapper_function_content, wrapper_config_content = self.wrapper_parser.parse_wrapper_file()
113
+
114
+ # Convert view path to function name
115
+ function_name = self.convert_view_path_to_function_name(view_name)
116
+
117
+ # Initialize update_functions list for storing update$stateKey functions
118
+ self.update_functions = []
119
+
120
+ # Remove Blade comments
121
+ blade_code = re.sub(r'{{--.*?--}}', '', blade_code, flags=re.DOTALL)
122
+
123
+ # Check for directives
124
+ has_await = '@await(' in blade_code
125
+ has_fetch = '@fetch(' in blade_code
126
+ has_subscribe = ('@subscribe(' in blade_code) or re.search(r'@dontsubscribe\b', blade_code, flags=re.IGNORECASE)
127
+
128
+ # NEW: Use DeclarationTracker to parse all declarations in order
129
+ all_declarations = self.declaration_tracker.parse_all_declarations(blade_code)
130
+
131
+ # Generate wrapper declarations from tracked declarations
132
+ wrapper_declarations_code, variable_list, state_declarations = self._generate_wrapper_declarations(all_declarations)
133
+
134
+ # Parse main components (keep for compatibility, but we'll use DeclarationTracker results)
135
+ extended_view, extends_expression, extends_data = self.parsers.parse_extends(blade_code)
136
+ vars_declaration = self.parsers.parse_vars(blade_code)
137
+ let_declarations = self.parsers.parse_let_directives(blade_code)
138
+ const_declarations = self.parsers.parse_const_directives(blade_code)
139
+ usestate_declarations = self.parsers.parse_usestate_directives(blade_code)
140
+
141
+ # Extract usestate_variables for event processor
142
+ usestate_variables = self._extract_usestate_variables(usestate_declarations, all_declarations)
143
+
144
+ # Update template_processor with usestate_variables (don't reinitialize to preserve watch_counter)
145
+ self.template_processor.state_variables = usestate_variables
146
+ self.template_processor.conditional_handlers.state_variables = usestate_variables
147
+ self.template_processor.loop_handlers.state_variables = usestate_variables
148
+ self.template_processor.event_processor.state_variables = usestate_variables
149
+ self.template_processor.echo_processor.state_variables = usestate_variables
150
+ self.template_processor.class_binding_handler.state_variables = usestate_variables
151
+
152
+ # Parse block directives
153
+ blade_code = self.parsers.parse_block_directives(blade_code)
154
+ blade_code = self.parsers.parse_endblock_directives(blade_code)
155
+ blade_code = self.parsers.parse_useblock_directives(blade_code)
156
+ blade_code = self.parsers.parse_onblock_directives(blade_code)
157
+
158
+ fetch_config = self.parsers.parse_fetch(blade_code) if has_fetch else None
159
+ subscribe_config = None # Deprecated: @subscribe directive removed, use @view/:subscribe instead
160
+ init_functions, css_content = self.parsers.parse_init(blade_code)
161
+ view_type_data = self.parsers.parse_view_type(blade_code)
162
+
163
+ # Extract unescaped content from @register blocks BEFORE restoring to blade_code
164
+ # This ensures register_parser gets original content without escaped backticks
165
+ register_content_unescaped = None
166
+ for placeholder, original_content in register_blocks.items():
167
+ # Extract just the content inside @register...@endregister (without the directives)
168
+ content_match = re.search(r'@(?:register|setup|script)\s*(?:\([^)]*\))?\s*(.*?)\s*@end(?:register|setup|script)', original_content, re.DOTALL | re.IGNORECASE)
169
+ if content_match:
170
+ if register_content_unescaped is None:
171
+ register_content_unescaped = content_match.group(1)
172
+ else:
173
+ # If multiple @register blocks, concatenate them
174
+ register_content_unescaped += "\n" + content_match.group(1)
175
+
176
+ # Restore @register blocks to blade_code for removal from template
177
+ # (This is needed so parse_register can find and remove them)
178
+ for placeholder, original_content in register_blocks.items():
179
+ blade_code = blade_code.replace(placeholder, original_content)
180
+
181
+ # Parse @register directive from blade_code (for removal from template)
182
+ register_content = self.parsers.parse_register(blade_code)
183
+
184
+ # Use unescaped content for register_parser to avoid escaped backticks
185
+ if register_content_unescaped:
186
+ register_data = self.register_parser.parse_register_content(register_content_unescaped, view_name)
187
+ elif register_content:
188
+ # Fallback: use escaped content if extraction failed
189
+ register_data = self.register_parser.parse_register_content(register_content, view_name)
190
+ else:
191
+ register_data = None
192
+
193
+ # Remove script setup/import/imports/scope content from blade_code before processing
194
+ # These should only be used for import statements, not for render function content
195
+ script_types = ['setup', 'import', 'imports', 'scope', 'scoped']
196
+ for script_type in script_types:
197
+ pattern = rf'<script\s+{script_type}[^>]*>.*?</script>'
198
+ blade_code = re.sub(pattern, '', blade_code, flags=re.DOTALL | re.IGNORECASE)
199
+
200
+ # Remove @viewType directive after parsing
201
+ blade_code = re.sub(r'@viewtype\s*\([^)]*\)', '', blade_code, flags=re.IGNORECASE)
202
+
203
+ # Then remove script setup/import/imports/scope content from register_content for template processing
204
+ if register_content:
205
+ for script_type in script_types:
206
+ pattern = rf'<script\s+{script_type}[^>]*>.*?</script>'
207
+ register_content = re.sub(pattern, '', register_content, flags=re.DOTALL | re.IGNORECASE)
208
+
209
+ # Process template content
210
+ # NOTE: verbatim blocks are already protected as placeholders, so they won't be processed
211
+ template_content, sections = self.template_processor.process_template(blade_code)
212
+
213
+ # ========================================================================
214
+ # Restore @verbatim blocks - escape backticks for template string safety
215
+ # ========================================================================
216
+ # Verbatim content is restored but backticks must be escaped because
217
+ # template_content will be inserted into a JavaScript template string (backticks)
218
+ # Example: __outputRenderedContent__ = `{template_content}`
219
+ # If verbatim content has backticks, they will break the template string syntax
220
+ # So we escape: ` → \` and \` → \\`
221
+ for placeholder, content in verbatim_blocks.items():
222
+ # Escape backticks and ${} in verbatim content for safe insertion into template string
223
+ # Verbatim content will be inserted into: __outputRenderedContent__ = `{content}`
224
+ # So we need to escape:
225
+ # 1. Backticks: ` → \` (to prevent breaking template string)
226
+ # 2. ${...}: ${ → \${ (to prevent JavaScript interpolation)
227
+ # 3. Already escaped: \` → \\` and \${ → \\${
228
+
229
+ # Step 1: Protect already escaped sequences
230
+ protected_content = content.replace('\\`', '__ESCAPED_BACKTICK__')
231
+ protected_content = protected_content.replace('\\${', '__ESCAPED_DOLLAR_BRACE__')
232
+
233
+ # Step 2: Escape unescaped backticks and ${ sequences
234
+ protected_content = protected_content.replace('`', '\\`')
235
+ protected_content = protected_content.replace('${', '\\${')
236
+
237
+ # Step 3: Restore protected sequences (now double-escaped)
238
+ protected_content = protected_content.replace('__ESCAPED_BACKTICK__', '\\\\`')
239
+ protected_content = protected_content.replace('__ESCAPED_DOLLAR_BRACE__', '\\\\${')
240
+
241
+ # Replace placeholder with escaped content
242
+ template_content = template_content.replace(placeholder, protected_content)
243
+
244
+ # Extract wrapper config from template content
245
+ wrapper_config = self._extract_wrapper_config(template_content)
246
+
247
+ # If wrapper exists, extract inner and outer content (separate before/after)
248
+ outer_before = ''
249
+ outer_after = ''
250
+ if wrapper_config:
251
+ inner_content, outer_before_raw, outer_after_raw = self._extract_wrapper_inner_content(template_content)
252
+ template_content = inner_content
253
+
254
+ # Filter outer content to only include directives (remove pure HTML)
255
+ outer_before = self._filter_directives_only(outer_before_raw) if outer_before_raw else ''
256
+ outer_after = self._filter_directives_only(outer_after_raw) if outer_after_raw else ''
257
+
258
+ # Remove __WRAPPER_CONFIG__ and __WRAPPER_END__ from template content
259
+ if wrapper_config:
260
+ # Remove __WRAPPER_END__ marker first
261
+ template_content = re.sub(r'__WRAPPER_END__\s*', '', template_content)
262
+
263
+ # Use same logic as extraction to handle nested braces for config
264
+ match = re.search(r'__WRAPPER_CONFIG__\s*=\s*', template_content)
265
+ if match:
266
+ start_pos = match.start()
267
+ end_pos = match.end()
268
+
269
+ # Find the end of the config object
270
+ if end_pos < len(template_content) and template_content[end_pos] == '{':
271
+ brace_count = 0
272
+ in_string = False
273
+ string_char = None
274
+
275
+ for i in range(end_pos, len(template_content)):
276
+ char = template_content[i]
277
+
278
+ if char in ['"', "'"] and (i == 0 or template_content[i-1] != '\\'):
279
+ if not in_string:
280
+ in_string = True
281
+ string_char = char
282
+ elif char == string_char:
283
+ in_string = False
284
+ string_char = None
285
+
286
+ if not in_string:
287
+ if char == '{':
288
+ brace_count += 1
289
+ elif char == '}':
290
+ brace_count -= 1
291
+ if brace_count == 0:
292
+ config_end = i + 1
293
+ # Include semicolon if present
294
+ if config_end < len(template_content) and template_content[config_end] == ';':
295
+ config_end += 1
296
+ # Remove including trailing whitespace/newline
297
+ while config_end < len(template_content) and template_content[config_end] in [' ', '\n', '\r', '\t']:
298
+ config_end += 1
299
+ template_content = template_content[:start_pos] + template_content[config_end:]
300
+ break
301
+
302
+ # Generate sections info
303
+ sections_info = self.template_analyzer.analyze_sections_info(sections, vars_declaration, has_await, has_fetch)
304
+
305
+ # Integrate register_data vào sections_info
306
+ if register_data and register_data.get('sections'):
307
+ for section_name, script_obj in register_data['sections'].items():
308
+ # Tìm section trong sections_info list
309
+ section_found = False
310
+ for section_info in sections_info:
311
+ if section_info.get('name') == section_name:
312
+ section_info['script'] = script_obj
313
+ section_found = True
314
+ break
315
+
316
+ # Nếu không tìm thấy, thêm section mới
317
+ if not section_found:
318
+ sections_info.append({
319
+ 'name': section_name,
320
+ 'type': 'short',
321
+ 'preloader': False,
322
+ 'useVars': False,
323
+ 'script': script_obj
324
+ })
325
+
326
+ # Analyze conditional structures outside sections
327
+ conditional_content = self.template_analyzer.analyze_conditional_structures(template_content, vars_declaration, has_await, has_fetch)
328
+
329
+ # Generate components
330
+ vars_line = " " + vars_declaration + "\n" if vars_declaration else ""
331
+
332
+ # Combine all directive declarations
333
+ # Collect declarations for render function - chỉ @let và @const, không bao gồm @useState
334
+ all_declarations = []
335
+ if let_declarations:
336
+ all_declarations.append(let_declarations)
337
+ if const_declarations:
338
+ all_declarations.append(const_declarations)
339
+ # Thêm usestate_declarations vào render function để xử lý updateStateByKey
340
+ if usestate_declarations:
341
+ all_declarations.append(usestate_declarations)
342
+
343
+ directives_line = ""
344
+ if all_declarations:
345
+ # Replace useState declarations with updateRealState calls
346
+ processed_declarations = []
347
+ has_usestate_declarations = False
348
+ for declaration in all_declarations:
349
+ if declaration.strip():
350
+ # Process each line separately to handle multi-line declarations
351
+ for line in declaration.split('\n'):
352
+ if line.strip():
353
+ # Check if this line contains @useState directive
354
+ if line.strip().startswith('@useState'):
355
+ # Handle @useState($value, $stateKey, $setStateKey) format
356
+ match = re.search(r'@useState\s*\(\s*\$?(\w+)\s*,\s*\$?(\w+)\s*,\s*\$?(\w+)\s*\)', line)
357
+ if match:
358
+ value = match.group(1).strip()
359
+ state_key = match.group(2).strip()
360
+ set_state_key = match.group(3).strip()
361
+ # Remove $ prefix if present
362
+ if state_key.startswith('$'):
363
+ state_key = state_key[1:]
364
+ if set_state_key.startswith('$'):
365
+ set_state_key = set_state_key[1:]
366
+ if value.startswith('$'):
367
+ value = value[1:]
368
+
369
+ # Only process valid state keys
370
+ if state_key and state_key.isalnum():
371
+ # Store update function for later (outside render)
372
+ self.update_functions.append(f" const update${state_key} = (value) => {{")
373
+ self.update_functions.append(f" if(__STATE__.__.canUpdateStateByKey){{")
374
+ self.update_functions.append(f" updateStateByKey('{state_key}', value);")
375
+ self.update_functions.append(f" {state_key} = value;")
376
+ self.update_functions.append(f" }}")
377
+ self.update_functions.append(f" }};")
378
+ # Add state initialization (inside render)
379
+ processed_declarations.append(f" update${state_key}({value});")
380
+ has_usestate_declarations = True
381
+ # Don't add the original line since we already have updateStateByKey
382
+ else:
383
+ processed_declarations.append(line)
384
+ else:
385
+ # Handle @useState($value) format (simple)
386
+ match_simple = re.search(r'@useState\s*\(\s*\$?(\w+)\s*\)', line)
387
+ if match_simple:
388
+ value = match_simple.group(1).strip()
389
+ # Remove $ prefix if present
390
+ if value.startswith('$'):
391
+ value = value[1:]
392
+ # For simple format, we'll skip it as it's just a value without state key
393
+ # Don't add the original line since we don't process it
394
+ else:
395
+ processed_declarations.append(line)
396
+ else:
397
+ # Check if this line contains useState destructuring
398
+ match = re.search(r'\[([^,]+),\s*[^]]+\]\s*=\s*useState\([^)]+\)', line)
399
+ if match:
400
+ state_key = match.group(1).strip()
401
+ # Remove $ prefix if present
402
+ if state_key.startswith('$'):
403
+ state_key = state_key[1:]
404
+ # Only process valid state keys
405
+ if state_key and state_key.isalnum():
406
+ # Extract value from useState(value)
407
+ value_match = re.search(r'useState\(([^)]+)\)', line)
408
+ value = value_match.group(1).strip() if value_match else 'null'
409
+ # Store update function for later (outside render)
410
+ self.update_functions.append(f" const update${state_key} = (value) => {{")
411
+ self.update_functions.append(f" if(__STATE__.__.canUpdateStateByKey){{")
412
+ self.update_functions.append(f" updateStateByKey('{state_key}', value);")
413
+ self.update_functions.append(f" {state_key} = value;")
414
+ self.update_functions.append(f" }}")
415
+ self.update_functions.append(f" }};")
416
+ # Add state initialization (inside render)
417
+ processed_declarations.append(f" update${state_key}({value});")
418
+ has_usestate_declarations = True
419
+ # Don't add the original line since we already have updateStateByKey
420
+ else:
421
+ processed_declarations.append(line)
422
+
423
+ directives_line = " " + "\n ".join(processed_declarations) + "\n"
424
+
425
+ # Add lockUpdateRealState if there are useState declarations
426
+ if has_usestate_declarations:
427
+ directives_line += " lockUpdateRealState();\n"
428
+
429
+ # Process init_functions để tạo updateStateByKey calls từ @onInit
430
+ init_update_functions = []
431
+ init_state_initializations = []
432
+
433
+ if init_functions:
434
+ for init_code in init_functions:
435
+ for line in init_code.split('\n'):
436
+ if line.strip():
437
+ # Check if this line contains @useState directive
438
+ if line.strip().startswith('@useState'):
439
+ # Handle @useState($value, $stateKey, $setStateKey) format
440
+ match = re.search(r'@useState\s*\(\s*\$?(\w+)\s*,\s*\$?(\w+)\s*,\s*\$?(\w+)\s*\)', line)
441
+ if match:
442
+ value = match.group(1).strip()
443
+ state_key = match.group(2).strip()
444
+ set_state_key = match.group(3).strip()
445
+ # Remove $ prefix if present
446
+ if state_key.startswith('$'):
447
+ state_key = state_key[1:]
448
+ if set_state_key.startswith('$'):
449
+ set_state_key = set_state_key[1:]
450
+ if value.startswith('$'):
451
+ value = value[1:]
452
+
453
+ # Only process valid state keys
454
+ if state_key and state_key.isalnum():
455
+ # Store update function for later (outside render)
456
+ self.update_functions.append(f" const update${state_key} = (value) => {{")
457
+ self.update_functions.append(f" if(__STATE__.__.canUpdateStateByKey){{")
458
+ self.update_functions.append(f" updateStateByKey('{state_key}', value);")
459
+ self.update_functions.append(f" {state_key} = value;")
460
+ self.update_functions.append(f" }}")
461
+ self.update_functions.append(f" }};")
462
+ # Add state initialization (inside render)
463
+ init_state_initializations.append(f" update${state_key}({value});")
464
+ else:
465
+ # Handle @useState($value) format (simple)
466
+ match_simple = re.search(r'@useState\s*\(\s*\$?(\w+)\s*\)', line)
467
+ if match_simple:
468
+ value = match_simple.group(1).strip()
469
+ # Remove $ prefix if present
470
+ if value.startswith('$'):
471
+ value = value[1:]
472
+ # For simple format, we'll skip it as it's just a value without state key
473
+ # Don't add the original line since we don't process it
474
+ else:
475
+ # Pattern 1: [stateKey, setState] = useState(value) - destructuring
476
+ match = re.search(r'\[([^,]+),\s*[^]]+\]\s*=\s*useState\([^)]+\)', line)
477
+ if match:
478
+ state_key = match.group(1).strip()
479
+ # Remove $ prefix if present
480
+ if state_key.startswith('$'):
481
+ state_key = state_key[1:]
482
+ # Only process valid state keys
483
+ if state_key and state_key.isalnum():
484
+ # Extract value from useState(value)
485
+ value_match = re.search(r'useState\(([^)]+)\)', line)
486
+ value = value_match.group(1).strip() if value_match else 'null'
487
+ # Store update function for later (outside render)
488
+ self.update_functions.append(f" const update${state_key} = (value) => {{")
489
+ self.update_functions.append(f" if(__STATE__.__.canUpdateStateByKey){{")
490
+ self.update_functions.append(f" updateStateByKey('{state_key}', value);")
491
+ self.update_functions.append(f" {state_key} = value;")
492
+ self.update_functions.append(f" }}")
493
+ self.update_functions.append(f" }};")
494
+ # Add state initialization (inside render)
495
+ init_state_initializations.append(f" update${state_key}({value});")
496
+ else:
497
+ # Pattern 2: const [stateKey] = useState(value) - simple destructuring
498
+ match_simple = re.search(r'const\s+\[([^\]]+)\]\s*=\s*useState\(([^)]+)\)', line)
499
+ if match_simple:
500
+ state_key = match_simple.group(1).strip()
501
+ value = match_simple.group(2).strip()
502
+ if state_key.startswith('$'):
503
+ state_key = state_key[1:]
504
+ if state_key and state_key.isalnum():
505
+ # Store update function for later (outside render)
506
+ self.update_functions.append(f" const update${state_key} = (value) => {{")
507
+ self.update_functions.append(f" if(__STATE__.__.canUpdateStateByKey){{")
508
+ self.update_functions.append(f" {state_key} = value;")
509
+ self.update_functions.append(f" }}")
510
+ self.update_functions.append(f" return updateStateByKey('{state_key}', value);")
511
+ self.update_functions.append(f" }};")
512
+ # Add state initialization (inside render)
513
+ init_state_initializations.append(f" update${state_key}({value});")
514
+
515
+ # Add init state initializations to directives_line (inside render)
516
+ if init_state_initializations:
517
+ if directives_line.strip():
518
+ directives_line += " " + "\n ".join(init_state_initializations) + "\n"
519
+ else:
520
+ directives_line = " " + "\n ".join(init_state_initializations) + "\n"
521
+
522
+ # Add lockUpdateRealState if there are useState declarations from init
523
+ if init_state_initializations:
524
+ directives_line += " lockUpdateRealState();\n"
525
+
526
+ view_id_line = " \n"
527
+
528
+ # Calculate has_prerender - complex logic
529
+ has_prerender = self._calculate_prerender_need(
530
+ has_await, has_fetch, vars_declaration,
531
+ sections_info, template_content,
532
+ usestate_declarations, let_declarations, const_declarations
533
+ )
534
+
535
+ # Process binding directives (@val and @bind)
536
+ template_content = self.binding_directive_service.process_all_binding_directives(template_content)
537
+
538
+ # Update style and show handlers with state variables
539
+ self.style_directive_handler.state_variables = usestate_variables
540
+ self.show_directive_handler.state_variables = usestate_variables
541
+
542
+ # Process @style directive
543
+ template_content = self.style_directive_handler.process_style_directive(template_content)
544
+
545
+ # Process @show directive
546
+ template_content = self.show_directive_handler.process_show_directive(template_content)
547
+
548
+ # Generate render function (setup script will be added to view function instead)
549
+ render_function = self.function_generators.generate_render_function(template_content, vars_declaration, extended_view, extends_expression, extends_data, sections_info, has_prerender, "", directives_line, outer_before, outer_after)
550
+
551
+ # Generate init function
552
+ init_code = '\n '.join(init_functions) if init_functions else ''
553
+ init_function = "function() { " + init_code + " }"
554
+
555
+ # Generate prerender function (chỉ sử dụng vars_declaration thuần túy, không có directives)
556
+ # Fix double braces issue by using string formatting instead of f-string
557
+ if vars_declaration:
558
+ prerender_vars_line = " " + vars_declaration + "\n"
559
+ else:
560
+ prerender_vars_line = ""
561
+
562
+
563
+ prerender_func = self.function_generators.generate_prerender_function(has_await, has_fetch, prerender_vars_line, view_id_line, template_content, extended_view, extends_expression, extends_data, sections_info, conditional_content, has_prerender)
564
+
565
+ # Generate loadServerData function - empty function (logic removed)
566
+ load_server_data_func = self.function_generators.generate_load_server_data_function()
567
+
568
+ # CSS functions - combine CSS từ @onInit và @register
569
+ combined_css_content = css_content.copy() if css_content else []
570
+
571
+ # Thêm CSS từ @register
572
+ if register_data and register_data.get('css'):
573
+ css_data = register_data['css']
574
+
575
+ # Thêm inline CSS
576
+ if css_data.get('inline'):
577
+ combined_css_content.append(css_data['inline'])
578
+
579
+ # Thêm external CSS links
580
+ if css_data.get('external'):
581
+ for css_url in css_data['external']:
582
+ combined_css_content.append(f'/* External CSS: {css_url} */')
583
+
584
+ # CSS functions removed - using scripts/styles arrays instead
585
+
586
+ # Process sections_info to handle script objects properly
587
+ sections_js_object = {}
588
+ for section in sections_info:
589
+ section_name = section.get('name', '')
590
+ section_config = {
591
+ 'type': section.get('type', 'short'),
592
+ 'preloader': section.get('preloader', False),
593
+ 'useVars': section.get('useVars', False)
594
+ }
595
+
596
+ # Handle script object - keep as JavaScript object, not JSON string
597
+ if 'script' in section and section['script']:
598
+ section_config['script'] = section['script'] # Keep as JavaScript object string
599
+ else:
600
+ section_config['script'] = '{}'
601
+
602
+ sections_js_object[section_name] = section_config
603
+
604
+ # Create JavaScript object string instead of array with proper formatting
605
+ sections_parts = []
606
+ for name, config in sections_js_object.items():
607
+ section_parts = [
608
+ f'"type":"{config["type"]}"',
609
+ f'"preloader":{str(config["preloader"]).lower()}',
610
+ f'"useVars":{str(config["useVars"]).lower()}',
611
+ f'"script":{config["script"]}'
612
+ ]
613
+ section_content = ",\n ".join(section_parts)
614
+ sections_parts.append(f' "{name}":{{\n {section_content}\n }}')
615
+
616
+ if sections_parts:
617
+ sections_json = '{\n' + ',\n'.join(sections_parts) + '\n }'
618
+ else:
619
+ sections_json = '{}'
620
+
621
+ # Collect long section names for renderLongSections
622
+ render_long_sections = []
623
+ for section in sections_info:
624
+ section_type = section.get('type', 'short')
625
+ if section_type == 'long':
626
+ section_name = section.get('name', '')
627
+ if section_name:
628
+ render_long_sections.append(f'"{section_name}"')
629
+
630
+ render_long_sections_json = '[' + ','.join(render_long_sections) + ']'
631
+
632
+ # Analyze sections in render and prerender functions
633
+ render_sections = self._extract_sections_from_template(template_content, sections_info)
634
+ prerender_sections = self._extract_sections_from_prerender(has_prerender, has_await, has_fetch, sections_info)
635
+
636
+ render_sections_json = '[' + ','.join([f'"{section}"' for section in render_sections]) + ']'
637
+ prerender_sections_json = '[' + ','.join([f'"{section}"' for section in prerender_sections]) + ']'
638
+
639
+ if extended_view:
640
+ # Static view name
641
+ super_view_config = "'" + extended_view + "'"
642
+ has_super_view = "true"
643
+ elif extends_expression:
644
+ # Dynamic expression - add to config
645
+ super_view_config = extends_expression
646
+ has_super_view = "true"
647
+ else:
648
+ super_view_config = "null"
649
+ has_super_view = "false"
650
+
651
+ # Determine view type
652
+ view_type = "view" # default
653
+ if view_type_data and view_type_data.get('viewType'):
654
+ view_type = view_type_data['viewType']
655
+
656
+ # Prepare wrapperConfig value for view config (from @wrap directive)
657
+ wrapper_config_value = ""
658
+ if wrapper_config:
659
+ # From @wrap directive - use directly
660
+ wrapper_config_value = wrapper_config
661
+ else:
662
+ # Default config
663
+ wrapper_config_value = "{ enable: false, tag: null, follow: true, attributes: {} }"
664
+
665
+ # Normalize legacy `follow` -> `subscribe` in wrapper_config_value.
666
+ # If `subscribe` already present, remove `follow`. Otherwise replace `follow` key with `subscribe`.
667
+ try:
668
+ import re as _re_norm
669
+ if wrapper_config_value and _re_norm.search(r'\bfollow\s*:', wrapper_config_value):
670
+ # If subscribe exists, remove follow occurrence
671
+ if _re_norm.search(r'\bsubscribe\s*:', wrapper_config_value):
672
+ # Remove follow: ... (handle commas and spacing)
673
+ wrapper_config_value = _re_norm.sub(r',?\s*follow\s*:\s*(true|false|\[[^\]]*\]|"[^"]*"|\'[^\']*\'|[^,\}]+)\s*,?', wrapper_config_value)
674
+ # Normalize commas
675
+ wrapper_config_value = _re_norm.sub(r',\s*,', ',', wrapper_config_value)
676
+ wrapper_config_value = _re_norm.sub(r',\s*}', '}', wrapper_config_value)
677
+ else:
678
+ # Replace follow: VALUE with subscribe: VALUE
679
+ def _replace_follow(m):
680
+ val = m.group(1)
681
+ return f'subscribe: {val}'
682
+ wrapper_config_value = _re_norm.sub(r'\bfollow\s*:\s*(true|false|\[[^\]]*\]|"[^"]*"|\'[^\']*\'|[^,\}]+)', _replace_follow, wrapper_config_value)
683
+ except Exception:
684
+ pass
685
+
686
+ # Prepare WRAPPER_CONFIG properties to add to view config (from wraper.js)
687
+ # These are separate properties, NOT nested in wrapperConfig
688
+ wrapper_props_line = ""
689
+ if wrapper_config_content:
690
+ config_content = wrapper_config_content.strip()
691
+ # Remove trailing comma if exists
692
+ if config_content.endswith(','):
693
+ config_content = config_content[:-1]
694
+ # Add comma at the end
695
+ wrapper_props_line = "\n " + config_content.replace('\n', '\n ') + ","
696
+ # Detect state keys for registration - chỉ từ @let và @const, không từ @useState
697
+ state_keys = self._detect_state_keys(blade_code, let_declarations, const_declarations, "")
698
+
699
+ # Add wrapper function content to view function
700
+ wrapper_function_line = self._add_wrapper_content(wrapper_function_content)
701
+
702
+ # NEW: Add wrapper declarations (vars, let, const, useState) at the beginning
703
+ if wrapper_declarations_code:
704
+ wrapper_function_line = wrapper_function_line + wrapper_declarations_code + "\n"
705
+
706
+ # Add setup script to top of file (before export function)
707
+ # Collect setup content ONLY from setup scripts (with setup/view/component attributes)
708
+ # Regular scripts should NOT be here - they are in scripts array and will be inserted into DOM when mounted
709
+ setup_script_line = ""
710
+ setup_scripts = []
711
+
712
+ # Add setup content from setup scripts (imports + remaining code)
713
+ # NOTE: Only scripts with setup/view/component attributes are in setupContent
714
+ # Regular scripts (without these attributes) are in scripts array and should NOT be here
715
+ if register_data and register_data.get('setupContent'):
716
+ setup_content = register_data['setupContent']
717
+ if setup_content and setup_content.strip():
718
+ setup_scripts.append(setup_content)
719
+
720
+ # ❌ REMOVED: Regular scripts should NOT be added to setup_scripts
721
+ # They are already in scripts array and will be inserted into DOM when mounted
722
+ # Adding them here causes duplicate execution (file-level + DOM insertion)
723
+
724
+ # Generate View.registerScript() calls for inline scripts
725
+ # This ensures scripts are registered and can be retrieved by view name and key
726
+ script_registrations_line = ""
727
+ script_registrations = []
728
+ script_function_map = {} # Map script index to function name
729
+
730
+ if register_data and register_data.get('scripts'):
731
+ scripts_data = register_data['scripts']
732
+ for index, script in enumerate(scripts_data):
733
+ if script['type'] == 'code' and script.get('content', '').strip():
734
+ # Generate unique function name/key for script wrapper
735
+ script_function_name = f"__script_{view_name.replace('.', '_')}_{index}"
736
+ script_function_map[index] = script_function_name
737
+
738
+ # Wrap script content in View.registerScript() call
739
+ script_content = script['content']
740
+ # Indent script content for better readability
741
+ indented_content = '\n'.join(' ' + line if line.strip() else line
742
+ for line in script_content.split('\n'))
743
+
744
+ script_registration = f"""View.registerScript('{view_name}', '{script_function_name}', function () {{
745
+ {indented_content}
746
+ }});"""
747
+ script_registrations.append(script_registration)
748
+
749
+ if script_registrations:
750
+ script_registrations_line = '\n\n'.join(script_registrations) + "\n\n"
751
+
752
+ if setup_scripts:
753
+ setup_script_line = '\n\n'.join(setup_scripts) + "\n\n"
754
+
755
+ # Add lifecycle script to userDefined section (inside function)
756
+ lifecycle_script_line = ""
757
+ if register_data and register_data.get('lifecycle'):
758
+ lifecycle_script = register_data['lifecycle']
759
+ if lifecycle_script and lifecycle_script.strip():
760
+ # Lifecycle script is raw object content - use directly
761
+ lifecycle_script_line = lifecycle_script
762
+
763
+ # NOTE: State registration is now handled by DeclarationTracker in wrapper_declarations_code
764
+ # No need to add state registration here anymore
765
+ # if state_keys:
766
+ # # Add useState declarations with registration
767
+ # usestate_declarations_js = self._generate_usestate_declarations(state_keys)
768
+ # wrapper_function_line = wrapper_function_line + usestate_declarations_js
769
+
770
+ # NOTE: Update functions are now handled by DeclarationTracker
771
+ # No need to add update functions here anymore
772
+ # if self.update_functions:
773
+ # update_functions_js = "\n".join(self.update_functions) + "\n"
774
+ # wrapper_function_line = wrapper_function_line + update_functions_js
775
+
776
+ # Prepare userDefined object từ register_data (separate from config)
777
+ user_defined_value = "{}"
778
+ if lifecycle_script_line:
779
+ user_defined_value = lifecycle_script_line
780
+ else:
781
+ user_defined_value = "{}"
782
+
783
+ # Setup imports will be handled in build.py, not here
784
+
785
+ # Add resources array từ register_data
786
+ resources_line = ""
787
+ if register_data and register_data.get('resources'):
788
+ resources_data = register_data['resources']
789
+ resources_json_parts = []
790
+
791
+ for resource in resources_data:
792
+ # Process Blade syntax in attrs
793
+ processed_attrs = {}
794
+ has_template_strings = False
795
+
796
+ for key, value in resource["attrs"].items():
797
+ if isinstance(value, str) and '{{' in value and '}}' in value:
798
+ # Convert Blade syntax to template string
799
+ processed_value = self._convert_blade_to_template_string(value)
800
+ processed_attrs[key] = f'`{processed_value}`'
801
+ has_template_strings = True
802
+ else:
803
+ processed_attrs[key] = value
804
+
805
+ # Format resource object
806
+ if has_template_strings:
807
+ # Build manually to handle template strings
808
+ attrs_parts = []
809
+ for key, value in processed_attrs.items():
810
+ if isinstance(value, str) and value.startswith('`') and value.endswith('`'):
811
+ # Template string - don't quote
812
+ attrs_parts.append(f'"{key}":{value}')
813
+ else:
814
+ # Regular value - use JSON format
815
+ attrs_parts.append(f'"{key}":"{value}"')
816
+
817
+ attrs_str = '{' + ','.join(attrs_parts) + '}'
818
+ resource_parts = [
819
+ f'"tag":"{resource["tag"]}"',
820
+ f'"uuid":"{resource["uuid"]}"',
821
+ f'"attrs":{attrs_str}'
822
+ ]
823
+ resources_json_parts.append('{' + ','.join(resource_parts) + '}')
824
+ else:
825
+ # No template strings - use regular JSON format
826
+ resource_parts = [
827
+ f'"tag":"{resource["tag"]}"',
828
+ f'"uuid":"{resource["uuid"]}"',
829
+ f'"attrs":{self.compiler_utils.format_attrs(resource["attrs"])}'
830
+ ]
831
+ resources_json_parts.append('{' + ','.join(resource_parts) + '}')
832
+
833
+ # Handle template strings in final output
834
+ if any('`' in part for part in resources_json_parts):
835
+ resources_line = "\n resources: [" + ','.join(resources_json_parts) + "]"
836
+ else:
837
+ resources_json = '[' + ','.join(resources_json_parts) + ']'
838
+ resources_line = "\n resources: " + resources_json
839
+ else:
840
+ resources_line = "\n resources: []"
841
+
842
+ # Add scripts array từ register_data
843
+ # Use function wrapper for inline scripts to avoid JSON escaping issues
844
+ scripts_line = ""
845
+ if register_data and register_data.get('scripts'):
846
+ scripts_data = register_data['scripts']
847
+ scripts_json_parts = []
848
+
849
+ for index, script in enumerate(scripts_data):
850
+ script_parts = [f'"type":"{script["type"]}"']
851
+
852
+ if script['type'] == 'code':
853
+ # Use function wrapper instead of content string
854
+ if index in script_function_map:
855
+ script_function_name = script_function_map[index]
856
+ script_parts.append(f'"function":"{script_function_name}"')
857
+ else:
858
+ # Fallback: use content if function not generated
859
+ content_escaped = script["content"].replace('"', '\\"').replace('\n', '\\n')
860
+ script_parts.append(f'"content":"{content_escaped}"')
861
+ elif script['type'] == 'src':
862
+ # Process Blade syntax in src
863
+ src_value = script["src"]
864
+ if '{{' in src_value and '}}' in src_value:
865
+ # Convert Blade syntax to template string
866
+ processed_src = self._convert_blade_to_template_string(src_value)
867
+ script_parts.append(f'"src":`{processed_src}`')
868
+ else:
869
+ # Regular string
870
+ script_parts.append(f'"src":"{src_value}"')
871
+
872
+ # Add id, className, attributes
873
+ if script.get('id'):
874
+ script_parts.append(f'"id":"{script["id"]}"')
875
+ if script.get('className'):
876
+ script_parts.append(f'"className":"{script["className"]}"')
877
+ if script.get('attributes'):
878
+ attrs_json = self.compiler_utils.format_attributes_to_json(script['attributes'])
879
+ script_parts.append(f'"attributes":{attrs_json}')
880
+
881
+ scripts_json_parts.append('{' + ','.join(script_parts) + '}')
882
+
883
+ # Use special handling for template strings - don't put in JSON string
884
+ if any('`' in part for part in scripts_json_parts):
885
+ # Build JavaScript array manually to handle template strings
886
+ scripts_line = "\n scripts: [" + ','.join(scripts_json_parts) + "]"
887
+ else:
888
+ scripts_json = '[' + ','.join(scripts_json_parts) + ']'
889
+ scripts_line = "\n scripts: " + scripts_json
890
+ else:
891
+ scripts_line = "\n scripts: []"
892
+
893
+ # Add styles array từ register_data
894
+ styles_line = ""
895
+ if register_data and register_data.get('styles'):
896
+ styles_data = register_data['styles']
897
+ styles_json_parts = []
898
+
899
+ for style in styles_data:
900
+ style_parts = [f'"type":"{style["type"]}"']
901
+
902
+ if style['type'] == 'code':
903
+ content_escaped = style["content"].replace('"', '\\"').replace('\n', '\\n')
904
+ style_parts.append(f'"content":"{content_escaped}"')
905
+ elif style['type'] == 'href':
906
+ # Process Blade syntax in href
907
+ href_value = style["href"]
908
+ if '{{' in href_value and '}}' in href_value:
909
+ # Convert Blade syntax to template string
910
+ processed_href = self._convert_blade_to_template_string(href_value)
911
+ style_parts.append(f'"href":`{processed_href}`')
912
+ else:
913
+ # Regular string
914
+ style_parts.append(f'"href":"{href_value}"')
915
+
916
+ # Add id, className, attributes
917
+ if style.get('id'):
918
+ style_parts.append(f'"id":"{style["id"]}"')
919
+ if style.get('className'):
920
+ style_parts.append(f'"className":"{style["className"]}"')
921
+ if style.get('attributes'):
922
+ attrs_json = self.compiler_utils.format_attributes_to_json(style['attributes'])
923
+ style_parts.append(f'"attributes":{attrs_json}')
924
+
925
+ styles_json_parts.append('{' + ','.join(style_parts) + '}')
926
+
927
+ # Use special handling for template strings - don't put in JSON string
928
+ if any('`' in part for part in styles_json_parts):
929
+ # Build JavaScript array manually to handle template strings
930
+ styles_line = "\n styles: [" + ','.join(styles_json_parts) + "]"
931
+ else:
932
+ styles_json = '[' + ','.join(styles_json_parts) + ']'
933
+ styles_line = "\n styles: " + styles_json
934
+ else:
935
+ styles_line = "\n styles: []"
936
+
937
+ # If wrapperConfig contains a `subscribe` property and no explicit subscribe_config
938
+ # was provided via other directives, read it but keep the property inside
939
+ # `wrapper_config_value` (do not remove it). This preserves subscribe inside
940
+ # wrapperConfig while still allowing top-level `subscribe` detection when needed.
941
+ if subscribe_config is None and wrapper_config_value:
942
+ try:
943
+ import re as _re_sub
944
+ m = _re_sub.search(r'\bsubscribe\s*:\s*(true|false|\[[^\]]*\])', wrapper_config_value)
945
+ if m:
946
+ wrapper_subscribe_val = m.group(1)
947
+ # Set subscribe_config from wrapper but do NOT remove it from wrapper_config_value
948
+ if wrapper_subscribe_val in ('true', 'false'):
949
+ subscribe_config = (wrapper_subscribe_val == 'true')
950
+ else:
951
+ inner = wrapper_subscribe_val.strip()[1:-1].strip()
952
+ if inner:
953
+ parts = [p.strip().strip('"\'') for p in inner.split(',') if p.strip()]
954
+ subscribe_config = parts
955
+ else:
956
+ subscribe_config = []
957
+ except Exception:
958
+ pass
959
+
960
+ # If wrapper exists and no explicit subscribe/dontsubscribe directive was provided,
961
+ # default to enabling subscribe and ensure wrapper_config_value contains it.
962
+ if wrapper_config and subscribe_config is None and wrapper_config_value:
963
+ try:
964
+ import re as _re_ins
965
+ if not _re_ins.search(r'\bsubscribe\s*:', wrapper_config_value):
966
+ # Insert subscribe: true right after opening brace
967
+ wrapper_config_value = _re_ins.sub(r'^\s*\{', '{ subscribe: true,', wrapper_config_value, count=1)
968
+ # Also set top-level subscribe_config to True for consistency
969
+ subscribe_config = True
970
+ except Exception:
971
+ pass
972
+
973
+ # Build the return string carefully to avoid syntax errors
974
+ # Build subscribe config JS value
975
+ # Optimization: If no vars/useState and no explicit subscribe config, default to false
976
+ if subscribe_config is None:
977
+ # Check if view uses vars or state
978
+ has_vars = bool(vars_declaration and vars_declaration.strip())
979
+ has_state = bool(state_keys) # state_keys detected from @let/@const/@useState
980
+
981
+ # If no state/vars used and no explicit subscribe directive, default to false
982
+ # Reasoning: without state/vars, re-rendering won't change anything
983
+ if not has_vars and not has_state:
984
+ subscribe_js = 'false'
985
+ else:
986
+ subscribe_js = 'true'
987
+ elif isinstance(subscribe_config, bool):
988
+ subscribe_js = 'true' if subscribe_config else 'false'
989
+ else:
990
+ subscribe_js = json.dumps(subscribe_config, ensure_ascii=False)
991
+
992
+ # If an explicit subscribe/dontsubscribe directive was provided,
993
+ # ensure wrapperConfig.subscribe mirrors that value so runtime
994
+ # logic (which reads wrapperConfig) remains consistent.
995
+ if wrapper_config_value and subscribe_config is not None:
996
+ try:
997
+ import re as _re_fix
998
+ if _re_fix.search(r'\bsubscribe\s*:', wrapper_config_value):
999
+ wrapper_config_value = _re_fix.sub(r'subscribe\s*:\s*[^,}]+', f'subscribe: {subscribe_js}', wrapper_config_value, count=1)
1000
+ else:
1001
+ wrapper_config_value = _re_fix.sub(r'^\s*\{', '{ subscribe: ' + subscribe_js + ',', wrapper_config_value, count=1)
1002
+ except Exception:
1003
+ pass
1004
+
1005
+ # Add View import if scripts are registered
1006
+ view_import = ""
1007
+ if script_registrations:
1008
+ view_import = "import { View } from '../core/View.js';\n\n"
1009
+
1010
+ return_template = setup_script_line + view_import + script_registrations_line + """export function """ + function_name + """($$$DATA$$$ = {}, systemData = {}) {
1011
+ const {App, View, __base__, __layout__, __page__, __component__, __template__, __context__, __partial__, __system__, __env = {}, __helper = {}} = systemData;
1012
+ const __VIEW_PATH__ = '""" + view_name + """';
1013
+ const __VIEW_ID__ = $$$DATA$$$.__SSR_VIEW_ID__ || """ + JS_FUNCTION_PREFIX + """.generateViewId();
1014
+ const __VIEW_TYPE__ = '""" + view_type + """';
1015
+ const cpparts = __VIEW_PATH__.split('.');
1016
+ cpparts.pop(); // Remove view name
1017
+ const __VIEW_NAMESPACE__ = cpparts.length > 0 ? cpparts.join('.') + '.' : '';
1018
+ """ + wrapper_function_line + """
1019
+ self.setup('""" + view_name + """', """ + user_defined_value + """, {
1020
+ superView: """ + super_view_config + """,
1021
+ hasSuperView: """ + has_super_view + """,
1022
+ viewType: '""" + view_type + """',
1023
+ sections: """ + sections_json + """,
1024
+ wrapperConfig: """ + wrapper_config_value + """,""" + wrapper_props_line + """
1025
+ hasAwaitData: """ + str(has_await).lower() + """,
1026
+ hasFetchData: """ + str(has_fetch).lower() + """,
1027
+ subscribe: """ + subscribe_js + """,
1028
+ fetch: """ + (self.compiler_utils.format_fetch_config(fetch_config) if fetch_config else 'null') + """,
1029
+ data: $$$DATA$$$,
1030
+ viewId: __VIEW_ID__,
1031
+ path: __VIEW_PATH__,
1032
+ usesVars: """ + str(bool(vars_declaration)).lower() + """,
1033
+ hasSections: """ + str(bool(sections)).lower() + """,
1034
+ hasSectionPreload: """ + str(any(section.get('preloader', False) for section in sections_info)).lower() + """,
1035
+ hasPrerender: """ + str(has_prerender).lower() + """,
1036
+ renderLongSections: """ + render_long_sections_json + """,
1037
+ renderSections: """ + render_sections_json + """,
1038
+ prerenderSections: """ + prerender_sections_json + """,""" + scripts_line + """,""" + styles_line + """,""" + resources_line + """,
1039
+ commitConstructorData: function() {
1040
+ // Then update states from data
1041
+ """ + self._generate_state_updates(state_declarations) + """
1042
+ // Finally lock state updates
1043
+ """ + ("lockUpdateRealState();" if state_declarations else "") + """
1044
+ },
1045
+ updateVariableData: function(data) {
1046
+ // Update all variables first
1047
+ for (const key in data) {
1048
+ if (data.hasOwnProperty(key)) {
1049
+ // Call updateVariableItemData directly from config
1050
+ if (typeof this.config.updateVariableItemData === 'function') {
1051
+ this.config.updateVariableItemData.call(this, key, data[key]);
1052
+ }
1053
+ }
1054
+ }
1055
+ // Then update states from data
1056
+ """ + self._generate_state_updates(state_declarations) + """
1057
+ // Finally lock state updates
1058
+ """ + ("lockUpdateRealState();" if state_declarations else "") + """
1059
+ },
1060
+ updateVariableItemData: function(key, value) {
1061
+ this.data[key] = value;
1062
+ if (typeof __UPDATE_DATA_TRAIT__[key] === "function") {
1063
+ __UPDATE_DATA_TRAIT__[key](value);
1064
+ }
1065
+ },
1066
+ prerender: """ + prerender_func + """,
1067
+ render: """ + render_function + """
1068
+ });
1069
+ return self;
1070
+ }"""
1071
+
1072
+ # Restore verbatim blocks in final return_template (in case any were in scripts/styles)
1073
+ # For scripts/styles, we also need to escape backticks and ${} if they're in template strings
1074
+ # Escape to prevent breaking template string syntax
1075
+ for placeholder, content in verbatim_blocks.items():
1076
+ # Escape backticks and ${} in verbatim content for safe insertion into template strings
1077
+ # Step 1: Protect already escaped sequences
1078
+ protected_content = content.replace('\\`', '__ESCAPED_BACKTICK__')
1079
+ protected_content = protected_content.replace('\\${', '__ESCAPED_DOLLAR_BRACE__')
1080
+
1081
+ # Step 2: Escape unescaped backticks and ${ sequences
1082
+ protected_content = protected_content.replace('`', '\\`')
1083
+ protected_content = protected_content.replace('${', '\\${')
1084
+
1085
+ # Step 3: Restore protected sequences (now double-escaped)
1086
+ protected_content = protected_content.replace('__ESCAPED_BACKTICK__', '\\\\`')
1087
+ protected_content = protected_content.replace('__ESCAPED_DOLLAR_BRACE__', '\\\\${')
1088
+
1089
+ return_template = return_template.replace(placeholder, protected_content)
1090
+
1091
+ return return_template
1092
+
1093
+ def _add_wrapper_content(self, wrapper_function_content):
1094
+ """Add wrapper content to view function"""
1095
+ if wrapper_function_content:
1096
+ return wrapper_function_content + "\n"
1097
+ return ""
1098
+
1099
+ def _extract_sections_from_template(self, template_content, sections_info):
1100
+ """Extract section names that are used in the template content"""
1101
+ sections_used = []
1102
+ if not template_content or not sections_info:
1103
+ return sections_used
1104
+
1105
+ for section in sections_info:
1106
+ section_name = section.get('name', '')
1107
+ if section_name:
1108
+ # Check if section is used in template content
1109
+ if f"App.View.section('{section_name}'" in template_content:
1110
+ sections_used.append(section_name)
1111
+
1112
+ return sections_used
1113
+
1114
+ def _extract_sections_from_prerender(self, has_prerender, has_await, has_fetch, sections_info):
1115
+ """Extract section names that are used in prerender function"""
1116
+ sections_used = []
1117
+
1118
+ if not has_prerender:
1119
+ return sections_used
1120
+
1121
+ # If has prerender, check which sections are used
1122
+ # For now, we'll include sections that have preloader or are used with await/fetch
1123
+ for section in sections_info:
1124
+ section_name = section.get('name', '')
1125
+ if section_name:
1126
+ # Include sections that have preloader
1127
+ if section.get('preloader', False):
1128
+ sections_used.append(section_name)
1129
+ # Include sections used with await/fetch (these typically need prerender)
1130
+ elif (has_await or has_fetch) and section.get('useVars', False):
1131
+ sections_used.append(section_name)
1132
+
1133
+ return sections_used
1134
+
1135
+ def _extract_usestate_variables(self, usestate_declarations, all_declarations):
1136
+ """
1137
+ Extract usestate variable names from declarations
1138
+ Returns a set of variable names that have useState declarations
1139
+ Includes both state variable names and setter names
1140
+ """
1141
+ usestate_variables = set()
1142
+
1143
+ # Extract from usestate_declarations string
1144
+ if usestate_declarations:
1145
+ # Pattern: const [stateKey, setStateKey] = useState(...)
1146
+ matches = re.findall(r'const\s+\[([^,]+),\s*([^\]]+)\]', usestate_declarations)
1147
+ for match in matches:
1148
+ state_var = match[0].strip()
1149
+ setter_var = match[1].strip()
1150
+ if state_var and state_var.isalnum():
1151
+ usestate_variables.add(state_var)
1152
+ if setter_var and setter_var.isalnum():
1153
+ usestate_variables.add(setter_var)
1154
+
1155
+ # Extract from all_declarations (from DeclarationTracker)
1156
+ for decl in all_declarations:
1157
+ if decl.get('type') in ['useState', 'const', 'let']:
1158
+ variables = decl.get('variables', [])
1159
+ for var in variables:
1160
+ if var.get('isUseState'):
1161
+ names = var.get('names', [])
1162
+ if names and len(names) > 0:
1163
+ # First name is the state variable
1164
+ state_var = names[0]
1165
+ if state_var and state_var.isalnum():
1166
+ usestate_variables.add(state_var)
1167
+ # Second name is the setter
1168
+ if len(names) > 1:
1169
+ setter_var = names[1]
1170
+ if setter_var and setter_var.isalnum():
1171
+ usestate_variables.add(setter_var)
1172
+
1173
+ return usestate_variables
1174
+
1175
+ def _detect_state_keys(self, blade_code, let_declarations, const_declarations, usestate_declarations):
1176
+ """Detect state keys from directives that use useState or destructuring"""
1177
+ state_keys = set()
1178
+
1179
+ # Check @useState directives
1180
+ if usestate_declarations:
1181
+ for declaration in usestate_declarations.split('\n'):
1182
+ if declaration.strip():
1183
+ # Extract state key from @useState(value, stateKey)
1184
+ # Pattern: const [stateKey] = useState(value);
1185
+ match = re.search(r'const\s+\[([^,]+),\s*[^]]+\]\s*=\s*useState\([^)]+\);', declaration)
1186
+ if match:
1187
+ state_key = match.group(1).strip()
1188
+ state_keys.add(state_key)
1189
+
1190
+ # Check @let and @const directives for destructuring patterns
1191
+ all_declarations = []
1192
+ if let_declarations:
1193
+ all_declarations.extend(let_declarations.split('\n'))
1194
+ if const_declarations:
1195
+ all_declarations.extend(const_declarations.split('\n'))
1196
+
1197
+ for declaration in all_declarations:
1198
+ if declaration.strip():
1199
+ # Check for [stateKey] = useState(...) pattern
1200
+ match = re.search(r'\[([^,]+),\s*[^]]+\]\s*=\s*useState\(', declaration)
1201
+ if match:
1202
+ state_key = match.group(1).strip()
1203
+ # Remove $ prefix if present
1204
+ if state_key.startswith('$'):
1205
+ state_key = state_key[1:]
1206
+ # Only add valid state keys (not empty or special characters)
1207
+ if state_key and state_key.isalnum():
1208
+ state_keys.add(state_key)
1209
+
1210
+ return list(state_keys)
1211
+
1212
+ def _generate_state_registration(self, state_keys):
1213
+ """Generate state registration code"""
1214
+ if not state_keys:
1215
+ return ""
1216
+
1217
+ registration_lines = []
1218
+ for state_key in state_keys:
1219
+ registration_lines.append(f" __STATE__.__.register('{state_key}');")
1220
+
1221
+ registration_lines.append(" __STATE__.__.lockRegister();")
1222
+
1223
+ return '\n'.join(registration_lines) + '\n'
1224
+
1225
+ def _generate_usestate_declarations(self, state_keys):
1226
+ """Generate useState declarations with custom setters"""
1227
+ if not state_keys:
1228
+ return ""
1229
+
1230
+ declaration_lines = []
1231
+ for state_key in state_keys:
1232
+ # Convert stateKey to set$stateKey format (e.g., todos -> set$todos)
1233
+ set_state_key = f'set${state_key}'
1234
+ declaration_lines.append(f" const {set_state_key} = __STATE__.__.register('{state_key}');")
1235
+ declaration_lines.append(f" let {state_key} = null;")
1236
+ declaration_lines.append(f" const {set_state_key} = (state) => {{")
1237
+ declaration_lines.append(f" {state_key} = state;")
1238
+ declaration_lines.append(f" {set_state_key}(state);")
1239
+ declaration_lines.append(f" }};")
1240
+
1241
+ # Don't add lockRegister - not needed anymore
1242
+
1243
+ return '\n'.join(declaration_lines) + '\n'
1244
+
1245
+ def _calculate_prerender_need(self, has_await, has_fetch, vars_declaration, sections_info, template_content, usestate_declarations, let_declarations, const_declarations):
1246
+ """
1247
+ Calculate if prerender is needed based on complex logic:
1248
+ - Need (has_await OR has_fetch) AND vars_declaration AND (sections use vars OR useState/let/const with vars)
1249
+ """
1250
+ # Must have await/fetch AND vars declaration
1251
+ if not (has_await or has_fetch) or not vars_declaration:
1252
+ return False
1253
+
1254
+ # Extract variable names from @vars declaration
1255
+ vars_names = self._extract_vars_names(vars_declaration)
1256
+ if not vars_names:
1257
+ return False
1258
+
1259
+ # Check if any section uses vars
1260
+ has_sections_with_vars = any(section.get('useVars', False) for section in sections_info)
1261
+
1262
+ # Check if template content uses vars
1263
+ has_template_with_vars = self._template_uses_vars(template_content, vars_names)
1264
+
1265
+ # Check if useState/let/const declarations use vars
1266
+ has_declarations_with_vars = self._declarations_use_vars(
1267
+ usestate_declarations, let_declarations, const_declarations, vars_names
1268
+ )
1269
+
1270
+ # Need prerender if any of these conditions are true
1271
+ return has_sections_with_vars or has_template_with_vars or has_declarations_with_vars
1272
+
1273
+ def _extract_vars_names(self, vars_declaration):
1274
+ """Extract variable names from @vars declaration"""
1275
+ if not vars_declaration:
1276
+ return []
1277
+
1278
+ # Extract variable names from {var1, var2} pattern (without let)
1279
+ import re
1280
+ pattern = r'\{([^}]+)\}'
1281
+ match = re.search(pattern, vars_declaration)
1282
+ if match:
1283
+ vars_str = match.group(1)
1284
+ # Split by comma and clean up
1285
+ return [var.strip().replace('$', '') for var in vars_str.split(',') if var.strip()]
1286
+
1287
+ return []
1288
+
1289
+ def _template_uses_vars(self, template_content, vars_names):
1290
+ """Check if template content uses any of the vars"""
1291
+ if not template_content or not vars_names:
1292
+ return False
1293
+
1294
+ # Check if any var name appears in template content
1295
+ for var_name in vars_names:
1296
+ if f'${{{var_name}}}' in template_content or f'${{' + var_name + '}}' in template_content:
1297
+ return True
1298
+
1299
+ return False
1300
+
1301
+ def _declarations_use_vars(self, usestate_declarations, let_declarations, const_declarations, vars_names):
1302
+ """Check if useState/let/const declarations use any of the vars"""
1303
+ if not vars_names:
1304
+ return False
1305
+
1306
+ all_declarations = []
1307
+ if usestate_declarations:
1308
+ all_declarations.append(usestate_declarations)
1309
+ if let_declarations:
1310
+ all_declarations.append(let_declarations)
1311
+ if const_declarations:
1312
+ all_declarations.append(const_declarations)
1313
+
1314
+ # Check if any declaration uses vars
1315
+ for declaration in all_declarations:
1316
+ for var_name in vars_names:
1317
+ if var_name in declaration:
1318
+ return True
1319
+
1320
+ return False
1321
+
1322
+ def _filter_directives_only(self, content):
1323
+ """Filter content to only include directives (@if/@section/@block), remove pure HTML
1324
+ Returns filtered content or empty string if no directives found
1325
+ """
1326
+ import re
1327
+
1328
+ if not content or not content.strip():
1329
+ return ''
1330
+
1331
+ # Extract only directive expressions (${...})
1332
+ # This regex finds ${...} patterns with proper nesting
1333
+ directive_parts = []
1334
+
1335
+ i = 0
1336
+ while i < len(content):
1337
+ # Find next ${
1338
+ match = re.search(r'\$\{(?:App\.View\.execute|App\.View\.section|this\.__section|this\.__useBlock|App\.View\.useBlock|this\.__subscribeBlock)\(', content[i:])
1339
+
1340
+ if not match:
1341
+ break
1342
+
1343
+ # Found a directive start
1344
+ start_pos = i + match.start()
1345
+
1346
+ # Find matching closing }
1347
+ depth = 0
1348
+ pos = start_pos + 2 # Skip ${
1349
+ in_string = False
1350
+ string_char = None
1351
+
1352
+ while pos < len(content):
1353
+ char = content[pos]
1354
+
1355
+ # Handle string escaping
1356
+ if char == '\\' and in_string:
1357
+ pos += 2
1358
+ continue
1359
+
1360
+ # Handle string delimiters
1361
+ if char in ['"', "'", '`'] and (pos == 0 or content[pos-1] != '\\'):
1362
+ if not in_string:
1363
+ in_string = True
1364
+ string_char = char
1365
+ elif char == string_char:
1366
+ in_string = False
1367
+ string_char = None
1368
+
1369
+ if not in_string:
1370
+ if char == '{':
1371
+ depth += 1
1372
+ elif char == '}':
1373
+ if depth == 0:
1374
+ # Found the closing }
1375
+ directive_parts.append(content[start_pos:pos+1])
1376
+ i = pos + 1
1377
+ break
1378
+ else:
1379
+ depth -= 1
1380
+
1381
+ pos += 1
1382
+ else:
1383
+ # Didn't find closing brace, skip this one
1384
+ i = start_pos + 1
1385
+ continue
1386
+
1387
+ # If no directives found, return empty
1388
+ if not directive_parts:
1389
+ return ''
1390
+
1391
+ # Join all directive parts with newlines
1392
+ result = '\n'.join(directive_parts)
1393
+
1394
+ return result
1395
+
1396
+ def _extract_wrapper_inner_content(self, template_content):
1397
+ """Extract inner and outer content from wrapper directive
1398
+ Returns: (inner_content, outer_before, outer_after)
1399
+ """
1400
+ import re
1401
+
1402
+ # Find __WRAPPER_CONFIG__ position (marks the start of wrapper)
1403
+ config_match = re.search(r'__WRAPPER_CONFIG__\s*=\s*', template_content)
1404
+
1405
+ if not config_match:
1406
+ return template_content, '', ''
1407
+
1408
+ # Content before wrapper
1409
+ outer_before = template_content[:config_match.start()].strip()
1410
+
1411
+ # Find where config ends (after the semicolon)
1412
+ config_end = config_match.end()
1413
+
1414
+ # Parse to find end of config object
1415
+ if config_end < len(template_content) and template_content[config_end] == '{':
1416
+ brace_count = 0
1417
+ in_string = False
1418
+ string_char = None
1419
+
1420
+ for i in range(config_end, len(template_content)):
1421
+ char = template_content[i]
1422
+
1423
+ if char in ['"', "'"] and (i == 0 or template_content[i-1] != '\\'):
1424
+ if not in_string:
1425
+ in_string = True
1426
+ string_char = char
1427
+ elif char == string_char:
1428
+ in_string = False
1429
+ string_char = None
1430
+
1431
+ if not in_string:
1432
+ if char == '{':
1433
+ brace_count += 1
1434
+ elif char == '}':
1435
+ brace_count -= 1
1436
+ if brace_count == 0:
1437
+ config_end = i + 1
1438
+ if config_end < len(template_content) and template_content[config_end] == ';':
1439
+ config_end += 1
1440
+ break
1441
+
1442
+ # Skip whitespace/newlines after config
1443
+ while config_end < len(template_content) and template_content[config_end] in [' ', '\n', '\r', '\t']:
1444
+ config_end += 1
1445
+
1446
+ # Find __WRAPPER_END__ marker
1447
+ end_match = re.search(r'__WRAPPER_END__', template_content[config_end:])
1448
+
1449
+ if end_match:
1450
+ # Extract content between config and end marker
1451
+ inner_end = config_end + end_match.start()
1452
+ inner_content = template_content[config_end:inner_end]
1453
+
1454
+ # Content after wrapper
1455
+ outer_after_start = config_end + end_match.end()
1456
+ outer_after = template_content[outer_after_start:].strip()
1457
+ else:
1458
+ # No end marker found, take all content after config (fallback)
1459
+ inner_content = template_content[config_end:]
1460
+ outer_after = ''
1461
+
1462
+ # Return separate before and after
1463
+ return inner_content, outer_before, outer_after
1464
+
1465
+ def _extract_wrapper_config(self, template_content):
1466
+ """Extract wrapper config from template content and return both config and cleaned template"""
1467
+ import re
1468
+
1469
+ # Find __WRAPPER_CONFIG__ = position
1470
+ match = re.search(r'__WRAPPER_CONFIG__\s*=\s*', template_content)
1471
+
1472
+ if not match:
1473
+ return None
1474
+
1475
+ # Start parsing from the opening brace
1476
+ start_pos = match.end()
1477
+ if start_pos >= len(template_content) or template_content[start_pos] != '{':
1478
+ return None
1479
+
1480
+ # Parse balanced braces
1481
+ brace_count = 0
1482
+ in_string = False
1483
+ string_char = None
1484
+ end_pos = start_pos
1485
+
1486
+ for i in range(start_pos, len(template_content)):
1487
+ char = template_content[i]
1488
+
1489
+ # Handle strings
1490
+ if char in ['"', "'"] and (i == 0 or template_content[i-1] != '\\'):
1491
+ if not in_string:
1492
+ in_string = True
1493
+ string_char = char
1494
+ elif char == string_char:
1495
+ in_string = False
1496
+ string_char = None
1497
+
1498
+ # Handle braces (only when not in string)
1499
+ if not in_string:
1500
+ if char == '{':
1501
+ brace_count += 1
1502
+ elif char == '}':
1503
+ brace_count -= 1
1504
+ if brace_count == 0:
1505
+ # Found matching closing brace
1506
+ end_pos = i + 1
1507
+ # Look for semicolon
1508
+ if end_pos < len(template_content) and template_content[end_pos] == ';':
1509
+ end_pos += 1
1510
+ config_str = template_content[start_pos:end_pos-1] # Exclude semicolon
1511
+ return config_str
1512
+
1513
+ return None
1514
+
1515
+ def _generate_wrapper_declarations(self, declarations):
1516
+ """
1517
+ Generate wrapper function declarations from DeclarationTracker
1518
+ Returns: (wrapper_code, variable_list, state_declarations)
1519
+ """
1520
+
1521
+ wrapper_lines = []
1522
+ variable_list = []
1523
+ update_trait_items = []
1524
+ state_declarations = [] # Store useState declarations separately
1525
+
1526
+ # First, generate __UPDATE_DATA_TRAIT__
1527
+ wrapper_lines.append(" const __UPDATE_DATA_TRAIT__ = {};")
1528
+
1529
+ for decl in declarations:
1530
+ decl_type = decl['type']
1531
+ variables = decl['variables']
1532
+
1533
+
1534
+ if decl_type == 'vars':
1535
+ # Process @vars variables -> destructure from $$$DATA$$$
1536
+ # Build destructuring parts with defaults when provided
1537
+ destructure_parts = []
1538
+ for var in variables:
1539
+ var_name = var['name']
1540
+ if var['hasDefault']:
1541
+ destructure_parts.append(f"{var_name} = {var['value']}")
1542
+ else:
1543
+ destructure_parts.append(f"{var_name}")
1544
+ # Add to __UPDATE_DATA_TRAIT__ and variable list
1545
+ update_trait_items.append(f" __UPDATE_DATA_TRAIT__.{var_name} = value => {var_name} = value;")
1546
+ variable_list.append(var_name)
1547
+ # Emit single destructuring statement
1548
+ if destructure_parts:
1549
+ wrapper_lines.append(f" let {{{', '.join(destructure_parts)}}} = $$$DATA$$$;")
1550
+
1551
+ elif decl_type == 'let':
1552
+ # Process @let variables
1553
+ for var in variables:
1554
+
1555
+ if var.get('isDestructuring'):
1556
+ # Handle destructuring
1557
+ if var.get('isUseState'):
1558
+ # [$stateKey, $setter] = useState($value)
1559
+ # Keep OLD state registration style, not React style!
1560
+ names = var['names']
1561
+ value = var['value']
1562
+
1563
+ # Extract state key and setter name
1564
+ if len(names) >= 2:
1565
+ state_key = names[0]
1566
+ setter_name_raw = names[1]
1567
+
1568
+ # Keep user-declared setter name as is
1569
+ setter_name = setter_name_raw
1570
+
1571
+ # Extract initial value from useState(value)
1572
+ import re
1573
+ match = re.search(r'useState\s*\(\s*(.+)\s*\)', value)
1574
+ initial_value = match.group(1) if match else 'null'
1575
+
1576
+ # Generate OLD style state registration
1577
+ state_declarations.append({
1578
+ 'stateKey': state_key,
1579
+ 'setterName': setter_name,
1580
+ 'initialValue': initial_value
1581
+ })
1582
+ # Don't add to variable_list or update_trait (useState variables)
1583
+ else:
1584
+ # Regular destructuring: [$a, $b] = [1, 2]
1585
+ names = var['names']
1586
+ value = var['value']
1587
+ bracket_type = '[' if var['destructuringType'] == 'array' else '{'
1588
+ close_bracket = ']' if var['destructuringType'] == 'array' else '}'
1589
+ wrapper_lines.append(f" let {bracket_type}{', '.join(names)}{close_bracket} = {value};")
1590
+
1591
+ # Add each destructured variable to update trait and variable list
1592
+ for name in names:
1593
+ update_trait_items.append(f" __UPDATE_DATA_TRAIT__.{name} = value => {name} = value;")
1594
+ variable_list.append(name)
1595
+ else:
1596
+ # Regular variable
1597
+ var_name = var['name']
1598
+ if var['hasDefault']:
1599
+ wrapper_lines.append(f" let {var_name} = {var['value']};")
1600
+ else:
1601
+ wrapper_lines.append(f" let {var_name};")
1602
+
1603
+ # Add to update trait and variable list
1604
+ update_trait_items.append(f" __UPDATE_DATA_TRAIT__.{var_name} = value => {var_name} = value;")
1605
+ variable_list.append(var_name)
1606
+
1607
+ elif decl_type == 'const':
1608
+ # Process @const variables
1609
+ for var in variables:
1610
+ if var.get('isDestructuring'):
1611
+ # Handle destructuring
1612
+ if var.get('isUseState'):
1613
+ # [$stateKey, $setter] = useState($value)
1614
+ # Keep OLD state registration style!
1615
+ names = var['names']
1616
+ value = var['value']
1617
+
1618
+ # Extract state key and setter name
1619
+ if len(names) >= 2:
1620
+ state_key = names[0]
1621
+ setter_name_raw = names[1]
1622
+
1623
+ # Keep user-declared setter name as is
1624
+ setter_name = setter_name_raw
1625
+
1626
+ # Extract initial value from useState(value)
1627
+ import re
1628
+ match = re.search(r'useState\s*\(\s*(.+)\s*\)', value)
1629
+ initial_value = match.group(1) if match else 'null'
1630
+
1631
+ # Generate OLD style state registration
1632
+ state_declarations.append({
1633
+ 'stateKey': state_key,
1634
+ 'setterName': setter_name,
1635
+ 'initialValue': initial_value
1636
+ })
1637
+ # Don't add to variable_list or update_trait (useState variables)
1638
+ else:
1639
+ # Regular const destructuring
1640
+ names = var['names']
1641
+ value = var['value']
1642
+ bracket_type = '[' if var['destructuringType'] == 'array' else '{'
1643
+ close_bracket = ']' if var['destructuringType'] == 'array' else '}'
1644
+ wrapper_lines.append(f" const {bracket_type}{', '.join(names)}{close_bracket} = {value};")
1645
+ # Const variables are not mutable, don't add to update_trait or variable_list
1646
+ else:
1647
+ # Regular const
1648
+ var_name = var['name']
1649
+ if var['hasDefault']:
1650
+ wrapper_lines.append(f" const {var_name} = {var['value']};")
1651
+ # Const variables are not mutable, don't add to update_trait or variable_list
1652
+
1653
+ elif decl_type == 'useState':
1654
+ # Process @useState directives - same as @let/@const with useState
1655
+ for var in variables:
1656
+ if var.get('isDestructuring') and var.get('isUseState'):
1657
+ # [@useState($value, $varName, $setVarName)] becomes [$varName, $setVarName] = useState($value)
1658
+ names = var['names']
1659
+ value = var['value']
1660
+
1661
+ # Extract state key and setter name
1662
+ if len(names) >= 2:
1663
+ state_key = names[0]
1664
+ setter_name_raw = names[1]
1665
+
1666
+ # Keep user-declared setter name as is
1667
+ setter_name = setter_name_raw
1668
+
1669
+ # Extract initial value from useState(value)
1670
+ import re
1671
+ match = re.search(r'useState\s*\(\s*(.+)\s*\)', value)
1672
+ initial_value = match.group(1) if match else 'null'
1673
+
1674
+ # Generate OLD style state registration
1675
+ state_declarations.append({
1676
+ 'stateKey': state_key,
1677
+ 'setterName': setter_name,
1678
+ 'initialValue': initial_value
1679
+ })
1680
+
1681
+ # Add update trait assignments after all variable declarations
1682
+ wrapper_lines.extend(update_trait_items)
1683
+
1684
+ # Generate __VARIABLE_LIST__
1685
+ variable_list_str = ', '.join([f'"{v}"' for v in variable_list])
1686
+ wrapper_lines.append(f" const __VARIABLE_LIST__ = [{variable_list_str}];")
1687
+
1688
+ # Generate OLD style state registrations
1689
+ for state in state_declarations:
1690
+ state_key = state['stateKey']
1691
+ setter_name = state['setterName'] # Keep user-declared name as is
1692
+ initial_value = state['initialValue']
1693
+
1694
+ # Convert setStateKey to set$stateKey format for register name only
1695
+ register_setter_name = setter_name
1696
+ if setter_name.startswith('set') and len(setter_name) > 3:
1697
+ # setTodos -> set$todos (convert first char after 'set' to lowercase)
1698
+ state_part = setter_name[3:] # Remove 'set'
1699
+ if state_part:
1700
+ state_part_lower = state_part[0].lower() + state_part[1:] if len(state_part) > 1 else state_part.lower()
1701
+ register_setter_name = f'set${state_part_lower}'
1702
+
1703
+ internal_register = f"set${state_key}" # __set$todosRegister
1704
+ wrapper_lines.append(f" const {internal_register} = __STATE__.__.register('{state_key}');")
1705
+ wrapper_lines.append(f" let {state_key} = null;")
1706
+ wrapper_lines.append(f" const {setter_name} = (state) => {{")
1707
+ wrapper_lines.append(f" {state_key} = state;")
1708
+ wrapper_lines.append(f" {internal_register}(state);")
1709
+ wrapper_lines.append(f" }};")
1710
+ wrapper_lines.append(f" __STATE__.__.setters.{setter_name} = {setter_name};")
1711
+ wrapper_lines.append(f" __STATE__.__.setters.{state_key} = {setter_name};")
1712
+ # Add update$stateKey function - use state_key as is
1713
+ update_func_name = f"update${state_key}"
1714
+ wrapper_lines.append(f" const {update_func_name} = (value) => {{")
1715
+ wrapper_lines.append(f" if(__STATE__.__.canUpdateStateByKey){{")
1716
+ wrapper_lines.append(f" updateStateByKey('{state_key}', value);")
1717
+ wrapper_lines.append(f" {state_key} = value;")
1718
+ wrapper_lines.append(f" }}")
1719
+ wrapper_lines.append(f" }};")
1720
+
1721
+ wrapper_code = '\n'.join(wrapper_lines)
1722
+
1723
+ return wrapper_code, variable_list, state_declarations
1724
+
1725
+ def _convert_blade_to_template_string(self, blade_expression):
1726
+ """Convert Blade expressions like {{ asset('css/file.css') }} to template string format"""
1727
+ # Convert {{ expression }} to ${App.View.escString(App.Helper.expression)}
1728
+ def replace_blade_expression(match):
1729
+ expression = match.group(1).strip()
1730
+
1731
+ # Handle common Laravel helpers
1732
+ if expression.startswith('asset('):
1733
+ return "${App.View.escString(App.Helper." + expression + ")}"
1734
+ elif expression.startswith('route('):
1735
+ return "${App.View.escString(App.View." + expression + ")}"
1736
+ elif expression.startswith('config('):
1737
+ return "${App.View.escString(App.Helper." + expression + ")}"
1738
+ elif expression.startswith('date('):
1739
+ return "${App.View.escString(App.Helper." + expression + ")}"
1740
+ else:
1741
+ # Generic expression
1742
+ return "${App.View.escString(" + expression + ")}"
1743
+
1744
+ # Replace all {{ expression }} with template string format
1745
+ result = re.sub(r'\{\{\s*([^}]+)\s*\}\}', replace_blade_expression, blade_expression)
1746
+ return result
1747
+
1748
+ def _generate_state_updates(self, state_declarations):
1749
+ """Generate state update calls for updateVariableData function"""
1750
+ if not state_declarations:
1751
+ return ""
1752
+
1753
+ update_lines = []
1754
+ for state in state_declarations:
1755
+ state_key = state['stateKey']
1756
+ setter_name = state['setterName']
1757
+ initial_value = state['initialValue']
1758
+ # Generate: update$userState(user) - use update function instead of setter
1759
+ update_func_name = f"update${state_key}"
1760
+ update_lines.append(f"{update_func_name}({initial_value});")
1761
+
1762
+ return "\n ".join(update_lines)
1763
+