create-prisma-php-app 3.6.2 → 3.6.4
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.
|
@@ -6,29 +6,22 @@ namespace Lib\Middleware;
|
|
|
6
6
|
|
|
7
7
|
final class CorsMiddleware
|
|
8
8
|
{
|
|
9
|
-
/** Entry point */
|
|
10
9
|
public static function handle(?array $overrides = null): void
|
|
11
10
|
{
|
|
12
|
-
// Not a CORS request
|
|
13
11
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
14
12
|
if ($origin === '') {
|
|
15
13
|
return;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
// Resolve config (env → overrides)
|
|
19
16
|
$cfg = self::buildConfig($overrides);
|
|
20
17
|
|
|
21
|
-
// Not allowed? Do nothing (browser will block)
|
|
22
18
|
if (!self::isAllowedOrigin($origin, $cfg['allowedOrigins'])) {
|
|
23
19
|
return;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
// Compute which value to send for Access-Control-Allow-Origin
|
|
27
|
-
// If credentials are disabled and '*' is in list, we can send '*'
|
|
28
22
|
$sendWildcard = (!$cfg['allowCredentials'] && self::listHasWildcard($cfg['allowedOrigins']));
|
|
29
23
|
$allowOriginValue = $sendWildcard ? '*' : self::normalize($origin);
|
|
30
24
|
|
|
31
|
-
// Vary for caches
|
|
32
25
|
header('Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers');
|
|
33
26
|
|
|
34
27
|
header('Access-Control-Allow-Origin: ' . $allowOriginValue);
|
|
@@ -37,7 +30,6 @@ final class CorsMiddleware
|
|
|
37
30
|
}
|
|
38
31
|
|
|
39
32
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
40
|
-
// Preflight response
|
|
41
33
|
$requestedHeaders = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] ?? '';
|
|
42
34
|
$allowedHeaders = $cfg['allowedHeaders'] !== ''
|
|
43
35
|
? $cfg['allowedHeaders']
|
|
@@ -49,7 +41,6 @@ final class CorsMiddleware
|
|
|
49
41
|
header('Access-Control-Max-Age: ' . (string) $cfg['maxAge']);
|
|
50
42
|
}
|
|
51
43
|
|
|
52
|
-
// Optional: Private Network Access preflights (Chrome)
|
|
53
44
|
if (!empty($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK'])) {
|
|
54
45
|
header('Access-Control-Allow-Private-Network: true');
|
|
55
46
|
}
|
|
@@ -59,13 +50,11 @@ final class CorsMiddleware
|
|
|
59
50
|
exit;
|
|
60
51
|
}
|
|
61
52
|
|
|
62
|
-
// Simple/actual request
|
|
63
53
|
if ($cfg['exposeHeaders'] !== '') {
|
|
64
54
|
header('Access-Control-Expose-Headers: ' . $cfg['exposeHeaders']);
|
|
65
55
|
}
|
|
66
56
|
}
|
|
67
57
|
|
|
68
|
-
/** Read env + normalize + apply overrides */
|
|
69
58
|
private static function buildConfig(?array $overrides): array
|
|
70
59
|
{
|
|
71
60
|
$allowed = self::parseList($_ENV['CORS_ALLOWED_ORIGINS'] ?? '');
|
|
@@ -86,12 +75,10 @@ final class CorsMiddleware
|
|
|
86
75
|
}
|
|
87
76
|
}
|
|
88
77
|
|
|
89
|
-
// Normalize patterns
|
|
90
78
|
$cfg['allowedOrigins'] = array_map([self::class, 'normalize'], $cfg['allowedOrigins']);
|
|
91
79
|
return $cfg;
|
|
92
80
|
}
|
|
93
81
|
|
|
94
|
-
/** CSV or JSON array → array<string> */
|
|
95
82
|
private static function parseList(string $raw): array
|
|
96
83
|
{
|
|
97
84
|
$raw = trim($raw);
|
|
@@ -118,13 +105,10 @@ final class CorsMiddleware
|
|
|
118
105
|
foreach ($list as $pattern) {
|
|
119
106
|
$p = self::normalize($pattern);
|
|
120
107
|
|
|
121
|
-
// literal "*"
|
|
122
108
|
if ($p === '*') return true;
|
|
123
109
|
|
|
124
|
-
// allow literal "null" for file:// or sandboxed if explicitly listed
|
|
125
110
|
if ($o === 'null' && strtolower($p) === 'null') return true;
|
|
126
111
|
|
|
127
|
-
// wildcard like https://*.example.com
|
|
128
112
|
if (strpos($p, '*') !== false) {
|
|
129
113
|
$regex = '/^' . str_replace('\*', '[^.]+', preg_quote($p, '/')) . '$/i';
|
|
130
114
|
if (preg_match($regex, $o)) return true;
|
|
@@ -16,53 +16,74 @@ use Bootstrap;
|
|
|
16
16
|
use LibXMLError;
|
|
17
17
|
use ReflectionClass;
|
|
18
18
|
use ReflectionProperty;
|
|
19
|
-
use ReflectionType;
|
|
20
19
|
use ReflectionNamedType;
|
|
21
20
|
use Lib\PHPX\TypeCoercer;
|
|
22
21
|
use Lib\PHPX\Exceptions\ComponentValidationException;
|
|
23
22
|
|
|
24
23
|
class TemplateCompiler
|
|
25
24
|
{
|
|
26
|
-
|
|
25
|
+
private const BINDING_REGEX = '/\{\{\s*((?:(?!\{\{|\}\})[\s\S])*?)\s*\}\}/uS';
|
|
26
|
+
private const ATTRIBUTE_REGEX = '/(\s[\w:-]+=)([\'"])(.*?)\2/s';
|
|
27
|
+
private const MUSTACHE_REGEX = '/\{\{[\s\S]*?\}\}/u';
|
|
28
|
+
private const NAMED_ENTITY_REGEX = '/&([a-zA-Z][a-zA-Z0-9]+);/';
|
|
29
|
+
private const COMPONENT_TAG_REGEX = '/<\/*[A-Z][\w-]*/u';
|
|
30
|
+
private const SELF_CLOSING_REGEX = '/<([a-z0-9-]+)([^>]*)\/>/i';
|
|
31
|
+
private const SCRIPT_REGEX = '#<script\b([^>]*?)>(.*?)</script>#is';
|
|
32
|
+
private const HEAD_PATTERNS = [
|
|
33
|
+
'open' => '/(<head\b[^>]*>)/i',
|
|
34
|
+
'close' => '/(<\/head\s*>)/i',
|
|
35
|
+
];
|
|
36
|
+
private const BODY_PATTERNS = [
|
|
37
|
+
'open' => '/<body([^>]*)>/i',
|
|
38
|
+
'close' => '/(<\/body\s*>)/i',
|
|
39
|
+
];
|
|
40
|
+
|
|
27
41
|
private const LITERAL_TEXT_TAGS = [
|
|
28
42
|
'code' => true,
|
|
29
|
-
'pre'
|
|
43
|
+
'pre' => true,
|
|
30
44
|
'samp' => true,
|
|
31
|
-
'kbd'
|
|
32
|
-
'var'
|
|
45
|
+
'kbd' => true,
|
|
46
|
+
'var' => true,
|
|
33
47
|
];
|
|
48
|
+
|
|
34
49
|
private const SYSTEM_PROPS = [
|
|
35
50
|
'children' => true,
|
|
36
51
|
'key' => true,
|
|
37
52
|
'ref' => true,
|
|
38
53
|
];
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'wbr'
|
|
55
|
+
private const SELF_CLOSING_TAGS = [
|
|
56
|
+
'area' => true,
|
|
57
|
+
'base' => true,
|
|
58
|
+
'br' => true,
|
|
59
|
+
'col' => true,
|
|
60
|
+
'command' => true,
|
|
61
|
+
'embed' => true,
|
|
62
|
+
'hr' => true,
|
|
63
|
+
'img' => true,
|
|
64
|
+
'input' => true,
|
|
65
|
+
'keygen' => true,
|
|
66
|
+
'link' => true,
|
|
67
|
+
'meta' => true,
|
|
68
|
+
'param' => true,
|
|
69
|
+
'source' => true,
|
|
70
|
+
'track' => true,
|
|
71
|
+
'wbr' => true,
|
|
58
72
|
];
|
|
73
|
+
|
|
74
|
+
private const SCRIPT_TYPES = [
|
|
75
|
+
'' => true,
|
|
76
|
+
'text/javascript' => true,
|
|
77
|
+
'application/javascript' => true,
|
|
78
|
+
'module' => true,
|
|
79
|
+
'text/php' => true,
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
private static array $classMappings = [];
|
|
83
|
+
private static array $reflectionCache = [];
|
|
59
84
|
private static array $sectionStack = [];
|
|
60
85
|
private static int $compileDepth = 0;
|
|
61
86
|
private static array $componentInstanceCounts = [];
|
|
62
|
-
private static array $reflections = [];
|
|
63
|
-
private static array $constructors = [];
|
|
64
|
-
private static array $publicProperties = [];
|
|
65
|
-
private static array $allowedProps = [];
|
|
66
87
|
|
|
67
88
|
public static function compile(string $templateContent): string
|
|
68
89
|
{
|
|
@@ -71,302 +92,292 @@ class TemplateCompiler
|
|
|
71
92
|
}
|
|
72
93
|
self::$compileDepth++;
|
|
73
94
|
|
|
74
|
-
|
|
75
|
-
self
|
|
95
|
+
try {
|
|
96
|
+
if (empty(self::$classMappings)) {
|
|
97
|
+
self::initializeClassMappings();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
$dom = self::convertToXml($templateContent);
|
|
101
|
+
$output = self::processChildNodes($dom->documentElement->childNodes);
|
|
102
|
+
|
|
103
|
+
return implode('', $output);
|
|
104
|
+
} finally {
|
|
105
|
+
self::$compileDepth--;
|
|
76
106
|
}
|
|
107
|
+
}
|
|
77
108
|
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
public static function injectDynamicContent(string $htmlContent): string
|
|
110
|
+
{
|
|
111
|
+
$replacements = [
|
|
112
|
+
self::HEAD_PATTERNS['open'] => '$1' . MainLayout::outputMetadata(),
|
|
113
|
+
self::HEAD_PATTERNS['close'] => MainLayout::outputHeadScripts() . '$1',
|
|
114
|
+
self::BODY_PATTERNS['close'] => MainLayout::outputFooterScripts() . '$1',
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
foreach ($replacements as $pattern => $replacement) {
|
|
118
|
+
$htmlContent = preg_replace($pattern, $replacement, $htmlContent, 1);
|
|
119
|
+
}
|
|
80
120
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
121
|
+
if (
|
|
122
|
+
!isset($_SERVER['HTTP_X_PPHP_NAVIGATION']) &&
|
|
123
|
+
!PrismaPHPSettings::$option->backendOnly
|
|
124
|
+
) {
|
|
125
|
+
$htmlContent = preg_replace(
|
|
126
|
+
self::BODY_PATTERNS['open'],
|
|
127
|
+
'<body$1 hidden>',
|
|
128
|
+
$htmlContent,
|
|
129
|
+
1
|
|
130
|
+
);
|
|
84
131
|
}
|
|
85
132
|
|
|
86
|
-
|
|
87
|
-
return implode('', $output);
|
|
133
|
+
return $htmlContent;
|
|
88
134
|
}
|
|
89
135
|
|
|
90
|
-
public static function
|
|
136
|
+
public static function convertToXml(string $templateContent): DOMDocument
|
|
91
137
|
{
|
|
92
|
-
$
|
|
138
|
+
$content = self::processContentForXml($templateContent);
|
|
139
|
+
$xml = "<root>{$content}</root>";
|
|
93
140
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
'$1' . MainLayout::outputMetadata(),
|
|
97
|
-
$htmlContent,
|
|
98
|
-
1
|
|
99
|
-
);
|
|
141
|
+
return self::createDomFromXml($xml);
|
|
142
|
+
}
|
|
100
143
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
144
|
+
private static function processContentForXml(string $content): string
|
|
145
|
+
{
|
|
146
|
+
return self::escapeMustacheAngles(
|
|
147
|
+
self::escapeAttributeAngles(
|
|
148
|
+
self::escapeLiteralTextContent(
|
|
149
|
+
self::escapeAmpersands(
|
|
150
|
+
self::normalizeNamedEntities(
|
|
151
|
+
self::protectInlineScripts($content)
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
)
|
|
108
156
|
);
|
|
157
|
+
}
|
|
109
158
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
159
|
+
private static function createDomFromXml(string $xml): DOMDocument
|
|
160
|
+
{
|
|
161
|
+
$dom = new DOMDocument('1.0', 'UTF-8');
|
|
162
|
+
libxml_use_internal_errors(true);
|
|
163
|
+
|
|
164
|
+
if (!$dom->loadXML($xml, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET)) {
|
|
165
|
+
$errors = self::getXmlErrors();
|
|
166
|
+
libxml_use_internal_errors(false);
|
|
167
|
+
throw new RuntimeException('XML Parsing Failed: ' . implode('; ', $errors));
|
|
119
168
|
}
|
|
120
169
|
|
|
121
|
-
|
|
170
|
+
libxml_clear_errors();
|
|
171
|
+
libxml_use_internal_errors(false);
|
|
122
172
|
|
|
123
|
-
$
|
|
124
|
-
|
|
125
|
-
MainLayout::outputFooterScripts() . '$1',
|
|
126
|
-
$htmlContent,
|
|
127
|
-
1
|
|
128
|
-
);
|
|
173
|
+
return $dom;
|
|
174
|
+
}
|
|
129
175
|
|
|
130
|
-
|
|
176
|
+
private static function processChildNodes($childNodes): array
|
|
177
|
+
{
|
|
178
|
+
$output = [];
|
|
179
|
+
foreach ($childNodes as $child) {
|
|
180
|
+
$output[] = self::processNode($child);
|
|
181
|
+
}
|
|
182
|
+
return $output;
|
|
131
183
|
}
|
|
132
184
|
|
|
133
185
|
private static function escapeAmpersands(string $content): string
|
|
134
186
|
{
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
187
|
+
return self::processCDataAwareParts(
|
|
188
|
+
$content,
|
|
189
|
+
fn($part) => preg_replace(
|
|
190
|
+
'/&(?![a-zA-Z][A-Za-z0-9]*;|#[0-9]+;|#x[0-9A-Fa-f]+;)/',
|
|
191
|
+
'&',
|
|
192
|
+
$part
|
|
193
|
+
)
|
|
139
194
|
);
|
|
140
195
|
}
|
|
141
196
|
|
|
142
197
|
private static function escapeAttributeAngles(string $html): string
|
|
143
198
|
{
|
|
144
199
|
return preg_replace_callback(
|
|
145
|
-
|
|
146
|
-
fn($m) => $m[1] . $m[2] .
|
|
200
|
+
self::ATTRIBUTE_REGEX,
|
|
201
|
+
static fn($m) => $m[1] . $m[2] .
|
|
202
|
+
str_replace(['<', '>'], ['<', '>'], $m[3]) . $m[2],
|
|
147
203
|
$html
|
|
148
204
|
);
|
|
149
205
|
}
|
|
150
206
|
|
|
151
|
-
private static function
|
|
207
|
+
private static function escapeLiteralTextContent(string $content): string
|
|
152
208
|
{
|
|
209
|
+
$literalTags = implode('|', array_keys(self::LITERAL_TEXT_TAGS));
|
|
210
|
+
$pattern = '/(<(?:' . $literalTags . ')\b[^>]*>)(.*?)(<\/(?:' . $literalTags . ')>)/is';
|
|
211
|
+
|
|
153
212
|
return preg_replace_callback(
|
|
154
|
-
|
|
155
|
-
|
|
213
|
+
$pattern,
|
|
214
|
+
static function ($matches) {
|
|
215
|
+
$openTag = $matches[1];
|
|
216
|
+
$textContent = $matches[2];
|
|
217
|
+
$closeTag = $matches[3];
|
|
218
|
+
|
|
219
|
+
$escapedContent = preg_replace_callback(
|
|
220
|
+
'/(\s|^|,)([<>]=?)(\s|$|,)/',
|
|
221
|
+
function ($match) {
|
|
222
|
+
$operator = $match[2];
|
|
223
|
+
$escapedOp = str_replace(['<', '>'], ['<', '>'], $operator);
|
|
224
|
+
return $match[1] . $escapedOp . $match[3];
|
|
225
|
+
},
|
|
226
|
+
$textContent
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return $openTag . $escapedContent . $closeTag;
|
|
230
|
+
},
|
|
156
231
|
$content
|
|
157
232
|
);
|
|
158
233
|
}
|
|
159
234
|
|
|
160
|
-
|
|
235
|
+
private static function escapeMustacheAngles(string $content): string
|
|
161
236
|
{
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
$content = self::escapeMustacheAngles($content);
|
|
168
|
-
|
|
169
|
-
$xml = "<root>{$content}</root>";
|
|
170
|
-
|
|
171
|
-
$dom = new DOMDocument('1.0', 'UTF-8');
|
|
172
|
-
libxml_use_internal_errors(true);
|
|
173
|
-
if (!$dom->loadXML($xml, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET)) {
|
|
174
|
-
throw new RuntimeException(
|
|
175
|
-
'XML Parsing Failed: ' . implode('; ', self::getXmlErrors())
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
libxml_clear_errors();
|
|
179
|
-
libxml_use_internal_errors(false);
|
|
180
|
-
return $dom;
|
|
237
|
+
return preg_replace_callback(
|
|
238
|
+
self::MUSTACHE_REGEX,
|
|
239
|
+
static fn($m) => str_replace(['<', '>'], ['<', '>'], $m[0]),
|
|
240
|
+
$content
|
|
241
|
+
);
|
|
181
242
|
}
|
|
182
243
|
|
|
183
244
|
private static function normalizeNamedEntities(string $html): string
|
|
184
245
|
{
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
static function (
|
|
188
|
-
|
|
246
|
+
return self::processCDataAwareParts(
|
|
247
|
+
$html,
|
|
248
|
+
static function (string $part): string {
|
|
249
|
+
return preg_replace_callback(
|
|
250
|
+
self::NAMED_ENTITY_REGEX,
|
|
251
|
+
static function (array $m): string {
|
|
252
|
+
$decoded = html_entity_decode($m[0], ENT_HTML5, 'UTF-8');
|
|
253
|
+
if ($decoded === $m[0]) {
|
|
254
|
+
return $m[0];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
$code = function_exists('mb_ord')
|
|
258
|
+
? mb_ord($decoded, 'UTF-8')
|
|
259
|
+
: unpack('N', mb_convert_encoding($decoded, 'UCS-4BE', 'UTF-8'))[1];
|
|
260
|
+
|
|
261
|
+
return '&#' . $code . ';';
|
|
262
|
+
},
|
|
263
|
+
$part
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
189
268
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
269
|
+
private static function processCDataAwareParts(string $content, callable $processor): string
|
|
270
|
+
{
|
|
271
|
+
$parts = preg_split('/(<!\[CDATA\[[\s\S]*?\]\]>)/', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
|
|
272
|
+
if ($parts === false) {
|
|
273
|
+
return $content;
|
|
274
|
+
}
|
|
193
275
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
276
|
+
foreach ($parts as $i => $part) {
|
|
277
|
+
if (!str_starts_with($part, '<![CDATA[')) {
|
|
278
|
+
$parts[$i] = $processor($part);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
197
281
|
|
|
198
|
-
|
|
199
|
-
return '&#' . $code . ';';
|
|
200
|
-
},
|
|
201
|
-
$html
|
|
202
|
-
);
|
|
282
|
+
return implode('', $parts);
|
|
203
283
|
}
|
|
204
284
|
|
|
205
285
|
private static function protectInlineScripts(string $html): string
|
|
206
286
|
{
|
|
207
|
-
if (
|
|
287
|
+
if (!str_contains($html, '<script')) {
|
|
208
288
|
return $html;
|
|
209
289
|
}
|
|
210
290
|
|
|
211
|
-
$
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (str_contains($m[2], '<![CDATA[')) {
|
|
218
|
-
return $m[0];
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
$type = '';
|
|
222
|
-
if (preg_match('/\btype\s*=\s*([\'"]?)([^\'"\s>]+)/i', $m[1], $t)) {
|
|
223
|
-
$type = strtolower($t[2]);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
$codeTypes = [
|
|
227
|
-
'',
|
|
228
|
-
'text/javascript',
|
|
229
|
-
'application/javascript',
|
|
230
|
-
'module',
|
|
231
|
-
'text/php',
|
|
232
|
-
];
|
|
233
|
-
|
|
234
|
-
if (!in_array($type, $codeTypes, true)) {
|
|
235
|
-
return $m[0];
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
$code = str_replace(']]>', ']]]]><![CDATA[>', $m[2]);
|
|
239
|
-
|
|
240
|
-
return "<script{$m[1]}><![CDATA[\n{$code}\n]]></script>";
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
$result = preg_replace_callback(
|
|
244
|
-
'#<script\b([^>]*?)>(.*?)</script>#is',
|
|
245
|
-
$callback,
|
|
246
|
-
$content
|
|
247
|
-
);
|
|
291
|
+
$callback = static function (array $m): string {
|
|
292
|
+
if (preg_match('/\bsrc\s*=/i', $m[1])) {
|
|
293
|
+
return $m[0];
|
|
294
|
+
}
|
|
248
295
|
|
|
249
|
-
if ($
|
|
250
|
-
$
|
|
251
|
-
|
|
252
|
-
$callback,
|
|
253
|
-
$content
|
|
254
|
-
);
|
|
296
|
+
if (str_contains($m[2], '<![CDATA[')) {
|
|
297
|
+
return $m[0];
|
|
298
|
+
}
|
|
255
299
|
|
|
256
|
-
|
|
300
|
+
$type = self::extractScriptType($m[1]);
|
|
301
|
+
if (!isset(self::SCRIPT_TYPES[$type])) {
|
|
302
|
+
return $m[0];
|
|
257
303
|
}
|
|
258
304
|
|
|
259
|
-
|
|
305
|
+
$code = str_replace(']]>', ']]]]><![CDATA[>', $m[2]);
|
|
306
|
+
return "<script{$m[1]}><![CDATA[\n{$code}\n]]></script>";
|
|
260
307
|
};
|
|
261
308
|
|
|
262
309
|
if (preg_match('/^(.*?<body\b[^>]*>)(.*?)(<\/body>.*)$/is', $html, $parts)) {
|
|
263
|
-
[
|
|
264
|
-
return $beforeBody .
|
|
310
|
+
[, $beforeBody, $body, $afterBody] = $parts;
|
|
311
|
+
return $beforeBody . self::processScriptsInContent($body, $callback) . $afterBody;
|
|
265
312
|
}
|
|
266
313
|
|
|
267
|
-
return
|
|
314
|
+
return self::processScriptsInContent($html, $callback);
|
|
268
315
|
}
|
|
269
316
|
|
|
270
|
-
|
|
317
|
+
private static function extractScriptType(string $attributes): string
|
|
271
318
|
{
|
|
272
|
-
if ($
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** @var DOMDocument $doc */
|
|
277
|
-
$doc = $node->ownerDocument;
|
|
278
|
-
|
|
279
|
-
$html = '';
|
|
280
|
-
foreach ($node->childNodes as $child) {
|
|
281
|
-
$html .= $doc->saveXML($child);
|
|
319
|
+
if (preg_match('/\btype\s*=\s*([\'"]?)([^\'"\s>]+)/i', $attributes, $matches)) {
|
|
320
|
+
return strtolower($matches[2]);
|
|
282
321
|
}
|
|
283
|
-
return
|
|
322
|
+
return '';
|
|
284
323
|
}
|
|
285
324
|
|
|
286
|
-
|
|
325
|
+
private static function processScriptsInContent(string $content, callable $callback): string
|
|
287
326
|
{
|
|
288
|
-
$
|
|
289
|
-
libxml_clear_errors();
|
|
290
|
-
return array_map(fn($e) => self::formatLibxmlError($e), $errors);
|
|
327
|
+
return preg_replace_callback(self::SCRIPT_REGEX, $callback, $content) ?? $content;
|
|
291
328
|
}
|
|
292
329
|
|
|
293
|
-
protected static function
|
|
330
|
+
protected static function processNode(DOMNode $node): string
|
|
294
331
|
{
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
default
|
|
332
|
+
return match (true) {
|
|
333
|
+
$node instanceof DOMText => self::processTextNode($node),
|
|
334
|
+
$node instanceof DOMElement => self::processElementNode($node),
|
|
335
|
+
$node instanceof DOMComment => "<!--{$node->textContent}-->",
|
|
336
|
+
default => $node->textContent,
|
|
300
337
|
};
|
|
301
|
-
return sprintf(
|
|
302
|
-
"[%s] Line %d, Col %d: %s",
|
|
303
|
-
$type,
|
|
304
|
-
$error->line,
|
|
305
|
-
$error->column,
|
|
306
|
-
trim($error->message)
|
|
307
|
-
);
|
|
308
338
|
}
|
|
309
339
|
|
|
310
|
-
|
|
340
|
+
private static function processElementNode(DOMElement $node): string
|
|
311
341
|
{
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if ($node instanceof DOMElement) {
|
|
317
|
-
$pushed = false;
|
|
318
|
-
$tag = strtolower($node->nodeName);
|
|
342
|
+
$tag = strtolower($node->nodeName);
|
|
343
|
+
$pushed = false;
|
|
319
344
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
!$node->hasAttribute('type')
|
|
324
|
-
) {
|
|
325
|
-
$node->setAttribute('type', 'text/php');
|
|
326
|
-
}
|
|
345
|
+
if ($tag === 'script' && !$node->hasAttribute('src') && !$node->hasAttribute('type')) {
|
|
346
|
+
$node->setAttribute('type', 'text/php');
|
|
347
|
+
}
|
|
327
348
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
349
|
+
// Handle component sections
|
|
350
|
+
if ($node->hasAttribute('pp-component')) {
|
|
351
|
+
self::$sectionStack[] = $node->getAttribute('pp-component');
|
|
352
|
+
$pushed = true;
|
|
353
|
+
}
|
|
332
354
|
|
|
355
|
+
try {
|
|
333
356
|
self::processAttributes($node);
|
|
334
357
|
|
|
335
358
|
if (isset(self::$classMappings[$node->nodeName])) {
|
|
336
|
-
|
|
359
|
+
return self::renderComponent(
|
|
337
360
|
$node,
|
|
338
361
|
$node->nodeName,
|
|
339
362
|
self::getNodeAttributes($node)
|
|
340
363
|
);
|
|
341
|
-
if ($pushed) {
|
|
342
|
-
array_pop(self::$sectionStack);
|
|
343
|
-
}
|
|
344
|
-
return $html;
|
|
345
364
|
}
|
|
346
365
|
|
|
347
|
-
$children = '';
|
|
348
|
-
foreach ($node->childNodes as $c) {
|
|
349
|
-
$children .= self::processNode($c);
|
|
350
|
-
}
|
|
366
|
+
$children = implode('', self::processChildNodes($node->childNodes));
|
|
351
367
|
$attrs = self::getNodeAttributes($node) + ['children' => $children];
|
|
352
|
-
$out = self::renderAsHtml($node->nodeName, $attrs);
|
|
353
368
|
|
|
369
|
+
return self::renderAsHtml($node->nodeName, $attrs);
|
|
370
|
+
} finally {
|
|
354
371
|
if ($pushed) {
|
|
355
372
|
array_pop(self::$sectionStack);
|
|
356
373
|
}
|
|
357
|
-
return $out;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if ($node instanceof DOMComment) {
|
|
361
|
-
return "<!--{$node->textContent}-->";
|
|
362
374
|
}
|
|
363
|
-
|
|
364
|
-
return $node->textContent;
|
|
365
375
|
}
|
|
366
376
|
|
|
367
377
|
private static function processTextNode(DOMText $node): string
|
|
368
378
|
{
|
|
369
379
|
$parent = strtolower($node->parentNode?->nodeName ?? '');
|
|
380
|
+
|
|
370
381
|
if (isset(self::LITERAL_TEXT_TAGS[$parent])) {
|
|
371
382
|
return htmlspecialchars(
|
|
372
383
|
$node->textContent,
|
|
@@ -377,32 +388,17 @@ class TemplateCompiler
|
|
|
377
388
|
|
|
378
389
|
return preg_replace_callback(
|
|
379
390
|
self::BINDING_REGEX,
|
|
380
|
-
fn($m) => self::processBindingExpression(trim($m[1])),
|
|
391
|
+
static fn($m) => self::processBindingExpression(trim($m[1])),
|
|
381
392
|
$node->textContent
|
|
382
393
|
);
|
|
383
394
|
}
|
|
384
395
|
|
|
385
|
-
private static function processAttributes(DOMElement $node): void
|
|
386
|
-
{
|
|
387
|
-
foreach ($node->attributes as $a) {
|
|
388
|
-
if (!preg_match(self::BINDING_REGEX, $a->value, $m)) {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
$rawExpr = trim($m[1]);
|
|
393
|
-
$node->setAttribute("pp-bind-{$a->name}", $rawExpr);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
396
|
private static function processBindingExpression(string $expr): string
|
|
398
397
|
{
|
|
399
398
|
$escaped = htmlspecialchars($expr, ENT_QUOTES, 'UTF-8');
|
|
399
|
+
$attribute = preg_match('/^[\w.]+$/u', $expr) ? 'pp-bind' : 'pp-bind-expr';
|
|
400
400
|
|
|
401
|
-
|
|
402
|
-
return "<span pp-bind=\"{$escaped}\"></span>";
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return "<span pp-bind-expr=\"{$escaped}\"></span>";
|
|
401
|
+
return "<span {$attribute}=\"{$escaped}\"></span>";
|
|
406
402
|
}
|
|
407
403
|
|
|
408
404
|
protected static function renderComponent(
|
|
@@ -410,232 +406,284 @@ class TemplateCompiler
|
|
|
410
406
|
string $componentName,
|
|
411
407
|
array $incomingProps
|
|
412
408
|
): string {
|
|
413
|
-
$mapping
|
|
414
|
-
|
|
415
|
-
$baseId = 's' . base_convert(sprintf('%u', crc32($mapping['className'])), 10, 36);
|
|
416
|
-
$idx = self::$componentInstanceCounts[$baseId] ?? 0;
|
|
417
|
-
self::$componentInstanceCounts[$baseId] = $idx + 1;
|
|
418
|
-
$sectionId = $idx === 0 ? $baseId : "{$baseId}{$idx}";
|
|
409
|
+
$mapping = self::selectComponentMapping($componentName);
|
|
410
|
+
$sectionId = self::generateSectionId($mapping['className']);
|
|
419
411
|
|
|
420
412
|
$originalStack = self::$sectionStack;
|
|
421
413
|
self::$sectionStack[] = $sectionId;
|
|
422
414
|
|
|
423
|
-
|
|
415
|
+
try {
|
|
416
|
+
PHPX::setRenderingContext($originalStack, $sectionId);
|
|
417
|
+
|
|
418
|
+
$instance = self::initializeComponentInstance($mapping, $incomingProps);
|
|
419
|
+
$instance->children = self::getChildrenHtml($node);
|
|
424
420
|
|
|
425
|
-
|
|
421
|
+
PHPX::setRenderingContext($originalStack, $sectionId);
|
|
426
422
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
423
|
+
return self::compileComponentHtml($instance->render(), $sectionId);
|
|
424
|
+
} finally {
|
|
425
|
+
self::$sectionStack = $originalStack;
|
|
430
426
|
}
|
|
427
|
+
}
|
|
431
428
|
|
|
432
|
-
|
|
429
|
+
private static function generateSectionId(string $className): string
|
|
430
|
+
{
|
|
431
|
+
$baseId = 's' . base_convert(sprintf('%u', crc32($className)), 10, 36);
|
|
432
|
+
$idx = self::$componentInstanceCounts[$baseId] ?? 0;
|
|
433
|
+
self::$componentInstanceCounts[$baseId] = $idx + 1;
|
|
433
434
|
|
|
434
|
-
$
|
|
435
|
+
return $idx === 0 ? $baseId : "{$baseId}{$idx}";
|
|
436
|
+
}
|
|
435
437
|
|
|
436
|
-
|
|
438
|
+
private static function getChildrenHtml(DOMElement $node): string
|
|
439
|
+
{
|
|
440
|
+
$output = [];
|
|
441
|
+
foreach ($node->childNodes as $child) {
|
|
442
|
+
$output[] = self::processNode($child);
|
|
443
|
+
}
|
|
444
|
+
return trim(implode('', $output));
|
|
445
|
+
}
|
|
437
446
|
|
|
438
|
-
|
|
447
|
+
private static function compileComponentHtml(string $html, string $sectionId): string
|
|
448
|
+
{
|
|
439
449
|
$html = self::preprocessFragmentSyntax($html);
|
|
440
|
-
|
|
441
450
|
$fragDom = self::convertToXml($html);
|
|
442
|
-
|
|
443
|
-
foreach ($
|
|
444
|
-
if ($
|
|
445
|
-
$
|
|
451
|
+
|
|
452
|
+
foreach ($fragDom->documentElement->childNodes as $child) {
|
|
453
|
+
if ($child instanceof DOMElement) {
|
|
454
|
+
$child->setAttribute('pp-component', $sectionId);
|
|
446
455
|
break;
|
|
447
456
|
}
|
|
448
457
|
}
|
|
449
458
|
|
|
450
459
|
$htmlOut = self::innerXml($fragDom);
|
|
451
|
-
$htmlOut =
|
|
452
|
-
'/<([a-z0-9-]+)([^>]*)\/>/i',
|
|
453
|
-
fn($m) => in_array(strtolower($m[1]), self::$selfClosingTags, true)
|
|
454
|
-
? $m[0]
|
|
455
|
-
: "<{$m[1]}{$m[2]}></{$m[1]}>",
|
|
456
|
-
$htmlOut
|
|
457
|
-
);
|
|
460
|
+
$htmlOut = self::normalizeSelfClosingTags($htmlOut);
|
|
458
461
|
|
|
459
|
-
if (
|
|
460
|
-
str_contains($htmlOut, '{{') ||
|
|
461
|
-
self::hasComponentTag($htmlOut) ||
|
|
462
|
-
stripos($htmlOut, '<script') !== false
|
|
463
|
-
) {
|
|
462
|
+
if (self::needsRecompilation($htmlOut)) {
|
|
464
463
|
$htmlOut = self::compile($htmlOut);
|
|
465
464
|
}
|
|
466
465
|
|
|
467
466
|
return $htmlOut;
|
|
468
467
|
}
|
|
469
468
|
|
|
470
|
-
private static function
|
|
469
|
+
private static function needsRecompilation(string $html): bool
|
|
471
470
|
{
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return $content;
|
|
471
|
+
return str_contains($html, '{{') ||
|
|
472
|
+
preg_match(self::COMPONENT_TAG_REGEX, $html) === 1 ||
|
|
473
|
+
stripos($html, '<script') !== false;
|
|
476
474
|
}
|
|
477
475
|
|
|
478
|
-
private static function
|
|
476
|
+
private static function normalizeSelfClosingTags(string $html): string
|
|
479
477
|
{
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (isset($mappings[0]) && is_array($mappings[0])) {
|
|
489
|
-
foreach ($mappings as $entry) {
|
|
490
|
-
$imp = isset($entry['importer'])
|
|
491
|
-
? str_replace('\\', '/', $entry['importer'])
|
|
492
|
-
: '';
|
|
493
|
-
if (str_replace($srcNorm, '', $imp) === $relImp) {
|
|
494
|
-
return $entry;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return $mappings[0];
|
|
498
|
-
}
|
|
499
|
-
return $mappings;
|
|
478
|
+
return preg_replace_callback(
|
|
479
|
+
self::SELF_CLOSING_REGEX,
|
|
480
|
+
static fn($m) => isset(self::SELF_CLOSING_TAGS[strtolower($m[1])])
|
|
481
|
+
? $m[0]
|
|
482
|
+
: "<{$m[1]}{$m[2]}></{$m[1]}>",
|
|
483
|
+
$html
|
|
484
|
+
);
|
|
500
485
|
}
|
|
501
486
|
|
|
502
|
-
|
|
487
|
+
private static function initializeComponentInstance(array $mapping, array $attributes)
|
|
503
488
|
{
|
|
504
|
-
|
|
505
|
-
throw new RuntimeException("Invalid mapping");
|
|
506
|
-
}
|
|
489
|
+
['className' => $className, 'filePath' => $filePath] = $mapping;
|
|
507
490
|
|
|
508
|
-
$className
|
|
509
|
-
$
|
|
510
|
-
|
|
511
|
-
require_once str_replace('\\', '/', SRC_PATH . '/' . $filePath);
|
|
512
|
-
if (!class_exists($className)) {
|
|
513
|
-
throw new RuntimeException("Class {$className} not found");
|
|
514
|
-
}
|
|
491
|
+
self::ensureClassLoaded($className, $filePath);
|
|
492
|
+
$reflection = self::getClassReflection($className);
|
|
515
493
|
|
|
516
|
-
self::
|
|
494
|
+
self::validateComponentProps($className, $attributes);
|
|
517
495
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
self::$reflections[$className] = $rc;
|
|
521
|
-
self::$constructors[$className] = $rc->getConstructor();
|
|
522
|
-
self::$publicProperties[$className] = array_filter(
|
|
523
|
-
$rc->getProperties(ReflectionProperty::IS_PUBLIC),
|
|
524
|
-
fn(ReflectionProperty $p) => !$p->isStatic()
|
|
525
|
-
);
|
|
526
|
-
}
|
|
496
|
+
$instance = $reflection['class']->newInstanceWithoutConstructor();
|
|
497
|
+
self::setInstanceProperties($instance, $attributes, $reflection['properties']);
|
|
527
498
|
|
|
528
|
-
|
|
499
|
+
$reflection['constructor']?->invoke($instance, $attributes);
|
|
529
500
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
$inst = $ref->newInstanceWithoutConstructor();
|
|
501
|
+
return $instance;
|
|
502
|
+
}
|
|
533
503
|
|
|
534
|
-
|
|
535
|
-
|
|
504
|
+
private static function ensureClassLoaded(string $className, string $filePath): void
|
|
505
|
+
{
|
|
506
|
+
if (!class_exists($className)) {
|
|
507
|
+
require_once str_replace('\\', '/', SRC_PATH . '/' . $filePath);
|
|
536
508
|
|
|
537
|
-
if (!
|
|
538
|
-
|
|
509
|
+
if (!class_exists($className)) {
|
|
510
|
+
throw new RuntimeException("Class {$className} not found");
|
|
539
511
|
}
|
|
540
|
-
$value = self::coerce($attributes[$name], $prop->getType());
|
|
541
|
-
$prop->setValue($inst, $value);
|
|
542
512
|
}
|
|
543
|
-
|
|
544
|
-
if ($ctor) {
|
|
545
|
-
$ctor->invoke($inst, $attributes);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return $inst;
|
|
549
513
|
}
|
|
550
514
|
|
|
551
|
-
private static function
|
|
515
|
+
private static function getClassReflection(string $className): array
|
|
552
516
|
{
|
|
553
|
-
if (isset(self::$
|
|
554
|
-
|
|
555
|
-
|
|
517
|
+
if (!isset(self::$reflectionCache[$className])) {
|
|
518
|
+
$rc = new ReflectionClass($className);
|
|
519
|
+
$publicProps = array_filter(
|
|
520
|
+
$rc->getProperties(ReflectionProperty::IS_PUBLIC),
|
|
521
|
+
static fn(ReflectionProperty $p) => !$p->isStatic()
|
|
522
|
+
);
|
|
556
523
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
524
|
+
self::$reflectionCache[$className] = [
|
|
525
|
+
'class' => $rc,
|
|
526
|
+
'constructor' => $rc->getConstructor(),
|
|
527
|
+
'properties' => $publicProps,
|
|
528
|
+
'allowedProps' => self::SYSTEM_PROPS + array_flip(
|
|
529
|
+
array_map(static fn($p) => $p->getName(), $publicProps)
|
|
530
|
+
),
|
|
531
|
+
];
|
|
532
|
+
}
|
|
560
533
|
|
|
561
|
-
$
|
|
562
|
-
|
|
563
|
-
fn(ReflectionProperty $p) => !$p->isStatic()
|
|
564
|
-
);
|
|
565
|
-
self::$publicProperties[$className] = $publicProps;
|
|
534
|
+
return self::$reflectionCache[$className];
|
|
535
|
+
}
|
|
566
536
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
537
|
+
private static function setInstanceProperties(
|
|
538
|
+
object $instance,
|
|
539
|
+
array $attributes,
|
|
540
|
+
array $properties
|
|
541
|
+
): void {
|
|
542
|
+
foreach ($properties as $prop) {
|
|
543
|
+
$name = $prop->getName();
|
|
544
|
+
if (array_key_exists($name, $attributes)) {
|
|
545
|
+
$value = TypeCoercer::coerce($attributes[$name], $prop->getType());
|
|
546
|
+
$prop->setValue($instance, $value);
|
|
547
|
+
}
|
|
570
548
|
}
|
|
571
|
-
self::$allowedProps[$className] = $allowed;
|
|
572
549
|
}
|
|
573
550
|
|
|
574
551
|
private static function validateComponentProps(string $className, array $attributes): void
|
|
575
552
|
{
|
|
576
|
-
|
|
553
|
+
$reflection = self::getClassReflection($className);
|
|
554
|
+
|
|
555
|
+
foreach ($reflection['properties'] as $prop) {
|
|
577
556
|
$name = $prop->getName();
|
|
578
557
|
$type = $prop->getType();
|
|
579
558
|
|
|
580
559
|
if (
|
|
581
|
-
$type instanceof ReflectionNamedType &&
|
|
582
|
-
|
|
583
|
-
|
|
560
|
+
$type instanceof ReflectionNamedType &&
|
|
561
|
+
$type->isBuiltin() &&
|
|
562
|
+
!$type->allowsNull() &&
|
|
563
|
+
!array_key_exists($name, $attributes)
|
|
584
564
|
) {
|
|
585
565
|
throw new ComponentValidationException(
|
|
586
566
|
$name,
|
|
587
567
|
$className,
|
|
588
|
-
array_map(fn($p) => $p->getName(),
|
|
568
|
+
array_map(static fn($p) => $p->getName(), $reflection['properties'])
|
|
589
569
|
);
|
|
590
570
|
}
|
|
591
571
|
}
|
|
592
|
-
|
|
593
|
-
return;
|
|
594
572
|
}
|
|
595
573
|
|
|
596
|
-
private static function
|
|
574
|
+
private static function selectComponentMapping(string $componentName): array
|
|
597
575
|
{
|
|
598
|
-
|
|
576
|
+
if (!isset(self::$classMappings[$componentName])) {
|
|
577
|
+
throw new RuntimeException("Component {$componentName} not registered");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
$mappings = self::$classMappings[$componentName];
|
|
581
|
+
|
|
582
|
+
if (!isset($mappings[0]) || !is_array($mappings[0])) {
|
|
583
|
+
return $mappings;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
$srcNorm = str_replace('\\', '/', SRC_PATH) . '/';
|
|
587
|
+
$relImp = str_replace($srcNorm, '', str_replace('\\', '/', Bootstrap::$contentToInclude));
|
|
588
|
+
|
|
589
|
+
foreach ($mappings as $entry) {
|
|
590
|
+
if (isset($entry['importer'])) {
|
|
591
|
+
$imp = str_replace([$srcNorm, '\\'], ['', '/'], $entry['importer']);
|
|
592
|
+
if ($imp === $relImp) {
|
|
593
|
+
return $entry;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return $mappings[0];
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
|
|
601
|
+
public static function innerXml(DOMNode $node): string
|
|
602
602
|
{
|
|
603
|
-
|
|
604
|
-
|
|
603
|
+
if ($node instanceof DOMDocument) {
|
|
604
|
+
$node = $node->documentElement;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
$html = '';
|
|
608
|
+
foreach ($node->childNodes as $child) {
|
|
609
|
+
$html .= $node->ownerDocument->saveXML($child);
|
|
605
610
|
}
|
|
611
|
+
|
|
612
|
+
return $html;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private static function preprocessFragmentSyntax(string $content): string
|
|
616
|
+
{
|
|
617
|
+
return str_replace(['<>', '</>'], ['<Fragment>', '</Fragment>'], $content);
|
|
606
618
|
}
|
|
607
619
|
|
|
608
|
-
|
|
620
|
+
private static function processAttributes(DOMElement $node): void
|
|
609
621
|
{
|
|
610
|
-
|
|
622
|
+
foreach ($node->attributes as $attr) {
|
|
623
|
+
if (preg_match(self::BINDING_REGEX, $attr->value, $matches)) {
|
|
624
|
+
$node->setAttribute("pp-bind-{$attr->name}", trim($matches[1]));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
611
627
|
}
|
|
612
628
|
|
|
613
629
|
private static function getNodeAttributes(DOMElement $node): array
|
|
614
630
|
{
|
|
615
|
-
$
|
|
616
|
-
foreach ($node->attributes as $
|
|
617
|
-
$
|
|
631
|
+
$attrs = [];
|
|
632
|
+
foreach ($node->attributes as $attr) {
|
|
633
|
+
$attrs[$attr->name] = $attr->value;
|
|
618
634
|
}
|
|
619
|
-
return $
|
|
635
|
+
return $attrs;
|
|
620
636
|
}
|
|
621
637
|
|
|
622
638
|
private static function renderAsHtml(string $tag, array $attrs): string
|
|
623
639
|
{
|
|
624
|
-
$
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
640
|
+
$children = $attrs['children'] ?? '';
|
|
641
|
+
unset($attrs['children']);
|
|
642
|
+
|
|
643
|
+
if (empty($attrs)) {
|
|
644
|
+
$attrStr = '';
|
|
645
|
+
} else {
|
|
646
|
+
$pairs = [];
|
|
647
|
+
foreach ($attrs as $name => $value) {
|
|
648
|
+
$pairs[] = sprintf(
|
|
649
|
+
'%s="%s"',
|
|
650
|
+
$name,
|
|
651
|
+
htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
|
652
|
+
);
|
|
628
653
|
}
|
|
629
|
-
$
|
|
630
|
-
'%s="%s"',
|
|
631
|
-
$k,
|
|
632
|
-
htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
|
633
|
-
);
|
|
654
|
+
$attrStr = ' ' . implode(' ', $pairs);
|
|
634
655
|
}
|
|
635
|
-
$attrStr = $pairs ? ' ' . implode(' ', $pairs) : '';
|
|
636
656
|
|
|
637
|
-
return
|
|
657
|
+
return isset(self::SELF_CLOSING_TAGS[strtolower($tag)])
|
|
638
658
|
? "<{$tag}{$attrStr} />"
|
|
639
|
-
: "<{$tag}{$attrStr}>{$
|
|
659
|
+
: "<{$tag}{$attrStr}>{$children}</{$tag}>";
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
protected static function initializeClassMappings(): void
|
|
663
|
+
{
|
|
664
|
+
self::$classMappings = PrismaPHPSettings::$classLogFiles;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
protected static function getXmlErrors(): array
|
|
668
|
+
{
|
|
669
|
+
$errors = libxml_get_errors();
|
|
670
|
+
libxml_clear_errors();
|
|
671
|
+
|
|
672
|
+
return array_map(static function (LibXMLError $error): string {
|
|
673
|
+
$type = match ($error->level) {
|
|
674
|
+
LIBXML_ERR_WARNING => 'Warning',
|
|
675
|
+
LIBXML_ERR_ERROR => 'Error',
|
|
676
|
+
LIBXML_ERR_FATAL => 'Fatal',
|
|
677
|
+
default => 'Unknown',
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
return sprintf(
|
|
681
|
+
"[%s] Line %d, Col %d: %s",
|
|
682
|
+
$type,
|
|
683
|
+
$error->line,
|
|
684
|
+
$error->column,
|
|
685
|
+
trim($error->message)
|
|
686
|
+
);
|
|
687
|
+
}, $errors);
|
|
640
688
|
}
|
|
641
689
|
}
|