create-prisma-php-app 1.26.526 → 1.26.528
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/bootstrap.php +34 -29
- package/dist/index.js +367 -366
- package/dist/src/Lib/MainLayout.php +6 -2
- package/dist/src/Lib/PHPX/IPHPX.php +6 -3
- package/dist/src/Lib/PHPX/PHPX.php +121 -0
- package/dist/src/Lib/PHPX/TemplateCompiler.php +59 -27
- package/dist/src/Lib/PHPX/TwMerge.php +214 -0
- package/dist/src/app/js/index.js +1 -1
- package/package.json +1 -1
- package/dist/src/Lib/PHPX/Utils.php +0 -41
|
@@ -12,7 +12,9 @@ class MainLayout
|
|
|
12
12
|
public static string $childLayoutChildren = '';
|
|
13
13
|
|
|
14
14
|
private static array $headScripts = [];
|
|
15
|
+
private static array $headScriptsMap = [];
|
|
15
16
|
private static array $footerScripts = [];
|
|
17
|
+
private static array $footerScriptsMap = [];
|
|
16
18
|
private static array $customMetadata = [];
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -24,8 +26,9 @@ class MainLayout
|
|
|
24
26
|
public static function addHeadScript(string ...$scripts): void
|
|
25
27
|
{
|
|
26
28
|
foreach ($scripts as $script) {
|
|
27
|
-
if (!
|
|
29
|
+
if (!isset(self::$headScriptsMap[$script])) {
|
|
28
30
|
self::$headScripts[] = $script;
|
|
31
|
+
self::$headScriptsMap[$script] = true;
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
}
|
|
@@ -43,8 +46,9 @@ class MainLayout
|
|
|
43
46
|
public static function addFooterScript(string ...$scripts): void
|
|
44
47
|
{
|
|
45
48
|
foreach ($scripts as $script) {
|
|
46
|
-
if (!
|
|
49
|
+
if (!isset(self::$footerScriptsMap[$script])) {
|
|
47
50
|
self::$footerScripts[] = $script;
|
|
51
|
+
self::$footerScriptsMap[$script] = true;
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -8,7 +8,8 @@ namespace Lib\PHPX;
|
|
|
8
8
|
* The interface for the PHPX component classes.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
interface IPHPX
|
|
11
|
+
interface IPHPX
|
|
12
|
+
{
|
|
12
13
|
/**
|
|
13
14
|
* Constructor to initialize the component with the given properties.
|
|
14
15
|
*
|
|
@@ -18,8 +19,10 @@ interface IPHPX {
|
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Registers or initializes any necessary components or settings. (Placeholder method).
|
|
22
|
+
*
|
|
23
|
+
* @param array<string, mixed> $props Optional properties to customize the component.
|
|
21
24
|
*/
|
|
22
|
-
public static function init(): void;
|
|
25
|
+
public static function init(array $props = []): void;
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* Renders the component with the given properties and children.
|
|
@@ -27,4 +30,4 @@ interface IPHPX {
|
|
|
27
30
|
* @return string The rendered HTML content.
|
|
28
31
|
*/
|
|
29
32
|
public function render(): string;
|
|
30
|
-
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Lib\PHPX;
|
|
4
|
+
|
|
5
|
+
use Lib\PHPX\IPHPX;
|
|
6
|
+
use Lib\PHPX\TwMerge;
|
|
7
|
+
|
|
8
|
+
class PHPX implements IPHPX
|
|
9
|
+
{
|
|
10
|
+
/**
|
|
11
|
+
* @var array<string, mixed> The properties or attributes passed to the component.
|
|
12
|
+
*/
|
|
13
|
+
protected array $props;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @var mixed The children elements or content to be rendered within the component.
|
|
17
|
+
*/
|
|
18
|
+
protected mixed $children;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @var string The CSS class for custom styling.
|
|
22
|
+
*/
|
|
23
|
+
protected string $class;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Constructor to initialize the component with the given properties.
|
|
27
|
+
*
|
|
28
|
+
* @param array<string, mixed> $props Optional properties to customize the component.
|
|
29
|
+
*/
|
|
30
|
+
public function __construct(array $props = [])
|
|
31
|
+
{
|
|
32
|
+
$this->props = $props;
|
|
33
|
+
$this->children = $props['children'] ?? '';
|
|
34
|
+
$this->class = $props['class'] ?? '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Registers or initializes any necessary components or settings. (Placeholder method).
|
|
39
|
+
*
|
|
40
|
+
* @param array<string, mixed> $props Optional properties to customize the initialization.
|
|
41
|
+
*/
|
|
42
|
+
public static function init(array $props = []): void
|
|
43
|
+
{
|
|
44
|
+
// Register the component or any necessary initialization
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Combines and returns the CSS classes for the component.
|
|
49
|
+
*
|
|
50
|
+
* This method merges the optional base CSS class with additional classes
|
|
51
|
+
* defined in the component's `$class` property. If no base class is provided,
|
|
52
|
+
* only the component's `$class` property will be used. It ensures that there
|
|
53
|
+
* are no duplicate classes and the classes are properly formatted.
|
|
54
|
+
*
|
|
55
|
+
* @param string|null $baseClass The optional base CSS class to be merged. Defaults to `null`.
|
|
56
|
+
* @return string The merged CSS class string.
|
|
57
|
+
*/
|
|
58
|
+
protected function getMergeClasses(?string $baseClass = null): string
|
|
59
|
+
{
|
|
60
|
+
return TwMerge::mergeClasses($baseClass, $this->class);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generates and returns a string of HTML attributes from the provided props.
|
|
65
|
+
* Excludes 'class' and 'children' props from being added as attributes.
|
|
66
|
+
*
|
|
67
|
+
* @return string The generated HTML attributes.
|
|
68
|
+
*/
|
|
69
|
+
protected function getAttributes(): string
|
|
70
|
+
{
|
|
71
|
+
// Filter out 'class' and 'children' props
|
|
72
|
+
$filteredProps = array_filter($this->props, function ($key) {
|
|
73
|
+
return !in_array($key, ['class', 'children']);
|
|
74
|
+
}, ARRAY_FILTER_USE_KEY);
|
|
75
|
+
|
|
76
|
+
// Build attributes string by escaping keys and values
|
|
77
|
+
$attributes = [];
|
|
78
|
+
foreach ($filteredProps as $key => $value) {
|
|
79
|
+
$escapedKey = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
|
|
80
|
+
$escapedValue = htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
|
81
|
+
$attributes[] = "$escapedKey='$escapedValue'";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return implode(' ', $attributes);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Renders the component as an HTML string with the appropriate classes and attributes.
|
|
89
|
+
* Also, allows for dynamic children rendering if a callable is passed.
|
|
90
|
+
*
|
|
91
|
+
* @return string The final rendered HTML of the component.
|
|
92
|
+
*/
|
|
93
|
+
public function render(): string
|
|
94
|
+
{
|
|
95
|
+
$attributes = $this->getAttributes();
|
|
96
|
+
$class = $this->getMergeClasses();
|
|
97
|
+
|
|
98
|
+
return <<<HTML
|
|
99
|
+
<div class="$class" $attributes>{$this->children}</div>
|
|
100
|
+
HTML;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Converts the object to its string representation by rendering the component.
|
|
105
|
+
*
|
|
106
|
+
* This method allows the object to be used directly in string contexts, such as
|
|
107
|
+
* when echoing or concatenating, by automatically invoking the `render()` method.
|
|
108
|
+
* If an exception occurs during rendering, it safely returns an empty string
|
|
109
|
+
* to prevent runtime errors, ensuring robustness in all scenarios.
|
|
110
|
+
*
|
|
111
|
+
* @return string The rendered HTML output of the component, or an empty string if rendering fails.
|
|
112
|
+
*/
|
|
113
|
+
public function __toString(): string
|
|
114
|
+
{
|
|
115
|
+
try {
|
|
116
|
+
return $this->render();
|
|
117
|
+
} catch (\Exception) {
|
|
118
|
+
return ''; // Return an empty string or a fallback message in case of errors
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -12,6 +12,24 @@ use DOMComment;
|
|
|
12
12
|
class TemplateCompiler
|
|
13
13
|
{
|
|
14
14
|
protected static $classMappings = [];
|
|
15
|
+
protected static $selfClosingTags = [
|
|
16
|
+
'area',
|
|
17
|
+
'base',
|
|
18
|
+
'br',
|
|
19
|
+
'col',
|
|
20
|
+
'command',
|
|
21
|
+
'embed',
|
|
22
|
+
'hr',
|
|
23
|
+
'img',
|
|
24
|
+
'input',
|
|
25
|
+
'keygen',
|
|
26
|
+
'link',
|
|
27
|
+
'meta',
|
|
28
|
+
'param',
|
|
29
|
+
'source',
|
|
30
|
+
'track',
|
|
31
|
+
'wbr'
|
|
32
|
+
];
|
|
15
33
|
|
|
16
34
|
public static function compile(string $templateContent): string
|
|
17
35
|
{
|
|
@@ -26,12 +44,14 @@ class TemplateCompiler
|
|
|
26
44
|
|
|
27
45
|
if (!$dom->loadXML($wrappedContent)) {
|
|
28
46
|
$errors = self::getXmlErrors();
|
|
29
|
-
throw new \RuntimeException(
|
|
47
|
+
throw new \RuntimeException(
|
|
48
|
+
"XML Parsing Failed: " . implode("; ", $errors)
|
|
49
|
+
);
|
|
30
50
|
}
|
|
31
51
|
libxml_clear_errors();
|
|
32
52
|
|
|
33
53
|
$root = $dom->documentElement;
|
|
34
|
-
$output =
|
|
54
|
+
$output = "";
|
|
35
55
|
foreach ($root->childNodes as $child) {
|
|
36
56
|
$output .= self::processNode($child);
|
|
37
57
|
}
|
|
@@ -55,21 +75,21 @@ class TemplateCompiler
|
|
|
55
75
|
protected static function formatLibxmlError(\LibXMLError $error): string
|
|
56
76
|
{
|
|
57
77
|
$errorType = match ($error->level) {
|
|
58
|
-
LIBXML_ERR_WARNING =>
|
|
59
|
-
LIBXML_ERR_ERROR =>
|
|
60
|
-
LIBXML_ERR_FATAL =>
|
|
61
|
-
default =>
|
|
78
|
+
LIBXML_ERR_WARNING => "Warning",
|
|
79
|
+
LIBXML_ERR_ERROR => "Error",
|
|
80
|
+
LIBXML_ERR_FATAL => "Fatal",
|
|
81
|
+
default => "Unknown",
|
|
62
82
|
};
|
|
63
83
|
|
|
64
84
|
// Highlight the tag name in the error message
|
|
65
85
|
$message = trim($error->message);
|
|
66
|
-
if (preg_match(
|
|
86
|
+
if (preg_match("/tag (.*?) /", $message, $matches)) {
|
|
67
87
|
$tag = $matches[1];
|
|
68
88
|
$message = str_replace($tag, "`{$tag}`", $message);
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
return sprintf(
|
|
72
|
-
|
|
92
|
+
"[%s] Line %d, Column %d: %s",
|
|
73
93
|
$errorType,
|
|
74
94
|
$error->line,
|
|
75
95
|
$error->column,
|
|
@@ -79,7 +99,7 @@ class TemplateCompiler
|
|
|
79
99
|
|
|
80
100
|
protected static function processNode($node): string
|
|
81
101
|
{
|
|
82
|
-
$output =
|
|
102
|
+
$output = "";
|
|
83
103
|
|
|
84
104
|
if ($node instanceof DOMElement) {
|
|
85
105
|
$componentName = $node->nodeName;
|
|
@@ -91,7 +111,7 @@ class TemplateCompiler
|
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
// Process child nodes
|
|
94
|
-
$innerContent =
|
|
114
|
+
$innerContent = "";
|
|
95
115
|
if ($node->hasChildNodes()) {
|
|
96
116
|
foreach ($node->childNodes as $child) {
|
|
97
117
|
$innerContent .= self::processNode($child);
|
|
@@ -100,11 +120,15 @@ class TemplateCompiler
|
|
|
100
120
|
|
|
101
121
|
// Include inner content as 'children' if it's not empty
|
|
102
122
|
if (trim($innerContent)) {
|
|
103
|
-
$attributes[
|
|
123
|
+
$attributes["children"] = $innerContent;
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
$output .= self::processComponent(
|
|
107
|
-
|
|
126
|
+
$output .= self::processComponent(
|
|
127
|
+
$componentName,
|
|
128
|
+
$attributes,
|
|
129
|
+
$innerContent
|
|
130
|
+
);
|
|
131
|
+
} elseif ($node instanceof DOMComment) {
|
|
108
132
|
$output .= "<!--{$node->textContent}-->";
|
|
109
133
|
} else {
|
|
110
134
|
// For text nodes and others
|
|
@@ -114,30 +138,38 @@ class TemplateCompiler
|
|
|
114
138
|
return $output;
|
|
115
139
|
}
|
|
116
140
|
|
|
117
|
-
protected static function initializeClassMappings()
|
|
141
|
+
protected static function initializeClassMappings(): void
|
|
118
142
|
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
143
|
+
foreach (
|
|
144
|
+
PrismaPHPSettings::$classLogFiles
|
|
145
|
+
as $classPath => $classInfo
|
|
146
|
+
) {
|
|
147
|
+
$normalizedClassPath = str_replace("\\", "/", $classPath);
|
|
148
|
+
$className = pathinfo($normalizedClassPath, PATHINFO_FILENAME);
|
|
123
149
|
self::$classMappings[$className] = $classPath;
|
|
124
150
|
}
|
|
125
151
|
}
|
|
126
152
|
|
|
127
|
-
protected static function processComponent(
|
|
128
|
-
|
|
153
|
+
protected static function processComponent(
|
|
154
|
+
string $componentName,
|
|
155
|
+
array $attributes,
|
|
156
|
+
string $innerContent
|
|
157
|
+
): string {
|
|
129
158
|
// Check if the component class exists in the mappings
|
|
130
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
isset(self::$classMappings[$componentName]) &&
|
|
161
|
+
class_exists(self::$classMappings[$componentName])
|
|
162
|
+
) {
|
|
131
163
|
$classPath = self::$classMappings[$componentName];
|
|
132
164
|
// Instantiate the component
|
|
133
165
|
$componentInstance = new $classPath($attributes);
|
|
134
166
|
return $componentInstance->render();
|
|
135
167
|
} else {
|
|
136
|
-
// Render as an
|
|
168
|
+
// Render as an HTML tag
|
|
137
169
|
$attributesString = self::renderAttributes($attributes);
|
|
138
170
|
|
|
139
|
-
//
|
|
140
|
-
if (
|
|
171
|
+
// Determine if the tag should be self-closing
|
|
172
|
+
if (in_array(strtolower($componentName), self::$selfClosingTags)) {
|
|
141
173
|
return "<$componentName $attributesString />";
|
|
142
174
|
} else {
|
|
143
175
|
return "<$componentName $attributesString>$innerContent</$componentName>";
|
|
@@ -148,16 +180,16 @@ class TemplateCompiler
|
|
|
148
180
|
protected static function renderAttributes(array $attributes): string
|
|
149
181
|
{
|
|
150
182
|
if (empty($attributes)) {
|
|
151
|
-
return
|
|
183
|
+
return "";
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
$attrArray = [];
|
|
155
187
|
foreach ($attributes as $key => $value) {
|
|
156
|
-
if ($key !==
|
|
188
|
+
if ($key !== "children") {
|
|
157
189
|
$attrArray[] = "{$key}=\"{$value}\"";
|
|
158
190
|
}
|
|
159
191
|
}
|
|
160
192
|
|
|
161
|
-
return
|
|
193
|
+
return " " . implode(" ", $attrArray);
|
|
162
194
|
}
|
|
163
195
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Lib\PHPX;
|
|
6
|
+
|
|
7
|
+
class TwMerge
|
|
8
|
+
{
|
|
9
|
+
private static $classGroupPatterns = [
|
|
10
|
+
// **General Padding classes**
|
|
11
|
+
"p" => "/^p-/",
|
|
12
|
+
|
|
13
|
+
// **Specific Padding classes**
|
|
14
|
+
"pt" => "/^pt-/",
|
|
15
|
+
"pr" => "/^pr-/",
|
|
16
|
+
"pb" => "/^pb-/",
|
|
17
|
+
"pl" => "/^pl-/",
|
|
18
|
+
"px" => "/^px-/",
|
|
19
|
+
"py" => "/^py-/",
|
|
20
|
+
|
|
21
|
+
// **Margin classes (similar logic)**
|
|
22
|
+
"m" => "/^m-/",
|
|
23
|
+
"mt" => "/^mt-/",
|
|
24
|
+
"mr" => "/^mr-/",
|
|
25
|
+
"mb" => "/^mb-/",
|
|
26
|
+
"ml" => "/^ml-/",
|
|
27
|
+
"mx" => "/^mx-/",
|
|
28
|
+
"my" => "/^my-/",
|
|
29
|
+
|
|
30
|
+
// **Background color classes**
|
|
31
|
+
"bg" => "/^bg-/",
|
|
32
|
+
|
|
33
|
+
// **Text size classes**
|
|
34
|
+
"text-size" => '/^text-(xs|sm|base|lg|xl|[2-9]xl)$/',
|
|
35
|
+
|
|
36
|
+
// **Text color classes**
|
|
37
|
+
"text-color" => '/^text-(?!xs$|sm$|base$|lg$|xl$|[2-9]xl$).+$/',
|
|
38
|
+
|
|
39
|
+
// **Text alignment classes**
|
|
40
|
+
"text-alignment" => '/^text-(left|center|right|justify)$/',
|
|
41
|
+
|
|
42
|
+
// **Text transform classes**
|
|
43
|
+
"text-transform" =>
|
|
44
|
+
'/^text-(uppercase|lowercase|capitalize|normal-case)$/',
|
|
45
|
+
|
|
46
|
+
// **Text decoration classes**
|
|
47
|
+
"text-decoration" => '/^text-(underline|line-through|no-underline)$/',
|
|
48
|
+
|
|
49
|
+
// **Border width classes**
|
|
50
|
+
"border-width" => '/^border(-[0-9]+)?$/',
|
|
51
|
+
|
|
52
|
+
// **Border color classes**
|
|
53
|
+
"border-color" => "/^border-(?![0-9])/",
|
|
54
|
+
|
|
55
|
+
// **Border radius classes**
|
|
56
|
+
"rounded" => '/^rounded(-.*)?$/',
|
|
57
|
+
|
|
58
|
+
// **Font weight classes**
|
|
59
|
+
"font" =>
|
|
60
|
+
'/^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/',
|
|
61
|
+
|
|
62
|
+
// **Hover background color classes**
|
|
63
|
+
"hover:bg" => "/^hover:bg-/",
|
|
64
|
+
|
|
65
|
+
// **Hover text color classes**
|
|
66
|
+
"hover:text" => "/^hover:text-/",
|
|
67
|
+
|
|
68
|
+
// **Transition classes**
|
|
69
|
+
"transition" => '/^transition(-[a-z]+)?$/',
|
|
70
|
+
|
|
71
|
+
// **Opacity classes
|
|
72
|
+
"opacity" => '/^opacity(-[0-9]+)?$/',
|
|
73
|
+
|
|
74
|
+
// **Other utility classes can be added here**
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
private static $conflictGroups = [
|
|
78
|
+
// **Padding conflict groups**
|
|
79
|
+
"p" => ["p", "px", "py", "pt", "pr", "pb", "pl"],
|
|
80
|
+
"px" => ["px", "pl", "pr"],
|
|
81
|
+
"py" => ["py", "pt", "pb"],
|
|
82
|
+
"pt" => ["pt"],
|
|
83
|
+
"pr" => ["pr"],
|
|
84
|
+
"pb" => ["pb"],
|
|
85
|
+
"pl" => ["pl"],
|
|
86
|
+
|
|
87
|
+
// **Margin conflict groups**
|
|
88
|
+
"m" => ["m", "mx", "my", "mt", "mr", "mb", "ml"],
|
|
89
|
+
"mx" => ["mx", "ml", "mr"],
|
|
90
|
+
"my" => ["my", "mt", "mb"],
|
|
91
|
+
"mt" => ["mt"],
|
|
92
|
+
"mr" => ["mr"],
|
|
93
|
+
"mb" => ["mb"],
|
|
94
|
+
"ml" => ["ml"],
|
|
95
|
+
|
|
96
|
+
// **Border width conflict group**
|
|
97
|
+
"border-width" => ["border-width"],
|
|
98
|
+
|
|
99
|
+
// **Border color conflict group**
|
|
100
|
+
"border-color" => ["border-color"],
|
|
101
|
+
|
|
102
|
+
// **Text size conflict group**
|
|
103
|
+
"text-size" => ["text-size"],
|
|
104
|
+
|
|
105
|
+
// **Text color conflict group**
|
|
106
|
+
"text-color" => ["text-color"],
|
|
107
|
+
|
|
108
|
+
// **Text alignment conflict group**
|
|
109
|
+
"text-alignment" => ["text-alignment"],
|
|
110
|
+
|
|
111
|
+
// **Text transform conflict group**
|
|
112
|
+
"text-transform" => ["text-transform"],
|
|
113
|
+
|
|
114
|
+
// **Text decoration conflict group**
|
|
115
|
+
"text-decoration" => ["text-decoration"],
|
|
116
|
+
|
|
117
|
+
// **Opacity conflict group
|
|
118
|
+
"opacity" => ["opacity"],
|
|
119
|
+
|
|
120
|
+
// **Add other conflict groups as needed**
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Merges multiple CSS class strings or arrays of CSS class strings into a single, optimized CSS class string.
|
|
125
|
+
*
|
|
126
|
+
* This method processes the provided classes, which can be either strings or arrays of strings, removes
|
|
127
|
+
* duplicate or conflicting classes, and prioritizes the last occurrence of a class. It splits class strings
|
|
128
|
+
* by whitespace, handles conflicting class groups, and ensures a clean and well-formatted output.
|
|
129
|
+
*
|
|
130
|
+
* ### Features:
|
|
131
|
+
* - Accepts individual class strings or arrays of class strings.
|
|
132
|
+
* - Automatically handles arrays by flattening them into individual strings.
|
|
133
|
+
* - Removes duplicate or conflicting classes based on class groups.
|
|
134
|
+
* - Combines all classes into a single string, properly formatted and optimized.
|
|
135
|
+
*
|
|
136
|
+
* @param string|array ...$classes The CSS classes to be merged. Each argument can be a string or an array of strings.
|
|
137
|
+
* @return string A single CSS class string with duplicates and conflicts resolved.
|
|
138
|
+
*/
|
|
139
|
+
public static function mergeClasses(string|array ...$classes): string
|
|
140
|
+
{
|
|
141
|
+
$classArray = [];
|
|
142
|
+
|
|
143
|
+
foreach ($classes as $class) {
|
|
144
|
+
// Handle arrays by flattening them into strings
|
|
145
|
+
$classList = is_array($class) ? $class : [$class];
|
|
146
|
+
foreach ($classList as $item) {
|
|
147
|
+
if (!empty(trim($item))) {
|
|
148
|
+
// Split the classes by any whitespace characters
|
|
149
|
+
$splitClasses = preg_split("/\s+/", $item);
|
|
150
|
+
foreach ($splitClasses as $individualClass) {
|
|
151
|
+
$classKey = self::getClassGroup($individualClass);
|
|
152
|
+
$conflictingKeys = self::getConflictingKeys($classKey);
|
|
153
|
+
|
|
154
|
+
// Remove any conflicting classes
|
|
155
|
+
foreach ($conflictingKeys as $key) {
|
|
156
|
+
unset($classArray[$key]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update the array, prioritizing the last occurrence
|
|
160
|
+
$classArray[$classKey] = $individualClass;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Combine the final classes into a single string
|
|
167
|
+
return implode(" ", array_values($classArray));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private static function getClassGroup($class)
|
|
171
|
+
{
|
|
172
|
+
// Match optional prefixes (responsive and variants)
|
|
173
|
+
$pattern = '/^((?:[a-z-]+:)*)(.+)$/';
|
|
174
|
+
|
|
175
|
+
if (preg_match($pattern, $class, $matches)) {
|
|
176
|
+
$prefixes = $matches[1]; // Includes responsive and variant prefixes
|
|
177
|
+
$utilityClass = $matches[2]; // The utility class
|
|
178
|
+
|
|
179
|
+
// Now match utilityClass against patterns
|
|
180
|
+
foreach (self::$classGroupPatterns as $groupKey => $regex) {
|
|
181
|
+
if (preg_match($regex, $utilityClass)) {
|
|
182
|
+
return $prefixes . $groupKey;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// If no match, use the full class
|
|
187
|
+
return $prefixes . $utilityClass;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// For classes without a recognizable prefix, return the class itself
|
|
191
|
+
return $class;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private static function getConflictingKeys($classKey)
|
|
195
|
+
{
|
|
196
|
+
// Remove any responsive or variant prefixes
|
|
197
|
+
$baseClassKey = preg_replace("/^(?:[a-z-]+:)+/", "", $classKey);
|
|
198
|
+
|
|
199
|
+
// Check for conflicts
|
|
200
|
+
if (isset(self::$conflictGroups[$baseClassKey])) {
|
|
201
|
+
// Add responsive and variant prefixes back to the conflicting keys
|
|
202
|
+
$prefix = preg_replace(
|
|
203
|
+
"/" . preg_quote($baseClassKey, "/") . '$/',
|
|
204
|
+
"",
|
|
205
|
+
$classKey
|
|
206
|
+
);
|
|
207
|
+
return array_map(function ($conflict) use ($prefix) {
|
|
208
|
+
return $prefix . $conflict;
|
|
209
|
+
}, self::$conflictGroups[$baseClassKey]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return [$classKey];
|
|
213
|
+
}
|
|
214
|
+
}
|