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