create-prisma-php-app 4.0.0-alpha.19 → 4.0.0-alpha.20
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/dist/.htaccess +54 -41
- package/dist/bootstrap.php +133 -88
- package/dist/index.js +167 -72
- package/dist/settings/auto-swagger-docs.ts +196 -95
- package/dist/settings/bs-config.ts +56 -58
- package/dist/settings/restart-mcp.ts +58 -0
- package/dist/settings/restart-websocket.ts +44 -45
- package/dist/settings/utils.ts +240 -0
- package/dist/src/Lib/Headers/Boom.php +152 -168
- package/dist/src/Lib/IncludeTracker.php +1 -1
- package/dist/src/Lib/MCP/WeatherTools.php +104 -0
- package/dist/src/Lib/MCP/mcp-server.php +80 -0
- package/dist/src/Lib/MainLayout.php +19 -4
- package/dist/src/Lib/Middleware/CorsMiddleware.php +158 -0
- package/dist/src/Lib/PHPX/Fragment.php +32 -0
- package/dist/src/Lib/PHPX/PHPX.php +119 -5
- package/dist/src/Lib/PHPX/TemplateCompiler.php +78 -21
- package/dist/src/Lib/PHPX/TwMerge.php +305 -154
- package/dist/src/Lib/PHPX/TypeCoercer.php +5 -5
- package/dist/src/Lib/Request.php +4 -1
- package/dist/src/Lib/Security/RateLimiter.php +33 -0
- package/dist/src/Lib/Validator.php +20 -6
- package/dist/src/Lib/Websocket/websocket-server.php +105 -14
- package/dist/src/app/index.php +24 -5
- package/dist/src/app/js/index.js +1 -1
- package/package.json +1 -1
- package/dist/settings/restart-websocket.bat +0 -28
- package/dist/src/app/assets/images/prisma-php-black.svg +0 -6
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\Middleware;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CORS middleware driven by .env allow-list.
|
|
9
|
+
*
|
|
10
|
+
* Env keys (CSV or JSON array for origins):
|
|
11
|
+
* CORS_ALLOWED_ORIGINS=["http://localhost:5173","https://*.example.com"]
|
|
12
|
+
* CORS_ALLOW_CREDENTIALS=true
|
|
13
|
+
* CORS_ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
|
|
14
|
+
* CORS_ALLOWED_HEADERS=Content-Type,Authorization,X-Requested-With
|
|
15
|
+
* CORS_EXPOSE_HEADERS=
|
|
16
|
+
* CORS_MAX_AGE=86400
|
|
17
|
+
*
|
|
18
|
+
* Call as early as possible (after Dotenv::load, before session_start).
|
|
19
|
+
*/
|
|
20
|
+
final class CorsMiddleware
|
|
21
|
+
{
|
|
22
|
+
/** Entry point */
|
|
23
|
+
public static function handle(?array $overrides = null): void
|
|
24
|
+
{
|
|
25
|
+
// Not a CORS request
|
|
26
|
+
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
|
27
|
+
if ($origin === '') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Resolve config (env → overrides)
|
|
32
|
+
$cfg = self::buildConfig($overrides);
|
|
33
|
+
|
|
34
|
+
// Not allowed? Do nothing (browser will block)
|
|
35
|
+
if (!self::isAllowedOrigin($origin, $cfg['allowedOrigins'])) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Compute which value to send for Access-Control-Allow-Origin
|
|
40
|
+
// If credentials are disabled and '*' is in list, we can send '*'
|
|
41
|
+
$sendWildcard = (!$cfg['allowCredentials'] && self::listHasWildcard($cfg['allowedOrigins']));
|
|
42
|
+
$allowOriginValue = $sendWildcard ? '*' : self::normalize($origin);
|
|
43
|
+
|
|
44
|
+
// Vary for caches
|
|
45
|
+
header('Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers');
|
|
46
|
+
|
|
47
|
+
header('Access-Control-Allow-Origin: ' . $allowOriginValue);
|
|
48
|
+
if ($cfg['allowCredentials']) {
|
|
49
|
+
header('Access-Control-Allow-Credentials: true');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
53
|
+
// Preflight response
|
|
54
|
+
$requestedHeaders = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] ?? '';
|
|
55
|
+
$allowedHeaders = $cfg['allowedHeaders'] !== ''
|
|
56
|
+
? $cfg['allowedHeaders']
|
|
57
|
+
: ($requestedHeaders ?: 'Content-Type, Authorization, X-Requested-With');
|
|
58
|
+
|
|
59
|
+
header('Access-Control-Allow-Methods: ' . $cfg['allowedMethods']);
|
|
60
|
+
header('Access-Control-Allow-Headers: ' . $allowedHeaders);
|
|
61
|
+
if ($cfg['maxAge'] > 0) {
|
|
62
|
+
header('Access-Control-Max-Age: ' . (string) $cfg['maxAge']);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Optional: Private Network Access preflights (Chrome)
|
|
66
|
+
if (!empty($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK'])) {
|
|
67
|
+
header('Access-Control-Allow-Private-Network: true');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
http_response_code(204);
|
|
71
|
+
header('Content-Length: 0');
|
|
72
|
+
exit;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Simple/actual request
|
|
76
|
+
if ($cfg['exposeHeaders'] !== '') {
|
|
77
|
+
header('Access-Control-Expose-Headers: ' . $cfg['exposeHeaders']);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Read env + normalize + apply overrides */
|
|
82
|
+
private static function buildConfig(?array $overrides): array
|
|
83
|
+
{
|
|
84
|
+
$allowed = self::parseList($_ENV['CORS_ALLOWED_ORIGINS'] ?? '');
|
|
85
|
+
$cfg = [
|
|
86
|
+
'allowedOrigins' => $allowed,
|
|
87
|
+
'allowCredentials' => filter_var($_ENV['CORS_ALLOW_CREDENTIALS'] ?? 'false', FILTER_VALIDATE_BOOLEAN),
|
|
88
|
+
'allowedMethods' => $_ENV['CORS_ALLOWED_METHODS'] ?? 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
89
|
+
'allowedHeaders' => trim($_ENV['CORS_ALLOWED_HEADERS'] ?? ''),
|
|
90
|
+
'exposeHeaders' => trim($_ENV['CORS_EXPOSE_HEADERS'] ?? ''),
|
|
91
|
+
'maxAge' => (int)($_ENV['CORS_MAX_AGE'] ?? 86400),
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
if (is_array($overrides)) {
|
|
95
|
+
foreach ($overrides as $k => $v) {
|
|
96
|
+
if (array_key_exists($k, $cfg)) {
|
|
97
|
+
$cfg[$k] = $v;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Normalize patterns
|
|
103
|
+
$cfg['allowedOrigins'] = array_map([self::class, 'normalize'], $cfg['allowedOrigins']);
|
|
104
|
+
return $cfg;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** CSV or JSON array → array<string> */
|
|
108
|
+
private static function parseList(string $raw): array
|
|
109
|
+
{
|
|
110
|
+
$raw = trim($raw);
|
|
111
|
+
if ($raw === '') return [];
|
|
112
|
+
|
|
113
|
+
if ($raw[0] === '[') {
|
|
114
|
+
$arr = json_decode($raw, true);
|
|
115
|
+
if (is_array($arr)) {
|
|
116
|
+
return array_values(array_filter(array_map('strval', $arr), 'strlen'));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return array_values(array_filter(array_map('trim', explode(',', $raw)), 'strlen'));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private static function normalize(string $origin): string
|
|
123
|
+
{
|
|
124
|
+
return rtrim($origin, '/');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private static function isAllowedOrigin(string $origin, array $list): bool
|
|
128
|
+
{
|
|
129
|
+
$o = self::normalize($origin);
|
|
130
|
+
|
|
131
|
+
foreach ($list as $pattern) {
|
|
132
|
+
$p = self::normalize($pattern);
|
|
133
|
+
|
|
134
|
+
// literal "*"
|
|
135
|
+
if ($p === '*') return true;
|
|
136
|
+
|
|
137
|
+
// allow literal "null" for file:// or sandboxed if explicitly listed
|
|
138
|
+
if ($o === 'null' && strtolower($p) === 'null') return true;
|
|
139
|
+
|
|
140
|
+
// wildcard like https://*.example.com
|
|
141
|
+
if (strpos($p, '*') !== false) {
|
|
142
|
+
$regex = '/^' . str_replace('\*', '[^.]+', preg_quote($p, '/')) . '$/i';
|
|
143
|
+
if (preg_match($regex, $o)) return true;
|
|
144
|
+
} else {
|
|
145
|
+
if (strcasecmp($p, $o) === 0) return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private static function listHasWildcard(array $list): bool
|
|
152
|
+
{
|
|
153
|
+
foreach ($list as $p) {
|
|
154
|
+
if (trim($p) === '*') return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\PHPX;
|
|
6
|
+
|
|
7
|
+
use Lib\PHPX\PHPX;
|
|
8
|
+
|
|
9
|
+
class Fragment extends PHPX
|
|
10
|
+
{
|
|
11
|
+
/** @property ?string $as = div|span|section|article|nav|header|footer|main|aside */
|
|
12
|
+
public ?string $as = null;
|
|
13
|
+
public ?string $class = '';
|
|
14
|
+
|
|
15
|
+
public function __construct(array $props = [])
|
|
16
|
+
{
|
|
17
|
+
parent::__construct($props);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public function render(): string
|
|
21
|
+
{
|
|
22
|
+
if ($this->as !== null) {
|
|
23
|
+
$attributes = $this->getAttributes();
|
|
24
|
+
$class = $this->getMergeClasses($this->class);
|
|
25
|
+
$classAttr = $class ? "class=\"{$class}\"" : '';
|
|
26
|
+
|
|
27
|
+
return "<{$this->as} {$classAttr} {$attributes}>{$this->children}</{$this->as}>";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return $this->children;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -26,9 +26,19 @@ class PHPX implements IPHPX
|
|
|
26
26
|
*/
|
|
27
27
|
protected array $attributesArray = [];
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @var array|null Component hierarchy set during rendering
|
|
31
|
+
*/
|
|
32
|
+
protected static ?array $currentHierarchy = null;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @var string|null Current component ID
|
|
36
|
+
*/
|
|
37
|
+
protected static ?string $currentComponentId = null;
|
|
38
|
+
|
|
29
39
|
/**
|
|
30
40
|
* Constructor to initialize the component with the given properties.
|
|
31
|
-
*
|
|
41
|
+
*
|
|
32
42
|
* @param array<string, mixed> $props Optional properties to customize the component.
|
|
33
43
|
*/
|
|
34
44
|
public function __construct(array $props = [])
|
|
@@ -37,6 +47,112 @@ class PHPX implements IPHPX
|
|
|
37
47
|
$this->children = $props['children'] ?? '';
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Convert a property name to a JavaScript variable path.
|
|
52
|
+
*
|
|
53
|
+
* This method generates a JavaScript variable path for the given property name,
|
|
54
|
+
* based on the current component hierarchy. It is used to access properties in
|
|
55
|
+
* the JavaScript context of the component.
|
|
56
|
+
*
|
|
57
|
+
* @param string $name The property name to convert.
|
|
58
|
+
* @return string The JavaScript variable path for the property.
|
|
59
|
+
*/
|
|
60
|
+
protected function propsAsVar(string $name): string
|
|
61
|
+
{
|
|
62
|
+
if (empty($name)) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
$hierarchy = $this->getComponentHierarchy();
|
|
67
|
+
|
|
68
|
+
if (count($hierarchy) > 1 && end($hierarchy) === self::$currentComponentId) {
|
|
69
|
+
array_pop($hierarchy);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return "pphp.props." . implode('.', $hierarchy) . ".{$name}";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Emit a client-side dispatch call.
|
|
77
|
+
*
|
|
78
|
+
* @param non-empty-string|null $name Reactive key (e.g. "myVar", "voucher.total", or "app.s9ggniz.myVar").
|
|
79
|
+
* @param string $value Raw JS inserted verbatim (variable, expression, or pre-JSON-encoded value).
|
|
80
|
+
* @param array{
|
|
81
|
+
* scope?: 'current'|'parent'|'root'|string[]
|
|
82
|
+
* } $options Dispatch options (defaults to ['scope' => 'current']).
|
|
83
|
+
*
|
|
84
|
+
* @return string JavaScript snippet like: pphp.dispatchEvent("myVar", 123, {"scope":"current"});
|
|
85
|
+
*/
|
|
86
|
+
protected function dispatchEvent(?string $name, string $value, array $options = []): string
|
|
87
|
+
{
|
|
88
|
+
$name = trim((string) $name);
|
|
89
|
+
if ($name === '') {
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// normalize options
|
|
94
|
+
$opts = $options;
|
|
95
|
+
if (!array_key_exists('scope', $opts)) {
|
|
96
|
+
$opts['scope'] = 'current';
|
|
97
|
+
} else {
|
|
98
|
+
if (is_string($opts['scope'])) {
|
|
99
|
+
$allowed = ['current', 'parent', 'root'];
|
|
100
|
+
if (!in_array($opts['scope'], $allowed, true)) {
|
|
101
|
+
$opts['scope'] = 'current';
|
|
102
|
+
}
|
|
103
|
+
} elseif (is_array($opts['scope'])) {
|
|
104
|
+
$opts['scope'] = array_values(array_map('strval', $opts['scope']));
|
|
105
|
+
} else {
|
|
106
|
+
$opts['scope'] = 'current';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// safely encode name/options; value is inserted verbatim
|
|
111
|
+
$jsName = json_encode($name, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
112
|
+
$jsOpts = json_encode($opts, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
113
|
+
|
|
114
|
+
return "pphp.dispatchEvent({$jsName}, {$value}, {$jsOpts});";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Set the component hierarchy and current component ID (called by TemplateCompiler during rendering)
|
|
119
|
+
*
|
|
120
|
+
* @param array $hierarchy The component hierarchy
|
|
121
|
+
* @param string $currentId The current component ID
|
|
122
|
+
*/
|
|
123
|
+
public static function setRenderingContext(array $hierarchy, string $currentId): void
|
|
124
|
+
{
|
|
125
|
+
self::$currentHierarchy = $hierarchy;
|
|
126
|
+
self::$currentComponentId = $currentId;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the parent component ID from the current hierarchy.
|
|
131
|
+
*
|
|
132
|
+
* @return string|null The parent component ID, or null if no parent exists
|
|
133
|
+
*/
|
|
134
|
+
protected function getParentComponent(): ?string
|
|
135
|
+
{
|
|
136
|
+
$fullHierarchy = $this->getComponentHierarchy();
|
|
137
|
+
|
|
138
|
+
if (count($fullHierarchy) >= 2) {
|
|
139
|
+
return $fullHierarchy[count($fullHierarchy) - 2];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get the full component hierarchy as an array.
|
|
147
|
+
* Useful for building complete hierarchy paths for JavaScript.
|
|
148
|
+
*
|
|
149
|
+
* @return array Array of component IDs from root to current
|
|
150
|
+
*/
|
|
151
|
+
protected function getComponentHierarchy(): array
|
|
152
|
+
{
|
|
153
|
+
return self::$currentHierarchy ?? ['app'];
|
|
154
|
+
}
|
|
155
|
+
|
|
40
156
|
/**
|
|
41
157
|
* Combines and returns the CSS classes for the component.
|
|
42
158
|
*
|
|
@@ -68,7 +184,7 @@ class PHPX implements IPHPX
|
|
|
68
184
|
unset($chunk);
|
|
69
185
|
|
|
70
186
|
$merged = PrismaPHPSettings::$option->tailwindcss
|
|
71
|
-
? TwMerge::
|
|
187
|
+
? TwMerge::merge(...$all)
|
|
72
188
|
: $this->mergeClasses(...$all);
|
|
73
189
|
|
|
74
190
|
return str_replace(array_keys($expr), array_values($expr), $merged);
|
|
@@ -118,9 +234,7 @@ class PHPX implements IPHPX
|
|
|
118
234
|
array_flip(array_merge($reserved, $exclude))
|
|
119
235
|
);
|
|
120
236
|
|
|
121
|
-
|
|
122
|
-
$props[$k] = $v;
|
|
123
|
-
}
|
|
237
|
+
$props = array_merge($params, $props);
|
|
124
238
|
|
|
125
239
|
$pairs = array_map(
|
|
126
240
|
static fn($k, $v) => sprintf(
|
|
@@ -108,12 +108,14 @@ class TemplateCompiler
|
|
|
108
108
|
);
|
|
109
109
|
|
|
110
110
|
if (!isset($_SERVER['HTTP_X_PPHP_NAVIGATION'])) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
if (!PrismaPHPSettings::$option->backendOnly) {
|
|
112
|
+
$htmlContent = preg_replace(
|
|
113
|
+
'/<body([^>]*)>/i',
|
|
114
|
+
'<body$1 hidden>',
|
|
115
|
+
$htmlContent,
|
|
116
|
+
1
|
|
117
|
+
);
|
|
118
|
+
}
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
$bodyClosePattern = '/(<\/body\s*>)/i';
|
|
@@ -202,9 +204,12 @@ class TemplateCompiler
|
|
|
202
204
|
|
|
203
205
|
private static function protectInlineScripts(string $html): string
|
|
204
206
|
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
if (stripos($html, '<script') === false) {
|
|
208
|
+
return $html;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
$processScripts = static function (string $content): string {
|
|
212
|
+
$callback = static function (array $m): string {
|
|
208
213
|
if (preg_match('/\bsrc\s*=/i', $m[1])) {
|
|
209
214
|
return $m[0];
|
|
210
215
|
}
|
|
@@ -233,9 +238,33 @@ class TemplateCompiler
|
|
|
233
238
|
$code = str_replace(']]>', ']]]]><![CDATA[>', $m[2]);
|
|
234
239
|
|
|
235
240
|
return "<script{$m[1]}><![CDATA[\n{$code}\n]]></script>";
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
$result = preg_replace_callback(
|
|
244
|
+
'#<script\b([^>]*?)>(.*?)</script>#is',
|
|
245
|
+
$callback,
|
|
246
|
+
$content
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if ($result === null) {
|
|
250
|
+
$result = preg_replace_callback(
|
|
251
|
+
'#<script\b([^>]*?)>(.*?)</script>#is',
|
|
252
|
+
$callback,
|
|
253
|
+
$content
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return $result ?? $content;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return $result;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (preg_match('/^(.*?<body\b[^>]*>)(.*?)(<\/body>.*)$/is', $html, $parts)) {
|
|
263
|
+
[$all, $beforeBody, $body, $afterBody] = $parts;
|
|
264
|
+
return $beforeBody . $processScripts($body) . $afterBody;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return $processScripts($html);
|
|
239
268
|
}
|
|
240
269
|
|
|
241
270
|
public static function innerXml(DOMNode $node): string
|
|
@@ -381,23 +410,35 @@ class TemplateCompiler
|
|
|
381
410
|
string $componentName,
|
|
382
411
|
array $incomingProps
|
|
383
412
|
): string {
|
|
384
|
-
$mapping
|
|
385
|
-
|
|
413
|
+
$mapping = self::selectComponentMapping($componentName);
|
|
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}";
|
|
419
|
+
|
|
420
|
+
$originalStack = self::$sectionStack;
|
|
421
|
+
self::$sectionStack[] = $sectionId;
|
|
422
|
+
|
|
423
|
+
PHPX::setRenderingContext($originalStack, $sectionId);
|
|
424
|
+
|
|
425
|
+
$instance = self::initializeComponentInstance($mapping, $incomingProps);
|
|
386
426
|
|
|
387
427
|
$childHtml = '';
|
|
388
428
|
foreach ($node->childNodes as $c) {
|
|
389
429
|
$childHtml .= self::processNode($c);
|
|
390
430
|
}
|
|
391
431
|
|
|
392
|
-
|
|
432
|
+
self::$sectionStack = $originalStack;
|
|
393
433
|
|
|
394
|
-
$
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
434
|
+
$instance->children = trim($childHtml);
|
|
435
|
+
|
|
436
|
+
PHPX::setRenderingContext($originalStack, $sectionId);
|
|
437
|
+
|
|
438
|
+
$html = $instance->render();
|
|
439
|
+
$html = self::preprocessFragmentSyntax($html);
|
|
398
440
|
|
|
399
|
-
$
|
|
400
|
-
$fragDom = self::convertToXml($html);
|
|
441
|
+
$fragDom = self::convertToXml($html);
|
|
401
442
|
$root = $fragDom->documentElement;
|
|
402
443
|
foreach ($root->childNodes as $c) {
|
|
403
444
|
if ($c instanceof DOMElement) {
|
|
@@ -407,6 +448,14 @@ class TemplateCompiler
|
|
|
407
448
|
}
|
|
408
449
|
|
|
409
450
|
$htmlOut = self::innerXml($fragDom);
|
|
451
|
+
$htmlOut = preg_replace_callback(
|
|
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
|
+
);
|
|
458
|
+
|
|
410
459
|
if (
|
|
411
460
|
str_contains($htmlOut, '{{') ||
|
|
412
461
|
self::hasComponentTag($htmlOut) ||
|
|
@@ -418,6 +467,14 @@ class TemplateCompiler
|
|
|
418
467
|
return $htmlOut;
|
|
419
468
|
}
|
|
420
469
|
|
|
470
|
+
private static function preprocessFragmentSyntax(string $content): string
|
|
471
|
+
{
|
|
472
|
+
$content = preg_replace('/<>/', '<Fragment>', $content);
|
|
473
|
+
$content = preg_replace('/<\/>/', '</Fragment>', $content);
|
|
474
|
+
|
|
475
|
+
return $content;
|
|
476
|
+
}
|
|
477
|
+
|
|
421
478
|
private static function selectComponentMapping(string $componentName): array
|
|
422
479
|
{
|
|
423
480
|
if (!isset(self::$classMappings[$componentName])) {
|