create-prisma-php-app 3.6.3 → 3.6.5
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.
|
@@ -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,154 +92,191 @@ 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
|
+
];
|
|
80
116
|
|
|
81
|
-
$
|
|
82
|
-
|
|
83
|
-
$output[] = self::processNode($child);
|
|
117
|
+
foreach ($replacements as $pattern => $replacement) {
|
|
118
|
+
$htmlContent = preg_replace($pattern, $replacement, $htmlContent, 1);
|
|
84
119
|
}
|
|
85
120
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
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
|
-
'<body$1 hidden>',
|
|
115
|
-
$htmlContent,
|
|
116
|
-
1
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
159
|
+
private static function createDomFromXml(string $xml): DOMDocument
|
|
160
|
+
{
|
|
161
|
+
$dom = new DOMDocument('1.0', 'UTF-8');
|
|
162
|
+
libxml_use_internal_errors(true);
|
|
120
163
|
|
|
121
|
-
$
|
|
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));
|
|
168
|
+
}
|
|
122
169
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
MainLayout::outputFooterScripts() . '$1',
|
|
126
|
-
$htmlContent,
|
|
127
|
-
1
|
|
128
|
-
);
|
|
170
|
+
libxml_clear_errors();
|
|
171
|
+
libxml_use_internal_errors(false);
|
|
129
172
|
|
|
130
|
-
return $
|
|
173
|
+
return $dom;
|
|
131
174
|
}
|
|
132
175
|
|
|
133
|
-
private static function
|
|
176
|
+
private static function processChildNodes($childNodes): array
|
|
134
177
|
{
|
|
135
|
-
$
|
|
136
|
-
|
|
137
|
-
|
|
178
|
+
$output = [];
|
|
179
|
+
foreach ($childNodes as $child) {
|
|
180
|
+
$output[] = self::processNode($child);
|
|
138
181
|
}
|
|
182
|
+
return $output;
|
|
183
|
+
}
|
|
139
184
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
$parts[$i] = preg_replace(
|
|
185
|
+
private static function escapeAmpersands(string $content): string
|
|
186
|
+
{
|
|
187
|
+
return self::processCDataAwareParts(
|
|
188
|
+
$content,
|
|
189
|
+
fn($part) => preg_replace(
|
|
146
190
|
'/&(?![a-zA-Z][A-Za-z0-9]*;|#[0-9]+;|#x[0-9A-Fa-f]+;)/',
|
|
147
191
|
'&',
|
|
148
192
|
$part
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
return implode('', $parts);
|
|
193
|
+
)
|
|
194
|
+
);
|
|
152
195
|
}
|
|
153
196
|
|
|
154
197
|
private static function escapeAttributeAngles(string $html): string
|
|
155
198
|
{
|
|
156
199
|
return preg_replace_callback(
|
|
157
|
-
|
|
158
|
-
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],
|
|
159
203
|
$html
|
|
160
204
|
);
|
|
161
205
|
}
|
|
162
206
|
|
|
163
|
-
private static function
|
|
207
|
+
private static function escapeLiteralTextContent(string $content): string
|
|
164
208
|
{
|
|
209
|
+
$literalTags = implode('|', array_keys(self::LITERAL_TEXT_TAGS));
|
|
210
|
+
$pattern = '/(<(?:' . $literalTags . ')\b[^>]*>)(.*?)(<\/(?:' . $literalTags . ')>)/is';
|
|
211
|
+
|
|
165
212
|
return preg_replace_callback(
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
},
|
|
168
231
|
$content
|
|
169
232
|
);
|
|
170
233
|
}
|
|
171
234
|
|
|
172
|
-
|
|
235
|
+
private static function escapeMustacheAngles(string $content): string
|
|
173
236
|
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
$content = self::escapeMustacheAngles($content);
|
|
180
|
-
|
|
181
|
-
$xml = "<root>{$content}</root>";
|
|
182
|
-
|
|
183
|
-
$dom = new DOMDocument('1.0', 'UTF-8');
|
|
184
|
-
libxml_use_internal_errors(true);
|
|
185
|
-
if (!$dom->loadXML($xml, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET)) {
|
|
186
|
-
throw new RuntimeException(
|
|
187
|
-
'XML Parsing Failed: ' . implode('; ', self::getXmlErrors())
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
libxml_clear_errors();
|
|
191
|
-
libxml_use_internal_errors(false);
|
|
192
|
-
return $dom;
|
|
237
|
+
return preg_replace_callback(
|
|
238
|
+
self::MUSTACHE_REGEX,
|
|
239
|
+
static fn($m) => str_replace(['<', '>'], ['<', '>'], $m[0]),
|
|
240
|
+
$content
|
|
241
|
+
);
|
|
193
242
|
}
|
|
194
243
|
|
|
195
244
|
private static function normalizeNamedEntities(string $html): string
|
|
196
245
|
{
|
|
197
|
-
|
|
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
|
+
}
|
|
268
|
+
|
|
269
|
+
private static function processCDataAwareParts(string $content, callable $processor): string
|
|
270
|
+
{
|
|
271
|
+
$parts = preg_split('/(<!\[CDATA\[[\s\S]*?\]\]>)/', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
|
|
198
272
|
if ($parts === false) {
|
|
199
|
-
return $
|
|
273
|
+
return $content;
|
|
200
274
|
}
|
|
201
275
|
|
|
202
276
|
foreach ($parts as $i => $part) {
|
|
203
|
-
if (str_starts_with($part, '<![CDATA[')) {
|
|
204
|
-
|
|
277
|
+
if (!str_starts_with($part, '<![CDATA[')) {
|
|
278
|
+
$parts[$i] = $processor($part);
|
|
205
279
|
}
|
|
206
|
-
|
|
207
|
-
$parts[$i] = preg_replace_callback(
|
|
208
|
-
'/&([a-zA-Z][a-zA-Z0-9]+);/',
|
|
209
|
-
static function (array $m): string {
|
|
210
|
-
$decoded = html_entity_decode($m[0], ENT_HTML5, 'UTF-8');
|
|
211
|
-
if ($decoded === $m[0]) {
|
|
212
|
-
return $m[0];
|
|
213
|
-
}
|
|
214
|
-
if (function_exists('mb_ord')) {
|
|
215
|
-
return '&#' . mb_ord($decoded, 'UTF-8') . ';';
|
|
216
|
-
}
|
|
217
|
-
$code = unpack('N', mb_convert_encoding($decoded, 'UCS-4BE', 'UTF-8'))[1];
|
|
218
|
-
return '&#' . $code . ';';
|
|
219
|
-
},
|
|
220
|
-
$part
|
|
221
|
-
);
|
|
222
280
|
}
|
|
223
281
|
|
|
224
282
|
return implode('', $parts);
|
|
@@ -226,169 +284,100 @@ class TemplateCompiler
|
|
|
226
284
|
|
|
227
285
|
private static function protectInlineScripts(string $html): string
|
|
228
286
|
{
|
|
229
|
-
if (
|
|
287
|
+
if (!str_contains($html, '<script')) {
|
|
230
288
|
return $html;
|
|
231
289
|
}
|
|
232
290
|
|
|
233
|
-
$
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (str_contains($m[2], '<![CDATA[')) {
|
|
240
|
-
return $m[0];
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
$type = '';
|
|
244
|
-
if (preg_match('/\btype\s*=\s*([\'"]?)([^\'"\s>]+)/i', $m[1], $t)) {
|
|
245
|
-
$type = strtolower($t[2]);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
$codeTypes = [
|
|
249
|
-
'',
|
|
250
|
-
'text/javascript',
|
|
251
|
-
'application/javascript',
|
|
252
|
-
'module',
|
|
253
|
-
'text/php',
|
|
254
|
-
];
|
|
255
|
-
|
|
256
|
-
if (!in_array($type, $codeTypes, true)) {
|
|
257
|
-
return $m[0];
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
$code = str_replace(']]>', ']]]]><![CDATA[>', $m[2]);
|
|
261
|
-
|
|
262
|
-
return "<script{$m[1]}><![CDATA[\n{$code}\n]]></script>";
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
$result = preg_replace_callback(
|
|
266
|
-
'#<script\b([^>]*?)>(.*?)</script>#is',
|
|
267
|
-
$callback,
|
|
268
|
-
$content
|
|
269
|
-
);
|
|
291
|
+
$callback = static function (array $m): string {
|
|
292
|
+
if (preg_match('/\bsrc\s*=/i', $m[1])) {
|
|
293
|
+
return $m[0];
|
|
294
|
+
}
|
|
270
295
|
|
|
271
|
-
if ($
|
|
272
|
-
$
|
|
273
|
-
|
|
274
|
-
$callback,
|
|
275
|
-
$content
|
|
276
|
-
);
|
|
296
|
+
if (str_contains($m[2], '<![CDATA[')) {
|
|
297
|
+
return $m[0];
|
|
298
|
+
}
|
|
277
299
|
|
|
278
|
-
|
|
300
|
+
$type = self::extractScriptType($m[1]);
|
|
301
|
+
if (!isset(self::SCRIPT_TYPES[$type])) {
|
|
302
|
+
return $m[0];
|
|
279
303
|
}
|
|
280
304
|
|
|
281
|
-
|
|
305
|
+
$code = str_replace(']]>', ']]]]><![CDATA[>', $m[2]);
|
|
306
|
+
return "<script{$m[1]}><![CDATA[\n{$code}\n]]></script>";
|
|
282
307
|
};
|
|
283
308
|
|
|
284
309
|
if (preg_match('/^(.*?<body\b[^>]*>)(.*?)(<\/body>.*)$/is', $html, $parts)) {
|
|
285
|
-
[
|
|
286
|
-
return $beforeBody .
|
|
310
|
+
[, $beforeBody, $body, $afterBody] = $parts;
|
|
311
|
+
return $beforeBody . self::processScriptsInContent($body, $callback) . $afterBody;
|
|
287
312
|
}
|
|
288
313
|
|
|
289
|
-
return
|
|
314
|
+
return self::processScriptsInContent($html, $callback);
|
|
290
315
|
}
|
|
291
316
|
|
|
292
|
-
|
|
317
|
+
private static function extractScriptType(string $attributes): string
|
|
293
318
|
{
|
|
294
|
-
if ($
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** @var DOMDocument $doc */
|
|
299
|
-
$doc = $node->ownerDocument;
|
|
300
|
-
|
|
301
|
-
$html = '';
|
|
302
|
-
foreach ($node->childNodes as $child) {
|
|
303
|
-
$html .= $doc->saveXML($child);
|
|
319
|
+
if (preg_match('/\btype\s*=\s*([\'"]?)([^\'"\s>]+)/i', $attributes, $matches)) {
|
|
320
|
+
return strtolower($matches[2]);
|
|
304
321
|
}
|
|
305
|
-
return
|
|
322
|
+
return '';
|
|
306
323
|
}
|
|
307
324
|
|
|
308
|
-
|
|
325
|
+
private static function processScriptsInContent(string $content, callable $callback): string
|
|
309
326
|
{
|
|
310
|
-
$
|
|
311
|
-
libxml_clear_errors();
|
|
312
|
-
return array_map(fn($e) => self::formatLibxmlError($e), $errors);
|
|
327
|
+
return preg_replace_callback(self::SCRIPT_REGEX, $callback, $content) ?? $content;
|
|
313
328
|
}
|
|
314
329
|
|
|
315
|
-
protected static function
|
|
330
|
+
protected static function processNode(DOMNode $node): string
|
|
316
331
|
{
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
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,
|
|
322
337
|
};
|
|
323
|
-
return sprintf(
|
|
324
|
-
"[%s] Line %d, Col %d: %s",
|
|
325
|
-
$type,
|
|
326
|
-
$error->line,
|
|
327
|
-
$error->column,
|
|
328
|
-
trim($error->message)
|
|
329
|
-
);
|
|
330
338
|
}
|
|
331
339
|
|
|
332
|
-
|
|
340
|
+
private static function processElementNode(DOMElement $node): string
|
|
333
341
|
{
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
342
|
+
$tag = strtolower($node->nodeName);
|
|
343
|
+
$pushed = false;
|
|
337
344
|
|
|
338
|
-
if ($node
|
|
339
|
-
$
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
$tag === 'script' &&
|
|
344
|
-
!$node->hasAttribute('src') &&
|
|
345
|
-
!$node->hasAttribute('type')
|
|
346
|
-
) {
|
|
347
|
-
$node->setAttribute('type', 'text/php');
|
|
348
|
-
}
|
|
345
|
+
if ($tag === 'script' && !$node->hasAttribute('src') && !$node->hasAttribute('type')) {
|
|
346
|
+
$node->setAttribute('type', 'text/php');
|
|
347
|
+
}
|
|
349
348
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
349
|
+
// Handle component sections
|
|
350
|
+
if ($node->hasAttribute('pp-component')) {
|
|
351
|
+
self::$sectionStack[] = $node->getAttribute('pp-component');
|
|
352
|
+
$pushed = true;
|
|
353
|
+
}
|
|
354
354
|
|
|
355
|
+
try {
|
|
355
356
|
self::processAttributes($node);
|
|
356
357
|
|
|
357
358
|
if (isset(self::$classMappings[$node->nodeName])) {
|
|
358
|
-
|
|
359
|
+
return self::renderComponent(
|
|
359
360
|
$node,
|
|
360
361
|
$node->nodeName,
|
|
361
362
|
self::getNodeAttributes($node)
|
|
362
363
|
);
|
|
363
|
-
if ($pushed) {
|
|
364
|
-
array_pop(self::$sectionStack);
|
|
365
|
-
}
|
|
366
|
-
return $html;
|
|
367
364
|
}
|
|
368
365
|
|
|
369
|
-
$children = '';
|
|
370
|
-
foreach ($node->childNodes as $c) {
|
|
371
|
-
$children .= self::processNode($c);
|
|
372
|
-
}
|
|
366
|
+
$children = implode('', self::processChildNodes($node->childNodes));
|
|
373
367
|
$attrs = self::getNodeAttributes($node) + ['children' => $children];
|
|
374
|
-
$out = self::renderAsHtml($node->nodeName, $attrs);
|
|
375
368
|
|
|
369
|
+
return self::renderAsHtml($node->nodeName, $attrs);
|
|
370
|
+
} finally {
|
|
376
371
|
if ($pushed) {
|
|
377
372
|
array_pop(self::$sectionStack);
|
|
378
373
|
}
|
|
379
|
-
return $out;
|
|
380
374
|
}
|
|
381
|
-
|
|
382
|
-
if ($node instanceof DOMComment) {
|
|
383
|
-
return "<!--{$node->textContent}-->";
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return $node->textContent;
|
|
387
375
|
}
|
|
388
376
|
|
|
389
377
|
private static function processTextNode(DOMText $node): string
|
|
390
378
|
{
|
|
391
379
|
$parent = strtolower($node->parentNode?->nodeName ?? '');
|
|
380
|
+
|
|
392
381
|
if (isset(self::LITERAL_TEXT_TAGS[$parent])) {
|
|
393
382
|
return htmlspecialchars(
|
|
394
383
|
$node->textContent,
|
|
@@ -399,32 +388,17 @@ class TemplateCompiler
|
|
|
399
388
|
|
|
400
389
|
return preg_replace_callback(
|
|
401
390
|
self::BINDING_REGEX,
|
|
402
|
-
fn($m) => self::processBindingExpression(trim($m[1])),
|
|
391
|
+
static fn($m) => self::processBindingExpression(trim($m[1])),
|
|
403
392
|
$node->textContent
|
|
404
393
|
);
|
|
405
394
|
}
|
|
406
395
|
|
|
407
|
-
private static function processAttributes(DOMElement $node): void
|
|
408
|
-
{
|
|
409
|
-
foreach ($node->attributes as $a) {
|
|
410
|
-
if (!preg_match(self::BINDING_REGEX, $a->value, $m)) {
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
$rawExpr = trim($m[1]);
|
|
415
|
-
$node->setAttribute("pp-bind-{$a->name}", $rawExpr);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
396
|
private static function processBindingExpression(string $expr): string
|
|
420
397
|
{
|
|
421
398
|
$escaped = htmlspecialchars($expr, ENT_QUOTES, 'UTF-8');
|
|
399
|
+
$attribute = preg_match('/^[\w.]+$/u', $expr) ? 'pp-bind' : 'pp-bind-expr';
|
|
422
400
|
|
|
423
|
-
|
|
424
|
-
return "<span pp-bind=\"{$escaped}\"></span>";
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return "<span pp-bind-expr=\"{$escaped}\"></span>";
|
|
401
|
+
return "<span {$attribute}=\"{$escaped}\"></span>";
|
|
428
402
|
}
|
|
429
403
|
|
|
430
404
|
protected static function renderComponent(
|
|
@@ -432,232 +406,284 @@ class TemplateCompiler
|
|
|
432
406
|
string $componentName,
|
|
433
407
|
array $incomingProps
|
|
434
408
|
): string {
|
|
435
|
-
$mapping
|
|
436
|
-
|
|
437
|
-
$baseId = 's' . base_convert(sprintf('%u', crc32($mapping['className'])), 10, 36);
|
|
438
|
-
$idx = self::$componentInstanceCounts[$baseId] ?? 0;
|
|
439
|
-
self::$componentInstanceCounts[$baseId] = $idx + 1;
|
|
440
|
-
$sectionId = $idx === 0 ? $baseId : "{$baseId}{$idx}";
|
|
409
|
+
$mapping = self::selectComponentMapping($componentName);
|
|
410
|
+
$sectionId = self::generateSectionId($mapping['className']);
|
|
441
411
|
|
|
442
412
|
$originalStack = self::$sectionStack;
|
|
443
413
|
self::$sectionStack[] = $sectionId;
|
|
444
414
|
|
|
445
|
-
|
|
415
|
+
try {
|
|
416
|
+
PHPX::setRenderingContext($originalStack, $sectionId);
|
|
446
417
|
|
|
447
|
-
|
|
418
|
+
$instance = self::initializeComponentInstance($mapping, $incomingProps);
|
|
419
|
+
$instance->children = self::getChildrenHtml($node);
|
|
448
420
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
421
|
+
PHPX::setRenderingContext($originalStack, $sectionId);
|
|
422
|
+
|
|
423
|
+
return self::compileComponentHtml($instance->render(), $sectionId);
|
|
424
|
+
} finally {
|
|
425
|
+
self::$sectionStack = $originalStack;
|
|
452
426
|
}
|
|
427
|
+
}
|
|
453
428
|
|
|
454
|
-
|
|
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;
|
|
455
434
|
|
|
456
|
-
$
|
|
435
|
+
return $idx === 0 ? $baseId : "{$baseId}{$idx}";
|
|
436
|
+
}
|
|
457
437
|
|
|
458
|
-
|
|
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
|
+
}
|
|
459
446
|
|
|
460
|
-
|
|
447
|
+
private static function compileComponentHtml(string $html, string $sectionId): string
|
|
448
|
+
{
|
|
461
449
|
$html = self::preprocessFragmentSyntax($html);
|
|
462
|
-
|
|
463
450
|
$fragDom = self::convertToXml($html);
|
|
464
|
-
|
|
465
|
-
foreach ($
|
|
466
|
-
if ($
|
|
467
|
-
$
|
|
451
|
+
|
|
452
|
+
foreach ($fragDom->documentElement->childNodes as $child) {
|
|
453
|
+
if ($child instanceof DOMElement) {
|
|
454
|
+
$child->setAttribute('pp-component', $sectionId);
|
|
468
455
|
break;
|
|
469
456
|
}
|
|
470
457
|
}
|
|
471
458
|
|
|
472
459
|
$htmlOut = self::innerXml($fragDom);
|
|
473
|
-
$htmlOut =
|
|
474
|
-
'/<([a-z0-9-]+)([^>]*)\/>/i',
|
|
475
|
-
fn($m) => in_array(strtolower($m[1]), self::$selfClosingTags, true)
|
|
476
|
-
? $m[0]
|
|
477
|
-
: "<{$m[1]}{$m[2]}></{$m[1]}>",
|
|
478
|
-
$htmlOut
|
|
479
|
-
);
|
|
460
|
+
$htmlOut = self::normalizeSelfClosingTags($htmlOut);
|
|
480
461
|
|
|
481
|
-
if (
|
|
482
|
-
str_contains($htmlOut, '{{') ||
|
|
483
|
-
self::hasComponentTag($htmlOut) ||
|
|
484
|
-
stripos($htmlOut, '<script') !== false
|
|
485
|
-
) {
|
|
462
|
+
if (self::needsRecompilation($htmlOut)) {
|
|
486
463
|
$htmlOut = self::compile($htmlOut);
|
|
487
464
|
}
|
|
488
465
|
|
|
489
466
|
return $htmlOut;
|
|
490
467
|
}
|
|
491
468
|
|
|
492
|
-
private static function
|
|
469
|
+
private static function needsRecompilation(string $html): bool
|
|
493
470
|
{
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return $content;
|
|
471
|
+
return str_contains($html, '{{') ||
|
|
472
|
+
preg_match(self::COMPONENT_TAG_REGEX, $html) === 1 ||
|
|
473
|
+
stripos($html, '<script') !== false;
|
|
498
474
|
}
|
|
499
475
|
|
|
500
|
-
private static function
|
|
476
|
+
private static function normalizeSelfClosingTags(string $html): string
|
|
501
477
|
{
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (isset($mappings[0]) && is_array($mappings[0])) {
|
|
511
|
-
foreach ($mappings as $entry) {
|
|
512
|
-
$imp = isset($entry['importer'])
|
|
513
|
-
? str_replace('\\', '/', $entry['importer'])
|
|
514
|
-
: '';
|
|
515
|
-
if (str_replace($srcNorm, '', $imp) === $relImp) {
|
|
516
|
-
return $entry;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return $mappings[0];
|
|
520
|
-
}
|
|
521
|
-
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
|
+
);
|
|
522
485
|
}
|
|
523
486
|
|
|
524
|
-
|
|
487
|
+
private static function initializeComponentInstance(array $mapping, array $attributes)
|
|
525
488
|
{
|
|
526
|
-
|
|
527
|
-
throw new RuntimeException("Invalid mapping");
|
|
528
|
-
}
|
|
489
|
+
['className' => $className, 'filePath' => $filePath] = $mapping;
|
|
529
490
|
|
|
530
|
-
$className
|
|
531
|
-
$
|
|
491
|
+
self::ensureClassLoaded($className, $filePath);
|
|
492
|
+
$reflection = self::getClassReflection($className);
|
|
532
493
|
|
|
533
|
-
|
|
534
|
-
if (!class_exists($className)) {
|
|
535
|
-
throw new RuntimeException("Class {$className} not found");
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
self::cacheClassReflection($className);
|
|
494
|
+
self::validateComponentProps($className, $attributes);
|
|
539
495
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
self::$reflections[$className] = $rc;
|
|
543
|
-
self::$constructors[$className] = $rc->getConstructor();
|
|
544
|
-
self::$publicProperties[$className] = array_filter(
|
|
545
|
-
$rc->getProperties(ReflectionProperty::IS_PUBLIC),
|
|
546
|
-
fn(ReflectionProperty $p) => !$p->isStatic()
|
|
547
|
-
);
|
|
548
|
-
}
|
|
496
|
+
$instance = $reflection['class']->newInstanceWithoutConstructor();
|
|
497
|
+
self::setInstanceProperties($instance, $attributes, $reflection['properties']);
|
|
549
498
|
|
|
550
|
-
|
|
499
|
+
$reflection['constructor']?->invoke($instance, $attributes);
|
|
551
500
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
$inst = $ref->newInstanceWithoutConstructor();
|
|
501
|
+
return $instance;
|
|
502
|
+
}
|
|
555
503
|
|
|
556
|
-
|
|
557
|
-
|
|
504
|
+
private static function ensureClassLoaded(string $className, string $filePath): void
|
|
505
|
+
{
|
|
506
|
+
if (!class_exists($className)) {
|
|
507
|
+
require_once str_replace('\\', '/', SRC_PATH . '/' . $filePath);
|
|
558
508
|
|
|
559
|
-
if (!
|
|
560
|
-
|
|
509
|
+
if (!class_exists($className)) {
|
|
510
|
+
throw new RuntimeException("Class {$className} not found");
|
|
561
511
|
}
|
|
562
|
-
$value = self::coerce($attributes[$name], $prop->getType());
|
|
563
|
-
$prop->setValue($inst, $value);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
if ($ctor) {
|
|
567
|
-
$ctor->invoke($inst, $attributes);
|
|
568
512
|
}
|
|
569
|
-
|
|
570
|
-
return $inst;
|
|
571
513
|
}
|
|
572
514
|
|
|
573
|
-
private static function
|
|
515
|
+
private static function getClassReflection(string $className): array
|
|
574
516
|
{
|
|
575
|
-
if (isset(self::$
|
|
576
|
-
|
|
577
|
-
|
|
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
|
+
);
|
|
578
523
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
+
}
|
|
582
533
|
|
|
583
|
-
$
|
|
584
|
-
|
|
585
|
-
fn(ReflectionProperty $p) => !$p->isStatic()
|
|
586
|
-
);
|
|
587
|
-
self::$publicProperties[$className] = $publicProps;
|
|
534
|
+
return self::$reflectionCache[$className];
|
|
535
|
+
}
|
|
588
536
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
+
}
|
|
592
548
|
}
|
|
593
|
-
self::$allowedProps[$className] = $allowed;
|
|
594
549
|
}
|
|
595
550
|
|
|
596
551
|
private static function validateComponentProps(string $className, array $attributes): void
|
|
597
552
|
{
|
|
598
|
-
|
|
553
|
+
$reflection = self::getClassReflection($className);
|
|
554
|
+
|
|
555
|
+
foreach ($reflection['properties'] as $prop) {
|
|
599
556
|
$name = $prop->getName();
|
|
600
557
|
$type = $prop->getType();
|
|
601
558
|
|
|
602
559
|
if (
|
|
603
|
-
$type instanceof ReflectionNamedType &&
|
|
604
|
-
|
|
605
|
-
|
|
560
|
+
$type instanceof ReflectionNamedType &&
|
|
561
|
+
$type->isBuiltin() &&
|
|
562
|
+
!$type->allowsNull() &&
|
|
563
|
+
!array_key_exists($name, $attributes)
|
|
606
564
|
) {
|
|
607
565
|
throw new ComponentValidationException(
|
|
608
566
|
$name,
|
|
609
567
|
$className,
|
|
610
|
-
array_map(fn($p) => $p->getName(),
|
|
568
|
+
array_map(static fn($p) => $p->getName(), $reflection['properties'])
|
|
611
569
|
);
|
|
612
570
|
}
|
|
613
571
|
}
|
|
614
|
-
|
|
615
|
-
return;
|
|
616
572
|
}
|
|
617
573
|
|
|
618
|
-
private static function
|
|
574
|
+
private static function selectComponentMapping(string $componentName): array
|
|
619
575
|
{
|
|
620
|
-
|
|
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];
|
|
621
599
|
}
|
|
622
600
|
|
|
623
|
-
|
|
601
|
+
public static function innerXml(DOMNode $node): string
|
|
624
602
|
{
|
|
625
|
-
|
|
626
|
-
|
|
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);
|
|
627
610
|
}
|
|
611
|
+
|
|
612
|
+
return $html;
|
|
628
613
|
}
|
|
629
614
|
|
|
630
|
-
|
|
615
|
+
private static function preprocessFragmentSyntax(string $content): string
|
|
616
|
+
{
|
|
617
|
+
return str_replace(['<>', '</>'], ['<Fragment>', '</Fragment>'], $content);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private static function processAttributes(DOMElement $node): void
|
|
631
621
|
{
|
|
632
|
-
|
|
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
|
+
}
|
|
633
627
|
}
|
|
634
628
|
|
|
635
629
|
private static function getNodeAttributes(DOMElement $node): array
|
|
636
630
|
{
|
|
637
|
-
$
|
|
638
|
-
foreach ($node->attributes as $
|
|
639
|
-
$
|
|
631
|
+
$attrs = [];
|
|
632
|
+
foreach ($node->attributes as $attr) {
|
|
633
|
+
$attrs[$attr->name] = $attr->value;
|
|
640
634
|
}
|
|
641
|
-
return $
|
|
635
|
+
return $attrs;
|
|
642
636
|
}
|
|
643
637
|
|
|
644
638
|
private static function renderAsHtml(string $tag, array $attrs): string
|
|
645
639
|
{
|
|
646
|
-
$
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
+
);
|
|
650
653
|
}
|
|
651
|
-
$
|
|
652
|
-
'%s="%s"',
|
|
653
|
-
$k,
|
|
654
|
-
htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
|
|
655
|
-
);
|
|
654
|
+
$attrStr = ' ' . implode(' ', $pairs);
|
|
656
655
|
}
|
|
657
|
-
$attrStr = $pairs ? ' ' . implode(' ', $pairs) : '';
|
|
658
656
|
|
|
659
|
-
return
|
|
657
|
+
return isset(self::SELF_CLOSING_TAGS[strtolower($tag)])
|
|
660
658
|
? "<{$tag}{$attrStr} />"
|
|
661
|
-
: "<{$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);
|
|
662
688
|
}
|
|
663
689
|
}
|