├── LICENSE.md
├── README.md
├── composer.json
├── phpunit.ci.xml
├── src
├── Cache
│ ├── EdgeArrayCache.php
│ ├── EdgeCacheInterface.php
│ ├── EdgeFileCache.php
│ └── EdgeStorageCache.php
├── Compiler
│ ├── Concern
│ │ ├── CompileClassTrait.php
│ │ ├── CompileCommentTrait.php
│ │ ├── CompileComponentTrait.php
│ │ ├── CompileConditional.php
│ │ ├── CompileEchoTrait.php
│ │ ├── CompileIncludeTrait.php
│ │ ├── CompileJsonTrait.php
│ │ ├── CompileLayoutTrait.php
│ │ ├── CompileLoopTrait.php
│ │ ├── CompileRawPhpTrait.php
│ │ └── CompileStackTrait.php
│ ├── EdgeCompiler.php
│ └── EdgeCompilerInterface.php
├── Component
│ ├── AbstractComponent.php
│ ├── AnonymousComponent.php
│ ├── AppendableAttributeValue.php
│ ├── ComponentAttributes.php
│ ├── ComponentExtension.php
│ ├── ComponentTagCompiler.php
│ ├── DynamicComponent.php
│ └── InvokableComponentVariable.php
├── Concern
│ ├── ManageComponentTrait.php
│ ├── ManageEventTrait.php
│ ├── ManageLayoutTrait.php
│ └── ManageStackTrait.php
├── Edge.php
├── EdgeHelper.php
├── Exception
│ ├── EdgeException.php
│ └── LayoutNotFoundException.php
├── Extension
│ ├── DirectivesExtensionInterface.php
│ ├── EdgeExtensionInterface.php
│ ├── GlobalVariablesExtensionInterface.php
│ └── ParsersExtensionInterface.php
├── Loader
│ ├── EdgeFileLoader.php
│ ├── EdgeLoaderInterface.php
│ └── EdgeStringLoader.php
├── Provider
│ └── EdgeProvider.php
├── Wrapper
│ └── SlotWrapper.php
├── bootstrap.php
└── functions.php
└── tmp
└── .gitkeep
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Simon Asika
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 | Edge
9 |
10 |
11 | Windwalker Edge package
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ### Installation
22 |
23 | ```bash
24 | composer require windwalker/edge ^4.0
25 | ```
26 |
27 | ### Resources
28 |
29 | - [Documentation](https://windwalker.io/documentation/components/edge/)
30 | - [Bug report](https://github.com/windwalker-io/framework)
31 | - [Contributing](https://github.com/windwalker-io/framework)
32 | - [Discussion](https://github.com/windwalker-io/framework/discussions)
33 |
34 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "windwalker/edge",
3 | "type": "windwalker-package",
4 | "description": "Windwalker Edge package",
5 | "keywords": [
6 | "windwalker",
7 | "framework",
8 | "edge",
9 | "template",
10 | "blade"
11 | ],
12 | "homepage": "https://github.com/windwalker-io/edge",
13 | "license": "MIT",
14 | "require": {
15 | "php": ">=8.2.0",
16 | "windwalker/data": "^4.0",
17 | "windwalker/utilities": "^4.0"
18 | },
19 | "require-dev": {
20 | "phpunit/phpunit": "^8.0||^9.0||^10.0",
21 | "windwalker/test": "^4.0",
22 | "windwalker/dom": "^4.0"
23 | },
24 | "suggest": {
25 | },
26 | "minimum-stability": "beta",
27 | "autoload": {
28 | "psr-4": {
29 | "Windwalker\\Edge\\": "src/"
30 | },
31 | "files": [
32 | "src/bootstrap.php"
33 | ]
34 | },
35 | "autoload-dev": {
36 | "psr-4": {
37 | "Windwalker\\Edge\\Test\\": "test/"
38 | }
39 | },
40 | "extra": {
41 | "branch-alias": {
42 | "dev-master": "4.x-dev"
43 | }
44 | },
45 | "config": {
46 | "platform": {
47 | "php": "8.2.0"
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/phpunit.ci.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | test
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Cache/EdgeArrayCache.php:
--------------------------------------------------------------------------------
1 | getCacheKey($path);
55 |
56 | return $this->data[$key] ?? '';
57 | }
58 |
59 | /**
60 | * store
61 | *
62 | * @param string $path
63 | * @param string $value
64 | *
65 | * @return void
66 | */
67 | public function store(string $path, string $value): void
68 | {
69 | $key = $this->getCacheKey($path);
70 |
71 | $this->data[$key] = $value;
72 | }
73 |
74 | /**
75 | * remove
76 | *
77 | * @param string $path
78 | *
79 | * @return void
80 | */
81 | public function remove(string $path): void
82 | {
83 | $key = $this->getCacheKey($path);
84 |
85 | unset($this->data[$key]);
86 | }
87 |
88 | /**
89 | * Method to get property Data
90 | *
91 | * @return array
92 | */
93 | public function getData(): array
94 | {
95 | return $this->data;
96 | }
97 |
98 | /**
99 | * Method to set property data
100 | *
101 | * @param array $data
102 | *
103 | * @return static Return self to support chaining.
104 | */
105 | public function setData(array $data): static
106 | {
107 | $this->data = $data;
108 |
109 | return $this;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/Cache/EdgeCacheInterface.php:
--------------------------------------------------------------------------------
1 | path = $path;
38 | }
39 |
40 | /**
41 | * isExpired
42 | *
43 | * @param string $path
44 | *
45 | * @return bool
46 | */
47 | public function isExpired(string $path): bool
48 | {
49 | $key = 'cache:' . $path;
50 |
51 | if (isset(static::$cacheStorage[$key])) {
52 | return static::$cacheStorage[$key];
53 | }
54 |
55 | $cachePath = $this->getCacheFile($this->getCacheKey($path));
56 |
57 | if (!is_file($cachePath)) {
58 | $expired = true;
59 | } else {
60 | $expired = filemtime($path) >= filemtime($cachePath);
61 | }
62 |
63 | if ($expired === false) {
64 | // Only cache if not expired
65 | static::$cacheStorage[$key] = false;
66 | }
67 |
68 | return $expired;
69 | }
70 |
71 | /**
72 | * getCacheKey
73 | *
74 | * @param string $path
75 | *
76 | * @return string
77 | */
78 | public function getCacheKey(string $path): string
79 | {
80 | if (static::$cacheStorage[$path] ?? null) {
81 | return static::$cacheStorage[$path];
82 | }
83 |
84 | $path = str_replace(['/', '\\'], '/', $path);
85 |
86 | $key = md5($path);
87 |
88 | if ($this->isDebug()) {
89 | $prefix = basename($path);
90 | $prefix = static::stripExtension($prefix);
91 | $prefix = static::stripExtension($prefix);
92 |
93 | $key = $prefix . '-' . $key . '.php';
94 | }
95 |
96 | return static::$cacheStorage[$path] = $key;
97 | }
98 |
99 | /**
100 | * getCacheFile
101 | *
102 | * @param string $key
103 | *
104 | * @return string
105 | */
106 | public function getCacheFile(string $key): string
107 | {
108 | return $this->path . '/~' . $key;
109 | }
110 |
111 | /**
112 | * load
113 | *
114 | * @param string $path
115 | *
116 | * @return string
117 | */
118 | public function load(string $path): string
119 | {
120 | return file_get_contents($this->getCacheFile($this->getCacheKey($path)));
121 | }
122 |
123 | /**
124 | * store
125 | *
126 | * @param string $path
127 | * @param string $value
128 | *
129 | * @return void
130 | */
131 | public function store(string $path, string $value): void
132 | {
133 | $value = self::replaceFirst(
134 | 'getCacheFile($this->getCacheKey($path));
140 |
141 | if (!is_dir(dirname($file))) {
142 | if (!mkdir($concurrentDirectory = dirname($file), 0755, true) && !is_dir($concurrentDirectory)) {
143 | throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
144 | }
145 | }
146 |
147 | file_put_contents($file, $value);
148 | }
149 |
150 | private static function replaceFirst(string $from, string $to, string $content): string
151 | {
152 | $from = '/' . preg_quote($from, '/') . '/';
153 |
154 | return preg_replace($from, $to, $content, 1);
155 | }
156 |
157 | /**
158 | * Remove an item from the cache by its unique key
159 | *
160 | * @param string $path The path to remove.
161 | *
162 | * @return void
163 | */
164 | public function remove(string $path): void
165 | {
166 | @unlink($this->getCacheFile($this->getCacheKey($path)));
167 | }
168 |
169 | /**
170 | * @return string
171 | */
172 | public function getPath(): string
173 | {
174 | return $this->path;
175 | }
176 |
177 | /**
178 | * @param string $path
179 | *
180 | * @return static Return self to support chaining.
181 | */
182 | public function setPath(string $path): static
183 | {
184 | $this->path = $path;
185 |
186 | return $this;
187 | }
188 |
189 | /**
190 | * @return bool
191 | */
192 | public function isDebug(): bool
193 | {
194 | return $this->debug;
195 | }
196 |
197 | /**
198 | * @param bool $debug
199 | *
200 | * @return static Return self to support chaining.
201 | */
202 | public function setDebug(bool $debug): static
203 | {
204 | $this->debug = $debug;
205 |
206 | return $this;
207 | }
208 |
209 | public static function stripExtension(string $file): string
210 | {
211 | return preg_replace('#\.[^.]*$#', '', $file);
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/Cache/EdgeStorageCache.php:
--------------------------------------------------------------------------------
1 | cache = $cache;
25 | }
26 |
27 | /**
28 | * @inheritDoc
29 | */
30 | public function isExpired(string $path): bool
31 | {
32 | return $this->cache->has($this->getCacheKey($path));
33 | }
34 |
35 | /**
36 | * @inheritDoc
37 | */
38 | public function getCacheKey(string $path): string
39 | {
40 | return md5($path);
41 | }
42 |
43 | /**
44 | * @inheritDoc
45 | */
46 | public function load(string $path): string
47 | {
48 | return $this->cache->get($this->getCacheKey($path));
49 | }
50 |
51 | /**
52 | * @inheritDoc
53 | */
54 | public function store(string $path, string $value): void
55 | {
56 | $this->cache->save($this->getCacheKey($path), $value);
57 | }
58 |
59 | /**
60 | * @inheritDoc
61 | */
62 | public function remove(string $path): void
63 | {
64 | $this->cache->remove($this->getCacheKey($path));
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileClassTrait.php:
--------------------------------------------------------------------------------
1 | \"";
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileCommentTrait.php:
--------------------------------------------------------------------------------
1 | contentTags[0], $this->contentTags[1]);
22 |
23 | return preg_replace($pattern, '', $value);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileComponentTrait.php:
--------------------------------------------------------------------------------
1 | stripParentheses($expression), 3) + ['', '', ''];
36 |
37 | $component = trim($component, '\'"');
38 |
39 | $uid = static::newComponentUid($component);
40 |
41 | if (Str::contains($component, '::class') || class_exists($component)) {
42 | return static::compileClassComponentOpening($component, $name, $data, $uid);
43 | }
44 |
45 | return "startComponent{$expression}; ?>";
46 | }
47 |
48 | /**
49 | * Get a new component hash for a component name.
50 | *
51 | * @return string
52 | * @throws \Exception
53 | */
54 | public static function newComponentUid(): string
55 | {
56 | static::$componentUidStack[] = $hash = uid();
57 |
58 | return $hash;
59 | }
60 |
61 | /**
62 | * Compile a class component opening.
63 | *
64 | * @param string $component
65 | * @param string $name
66 | * @param string $data
67 | * @param string $uid
68 | *
69 | * @return string
70 | */
71 | public static function compileClassComponentOpening(string $component, string $name, string $data, string $uid)
72 | {
73 | if (class_exists($component)) {
74 | $component = Str::ensureLeft($component, '\\');
75 | }
76 |
77 | $component = Str::ensureRight($component, '::class');
78 |
79 | return implode(
80 | "\n",
81 | [
82 | '',
83 | 'make(' . $component . ', ' . ($data ?: '[]') . '); ?>',
84 | 'withName(' . $name . '); ?>',
85 | 'shouldRender()): ?>',
86 | 'startComponent($component->resolveView(), $component->data()); ?>',
87 | ]
88 | );
89 | }
90 |
91 | /**
92 | * Compile the end-component statements into valid PHP.
93 | *
94 | * @return string
95 | */
96 | protected function compileEndComponent(): string
97 | {
98 | $uid = array_pop(static::$componentUidStack);
99 |
100 | return implode(
101 | "\n",
102 | [
103 | '',
104 | '',
105 | '',
106 | '',
107 | 'renderComponent(); ?>',
108 | ]
109 | );
110 | }
111 |
112 | /**
113 | * Compile the end-component statements into valid PHP.
114 | *
115 | * @return string
116 | */
117 | public function compileEndComponentClass(): string
118 | {
119 | return $this->compileEndComponent() . "\n" .
120 | implode(
121 | "\n",
122 | [
123 | '',
124 | ]
125 | );
126 | }
127 |
128 | /**
129 | * Compile the prop statement into valid PHP.
130 | *
131 | * @param string $expression
132 | *
133 | * @return string
134 | */
135 | protected function compileProps(string $expression): string
136 | {
137 | return "exceptProps{$expression}; ?>
138 | \$__value) {
139 | \$\$__key = \$\$__key ?? \$__value;
140 | } ?>
141 |
142 | \$__value) {
143 | if (array_key_exists(\$__key, \$__defined_vars)) unset(\$\$__key);
144 | } ?>
145 | ";
146 | }
147 |
148 | /**
149 | * Compile the slot statements into valid PHP.
150 | *
151 | * @param string $expression
152 | *
153 | * @return string
154 | */
155 | protected function compileSlot(string $expression): string
156 | {
157 | $expression = $this->stripParentheses($expression);
158 | $expr = Arr::explodeAndClear(',', $expression);
159 |
160 | $slots = ';';
161 |
162 | if (
163 | count($expr) <= 1
164 | && strtolower($expr[0] ?? '') !== 'null'
165 | ) {
166 | $slots = "(function (...\$__scope) use (\$__edge, \$__data) { extract(\$__data);";
167 | }
168 |
169 | return "slot({$expression})$slots ?>";
170 | }
171 |
172 | protected function compileScope(string $expression): string
173 | {
174 | $expression = $this->stripParentheses($expression);
175 |
176 | $expr = Arr::explodeAndClear(',', $expression);
177 |
178 | $extract = '';
179 |
180 | // Use: @scope(['foo' => $foo])
181 | if (count($expr) === 1 && str_starts_with($expr[0], '[')) {
182 | $extract = "{$expr[0]} = \$__scope; ";
183 | }
184 |
185 | // Use: @scope($foo, $bar)
186 | if (count($expr) > 0) {
187 | $destruct = [];
188 |
189 | foreach ($expr as $var) {
190 | $varName = Str::removeLeft($var, '$', 'ascii');
191 |
192 | $destruct[] = "'$varName' => $var";
193 | }
194 |
195 | $extract = '[' . implode(', ', $destruct) . '] = $__scope; ';
196 | }
197 |
198 | return "";
199 | }
200 |
201 | /**
202 | * Compile the end-slot statements into valid PHP.
203 | *
204 | * @return string
205 | */
206 | protected function compileEndSlot(): string
207 | {
208 | return 'endSlot(); ?>';
209 | }
210 |
211 | /**
212 | * Sanitize the given component attribute value.
213 | *
214 | * @param mixed $value
215 | *
216 | * @return mixed
217 | */
218 | public static function sanitizeComponentAttribute(mixed $value): mixed
219 | {
220 | // todo: Must escape stringable
221 | return is_string($value)
222 | // || (is_object($value) && !$value instanceof ComponentAttributes && method_exists($value, '__toString'))
223 | ? e($value)
224 | : $value;
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileConditional.php:
--------------------------------------------------------------------------------
1 | ";
31 | }
32 |
33 | /**
34 | * Compile the else-if statements into valid PHP.
35 | *
36 | * @param string $expression
37 | *
38 | * @return string
39 | */
40 | protected function compileElseif(string $expression): string
41 | {
42 | return "";
43 | }
44 |
45 | /**
46 | * Compile the unless statements into valid PHP.
47 | *
48 | * @param string $expression
49 | *
50 | * @return string
51 | */
52 | protected function compileUnless(string $expression): string
53 | {
54 | return "";
55 | }
56 |
57 | /**
58 | * Compile the end unless statements into valid PHP.
59 | *
60 | * @param string $expression
61 | *
62 | * @return string
63 | */
64 | protected function compileEndunless(string $expression): string
65 | {
66 | return '';
67 | }
68 |
69 | /**
70 | * Compile the else statements into valid PHP.
71 | *
72 | * @param string $expression
73 | *
74 | * @return string
75 | */
76 | protected function compileElse(string $expression): string
77 | {
78 | return '';
79 | }
80 |
81 | /**
82 | * Compile the end-if statements into valid PHP.
83 | *
84 | * @param string $expression
85 | *
86 | * @return string
87 | */
88 | protected function compileEndif(string $expression): string
89 | {
90 | return '';
91 | }
92 |
93 | /**
94 | * Compile the switch statements into valid PHP.
95 | *
96 | * @param string $expression
97 | *
98 | * @return string
99 | */
100 | protected function compileSwitch(string $expression): string
101 | {
102 | $this->firstCaseInSwitch = true;
103 |
104 | return "firstCaseInSwitch) {
117 | $this->firstCaseInSwitch = false;
118 |
119 | return "case {$expression}: ?>";
120 | }
121 |
122 | return "";
123 | }
124 |
125 | /**
126 | * Compile the default statements in switch case into valid PHP.
127 | *
128 | * @return string
129 | */
130 | protected function compileDefault(): string
131 | {
132 | return '';
133 | }
134 |
135 | /**
136 | * Compile the end switch statements into valid PHP.
137 | *
138 | * @return string
139 | */
140 | protected function compileEndSwitch(): string
141 | {
142 | return '';
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileEchoTrait.php:
--------------------------------------------------------------------------------
1 | getEchoMethods() as $method => $length) {
22 | $value = $this->$method($value);
23 | }
24 |
25 | return $value;
26 | }
27 |
28 | /**
29 | * Compile the "raw" echo statements.
30 | *
31 | * @param string $value
32 | *
33 | * @return string
34 | */
35 | protected function compileRawEchos(string $value): string
36 | {
37 | $pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->rawTags[0], $this->rawTags[1]);
38 |
39 | $callback = function ($matches) {
40 | $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
41 |
42 | return $matches[1] ? substr(
43 | $matches[0],
44 | 1
45 | ) : 'compileEchoDefaults($matches[2]) . '; ?>' . $whitespace;
46 | };
47 |
48 | return preg_replace_callback($pattern, $callback, $value);
49 | }
50 |
51 | /**
52 | * Compile the "regular" echo statements.
53 | *
54 | * @param string $value
55 | *
56 | * @return string
57 | */
58 | protected function compileRegularEchos(string $value): string
59 | {
60 | $pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->contentTags[0], $this->contentTags[1]);
61 |
62 | $callback = function ($matches) {
63 | $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
64 |
65 | $wrapped = sprintf($this->echoFormat, $this->compileEchoDefaults($matches[2]));
66 |
67 | return $matches[1] ? substr($matches[0], 1) : '' . $whitespace;
68 | };
69 |
70 | return preg_replace_callback($pattern, $callback, $value);
71 | }
72 |
73 | /**
74 | * Compile the escaped echo statements.
75 | *
76 | * @param string $value
77 | *
78 | * @return string
79 | */
80 | protected function compileEscapedEchos(string $value): string
81 | {
82 | $pattern = sprintf('/(@)?%s\s*(.+?)\s*%s(\r?\n)?/s', $this->escapedTags[0], $this->escapedTags[1]);
83 |
84 | $callback = function ($matches) {
85 | $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
86 |
87 | return $matches[1]
88 | ? $matches[0]
89 | : 'compileEchoDefaults(
90 | $matches[2]
91 | ) . '); ?>' . $whitespace;
92 | };
93 |
94 | return preg_replace_callback($pattern, $callback, $value);
95 | }
96 |
97 | /**
98 | * Compile the default values for the echo statement.
99 | *
100 | * @param string $value
101 | *
102 | * @return string
103 | */
104 | protected function compileEchoDefaults(string $value): string
105 | {
106 | return preg_replace('/^(?=\$)(.+?)(?:\s+or\s+)(.+?)$/s', 'isset($1) ? $1 : $2', $value);
107 | }
108 |
109 | /**
110 | * Get the echo methods in the proper order for compilation.
111 | *
112 | * @return array
113 | */
114 | protected function getEchoMethods(): array
115 | {
116 | $methods = [
117 | 'compileRawEchos' => strlen(stripcslashes($this->rawTags[0])),
118 | 'compileEscapedEchos' => strlen(stripcslashes($this->escapedTags[0])),
119 | 'compileRegularEchos' => strlen(stripcslashes($this->contentTags[0])),
120 | ];
121 |
122 | uksort(
123 | $methods,
124 | static function ($method1, $method2) use ($methods) {
125 | // Ensure the longest tags are processed first
126 | if ($methods[$method1] > $methods[$method2]) {
127 | return -1;
128 | }
129 |
130 | if ($methods[$method1] < $methods[$method2]) {
131 | return 1;
132 | }
133 |
134 | // Otherwise give preference to raw tags (assuming they've overridden)
135 | if ($method1 === 'compileRawEchos') {
136 | return -1;
137 | }
138 |
139 | if ($method2 === 'compileRawEchos') {
140 | return 1;
141 | }
142 |
143 | if ($method1 === 'compileEscapedEchos') {
144 | return -1;
145 | }
146 |
147 | if ($method2 === 'compileEscapedEchos') {
148 | return 1;
149 | }
150 |
151 | return null;
152 | }
153 | );
154 |
155 | return $methods;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileIncludeTrait.php:
--------------------------------------------------------------------------------
1 | renderEach{$expression}; ?>";
22 | }
23 |
24 | /**
25 | * Compile the include statements into valid PHP.
26 | *
27 | * @param string $expression
28 | *
29 | * @return string
30 | */
31 | protected function compileInclude(string $expression): string
32 | {
33 | $expression = $this->stripParentheses($expression);
34 |
35 | // phpcs:disable
36 | return "render($expression, \$__edge->except(get_defined_vars(), ['__data', '__path'])); ?>";
37 | // phpcs:enable
38 | }
39 |
40 | /**
41 | * Compile the include statements into valid PHP.
42 | *
43 | * @param string $expression
44 | *
45 | * @return string
46 | */
47 | protected function compileIncludeIf(string $expression): string
48 | {
49 | $expression = $this->stripParentheses($expression);
50 |
51 | // phpcs:disable
52 | return "exists($expression)) echo \$__edge->render($expression, \$__edge->except(get_defined_vars(), ['__data', '__path'])); ?>";
53 | // phpcs:enable
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileJsonTrait.php:
--------------------------------------------------------------------------------
1 | stripParentheses($expression));
28 |
29 | $options = isset($parts[1]) ? trim($parts[1]) : $this->jsonOptions;
30 |
31 | $depth = isset($parts[2]) ? trim($parts[2]) : 512;
32 |
33 | return "";
34 | }
35 |
36 | /**
37 | * Compile the JSON statement into valid PHP.
38 | *
39 | * @param string $expression
40 | * @return string
41 | */
42 | protected function compileJs(string $expression): string
43 | {
44 | $expression = $this->stripParentheses($expression);
45 |
46 | return "";
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileLayoutTrait.php:
--------------------------------------------------------------------------------
1 | stripParentheses($expression);
31 |
32 | // phpcs:disable
33 | $data = "render($expression, \$__edge->except(get_defined_vars(), ['__data', '__path'])); ?>";
34 | // phpcs:enable
35 |
36 | $this->footer[] = $data;
37 |
38 | return '';
39 | }
40 |
41 | /**
42 | * Compile the yield statements into valid PHP.
43 | *
44 | * @param string $expression
45 | *
46 | * @return string
47 | */
48 | protected function compileYield(string $expression): string
49 | {
50 | return "yieldContent{$expression}; ?>";
51 | }
52 |
53 | /**
54 | * Compile the show statements into valid PHP.
55 | *
56 | * @param string $expression
57 | *
58 | * @return string
59 | */
60 | protected function compileShow(string $expression): string
61 | {
62 | return 'yieldSection(); ?>';
63 | }
64 |
65 | /**
66 | * Compile the section statements into valid PHP.
67 | *
68 | * @param string $expression
69 | *
70 | * @return string
71 | */
72 | protected function compileSection(string $expression): string
73 | {
74 | $this->lastSection = trim($expression, "()'\" ");
75 |
76 | $params = explode(',', $expression);
77 |
78 | if (count($params) >= 2) {
79 | return "startSection{$expression}; ?>";
80 | }
81 |
82 | return "startSection{$expression}; if (\$__edge->hasParent{$expression}): ?>";
83 | }
84 |
85 | /**
86 | * Compile the append statements into valid PHP.
87 | *
88 | * @param string $expression
89 | *
90 | * @return string
91 | */
92 | protected function compileAppend(string $expression): string
93 | {
94 | return 'appendSection(); ?>';
95 | }
96 |
97 | /**
98 | * Compile the end-section statements into valid PHP.
99 | *
100 | * @param string $expression
101 | *
102 | * @return string
103 | */
104 | protected function compileEndsection(string $expression): string
105 | {
106 | return 'stopSection(); ?>';
107 | }
108 |
109 | /**
110 | * Compile the stop statements into valid PHP.
111 | *
112 | * @param string $expression
113 | *
114 | * @return string
115 | */
116 | protected function compileStop(string $expression): string
117 | {
118 | return 'stopSection(); ?>';
119 | }
120 |
121 | /**
122 | * Compile the overwrite statements into valid PHP.
123 | *
124 | * @param string $expression
125 | *
126 | * @return string
127 | */
128 | protected function compileOverwrite(string $expression): string
129 | {
130 | return 'stopSection(true); ?>';
131 | }
132 |
133 | /**
134 | * Compile the has section statements into valid PHP.
135 | *
136 | * @param string $expression
137 | *
138 | * @return string
139 | */
140 | protected function compileHasSection(string $expression): string
141 | {
142 | return "yieldContent{$expression}))): ?>";
143 | }
144 |
145 | /**
146 | * Replace the @parent directive to a placeholder.
147 | *
148 | * @return string
149 | */
150 | protected function compileParent(): string
151 | {
152 | return Edge::parentPlaceholder($this->lastSection ?: '');
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileLoopTrait.php:
--------------------------------------------------------------------------------
1 | ";
22 | }
23 |
24 | /**
25 | * Compile the foreach statements into valid PHP.
26 | *
27 | * @param string $expression
28 | *
29 | * @return string
30 | */
31 | protected function compileForeach(string $expression): string
32 | {
33 | return "";
34 | }
35 |
36 | /**
37 | * Compile the break statements into valid PHP.
38 | *
39 | * @param string $expression
40 | *
41 | * @return string
42 | */
43 | protected function compileBreak(string $expression): string
44 | {
45 | return $expression ? "" : '';
46 | }
47 |
48 | /**
49 | * Compile the continue statements into valid PHP.
50 | *
51 | * @param string $expression
52 | *
53 | * @return string
54 | */
55 | protected function compileContinue(string $expression): string
56 | {
57 | return $expression ? "" : '';
58 | }
59 |
60 | /**
61 | * Compile the forelse statements into valid PHP.
62 | *
63 | * @param string $expression
64 | *
65 | * @return string
66 | */
67 | protected function compileForelse(string $expression): string
68 | {
69 | $empty = '$__empty_' . ++$this->forelseCounter;
70 |
71 | return "";
72 | }
73 |
74 | /**
75 | * Compile the forelse statements into valid PHP.
76 | *
77 | * @param string $expression
78 | *
79 | * @return string
80 | */
81 | protected function compileEmpty(string $expression): string
82 | {
83 | $empty = '$__empty_' . $this->forelseCounter--;
84 |
85 | return "";
86 | }
87 |
88 | /**
89 | * Compile the while statements into valid PHP.
90 | *
91 | * @param string $expression
92 | *
93 | * @return string
94 | */
95 | protected function compileWhile(string $expression): string
96 | {
97 | return "";
98 | }
99 |
100 | /**
101 | * Compile the end-while statements into valid PHP.
102 | *
103 | * @param string $expression
104 | *
105 | * @return string
106 | */
107 | protected function compileEndwhile(string $expression): string
108 | {
109 | return '';
110 | }
111 |
112 | /**
113 | * Compile the end-for statements into valid PHP.
114 | *
115 | * @param string $expression
116 | *
117 | * @return string
118 | */
119 | protected function compileEndfor(string $expression): string
120 | {
121 | return '';
122 | }
123 |
124 | /**
125 | * Compile the end-for-each statements into valid PHP.
126 | *
127 | * @param string $expression
128 | *
129 | * @return string
130 | */
131 | protected function compileEndforeach(string $expression): string
132 | {
133 | return '';
134 | }
135 |
136 | /**
137 | * Compile the end-for-else statements into valid PHP.
138 | *
139 | * @param string $expression
140 | *
141 | * @return string
142 | */
143 | protected function compileEndforelse(string $expression): string
144 | {
145 | return '';
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileRawPhpTrait.php:
--------------------------------------------------------------------------------
1 | " : '';
34 | }
35 |
36 | /**
37 | * Compile the unset statements into valid PHP.
38 | *
39 | * @param string $expression
40 | *
41 | * @return string
42 | */
43 | protected function compileUnset(string $expression): string
44 | {
45 | return "";
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileStackTrait.php:
--------------------------------------------------------------------------------
1 | yieldPushContent{$expression}; ?>";
22 | }
23 |
24 | /**
25 | * Compile the push statements into valid PHP.
26 | *
27 | * @param string $expression
28 | *
29 | * @return string
30 | */
31 | protected function compilePush(string $expression): string
32 | {
33 | return "startPush{$expression}; ?>";
34 | }
35 |
36 | /**
37 | * Compile the endpush statements into valid PHP.
38 | *
39 | * @param string $expression
40 | *
41 | * @return string
42 | */
43 | protected function compileEndpush(string $expression): string
44 | {
45 | return 'stopPush(); ?>';
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Compiler/EdgeCompiler.php:
--------------------------------------------------------------------------------
1 | escape(%s)';
90 |
91 | /**
92 | * Array of footer lines to be added to template.
93 | *
94 | * @var array
95 | */
96 | protected array $footer = [];
97 |
98 | /**
99 | * Placeholder to temporary mark the position of verbatim blocks.
100 | *
101 | * @var string
102 | */
103 | protected string $verbatimPlaceholder = '@__verbatim__@';
104 |
105 | /**
106 | * Array to temporary store the verbatim blocks found in the template.
107 | *
108 | * @var array
109 | */
110 | protected array $verbatimBlocks = [];
111 |
112 | /**
113 | * Counter to keep track of nested forelse statements.
114 | *
115 | * @var int
116 | */
117 | protected int $forelseCounter = 0;
118 |
119 | /**
120 | * All of the available compiler functions.
121 | *
122 | * @var array
123 | */
124 | protected array $compilers = [
125 | 'Parsers',
126 | 'Statements',
127 | 'Comments',
128 | 'Echos',
129 | ];
130 |
131 | /**
132 | * compile
133 | *
134 | * @param string $value
135 | *
136 | * @return string
137 | */
138 | public function compile(string $value): string
139 | {
140 | $result = '';
141 |
142 | if (str_contains($value, '@verbatim')) {
143 | $value = $this->storeVerbatimBlocks($value);
144 | }
145 |
146 | $this->footer = [];
147 |
148 | // Here we will loop through all of the tokens returned by the Zend lexer and
149 | // parse each one into the corresponding valid PHP. We will then have this
150 | // template as the correctly rendered PHP that can be rendered natively.
151 | foreach (token_get_all($value) as $token) {
152 | $result .= is_array($token) ? $this->parseToken($token) : $token;
153 | }
154 |
155 | if (!empty($this->verbatimBlocks)) {
156 | $result = $this->restoreVerbatimBlocks($result);
157 | }
158 |
159 | // If there are any footer lines that need to get added to a template we will
160 | // add them here at the end of the template. This gets used mainly for the
161 | // template inheritance via the extends keyword that should be appended.
162 | if (count($this->footer) > 0) {
163 | $result = ltrim($result, PHP_EOL)
164 | . PHP_EOL . implode(PHP_EOL, array_reverse($this->footer));
165 | }
166 |
167 | return $result;
168 | }
169 |
170 | /**
171 | * Store the verbatim blocks and replace them with a temporary placeholder.
172 | *
173 | * @param string $value
174 | *
175 | * @return string
176 | */
177 | protected function storeVerbatimBlocks(string $value): string
178 | {
179 | return preg_replace_callback(
180 | '/(?verbatimBlocks[] = $matches[1];
183 |
184 | return $this->verbatimPlaceholder;
185 | },
186 | $value
187 | );
188 | }
189 |
190 | /**
191 | * Replace the raw placeholders with the original code stored in the raw blocks.
192 | *
193 | * @param string $result
194 | *
195 | * @return string
196 | */
197 | protected function restoreVerbatimBlocks(string $result): string
198 | {
199 | $result = preg_replace_callback(
200 | '/' . preg_quote($this->verbatimPlaceholder) . '/',
201 | function () {
202 | return array_shift($this->verbatimBlocks);
203 | },
204 | $result
205 | );
206 |
207 | $this->verbatimBlocks = [];
208 |
209 | return $result;
210 | }
211 |
212 | /**
213 | * Parse the tokens from the template.
214 | *
215 | * @param array $token
216 | *
217 | * @return string
218 | */
219 | protected function parseToken(array $token): string
220 | {
221 | [$id, $content] = $token;
222 |
223 | if ($id === T_INLINE_HTML) {
224 | foreach ($this->compilers as $type) {
225 | $content = $this->{"compile{$type}"}($content);
226 | }
227 | }
228 |
229 | return $content;
230 | }
231 |
232 | /**
233 | * compileParsers
234 | *
235 | * @param string $value
236 | *
237 | * @return string
238 | */
239 | protected function compileParsers(string $value): string
240 | {
241 | foreach ($this->parsers as $parser) {
242 | $value = $parser($value, $this);
243 | }
244 |
245 | return $value;
246 | }
247 |
248 | /**
249 | * Compile Blade statements that start with "@".
250 | *
251 | * @param string $value
252 | *
253 | * @return mixed
254 | */
255 | protected function compileStatements(string $value): mixed
256 | {
257 | return preg_replace_callback(
258 | '/\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?/x',
259 | [$this, 'compileStatement'],
260 | $value
261 | );
262 | }
263 |
264 | /**
265 | * compileStatement
266 | *
267 | * @param array $match
268 | *
269 | * @return string
270 | */
271 | protected function compileStatement(array $match): string
272 | {
273 | if (str_contains($match[1], '@')) {
274 | $match[0] = isset($match[3]) ? $match[1] . $match[3] : $match[1];
275 | } elseif (isset($this->directives[$match[1]])) {
276 | $match[0] = call_user_func($this->directives[$match[1]], $match[3] ?? '');
277 | } elseif (method_exists($this, $method = 'compile' . ucfirst($match[1]))) {
278 | $match[0] = $this->$method($match[3] ?? '');
279 | }
280 |
281 | return isset($match[3]) ? $match[0] : $match[0] . $match[2];
282 | }
283 |
284 | /**
285 | * Strip the parentheses from the given expression.
286 | *
287 | * @param string $expression
288 | *
289 | * @return string
290 | */
291 | protected function stripParentheses(string $expression): string
292 | {
293 | if (str_starts_with($expression, '(')) {
294 | $expression = substr($expression, 1, -1);
295 | }
296 |
297 | return $expression;
298 | }
299 |
300 | /**
301 | * Register a handler for custom directives.
302 | *
303 | * @param string $name
304 | * @param callable $handler
305 | *
306 | * @return EdgeCompiler
307 | */
308 | public function directive(string $name, callable $handler): static
309 | {
310 | $this->directives[$name] = $handler;
311 |
312 | return $this;
313 | }
314 |
315 | /**
316 | * Get the list of custom directives.
317 | *
318 | * @return array
319 | */
320 | public function getDirectives(): array
321 | {
322 | return $this->directives;
323 | }
324 |
325 | /**
326 | * Method to set property directives
327 | *
328 | * @param callable[] $directives
329 | *
330 | * @return static Return self to support chaining.
331 | */
332 | public function setDirectives(array $directives): static
333 | {
334 | $this->directives = $directives;
335 |
336 | return $this;
337 | }
338 |
339 | /**
340 | * parser
341 | *
342 | * @param callable $handler
343 | *
344 | * @return static
345 | */
346 | public function parser(callable $handler): static
347 | {
348 | $this->parsers[] = $handler;
349 |
350 | return $this;
351 | }
352 |
353 | /**
354 | * Method to set property parsers
355 | *
356 | * @param callable[] $parsers
357 | *
358 | * @return static Return self to support chaining.
359 | */
360 | public function setParsers(array $parsers): static
361 | {
362 | $this->parsers = $parsers;
363 |
364 | return $this;
365 | }
366 |
367 | /**
368 | * getParsers
369 | *
370 | * @return callable[]
371 | */
372 | public function getParsers(): array
373 | {
374 | return $this->parsers;
375 | }
376 |
377 | /**
378 | * Gets the raw tags used by the compiler.
379 | *
380 | * @return array
381 | */
382 | public function getRawTags(): array
383 | {
384 | return $this->rawTags;
385 | }
386 |
387 | /**
388 | * Sets the raw tags used for the compiler.
389 | *
390 | * @param string $openTag
391 | * @param string $closeTag
392 | *
393 | * @return void
394 | */
395 | public function setRawTags(string $openTag, string $closeTag): void
396 | {
397 | $this->rawTags = [preg_quote($openTag), preg_quote($closeTag)];
398 | }
399 |
400 | /**
401 | * Sets the content tags used for the compiler.
402 | *
403 | * @param string $openTag
404 | * @param string $closeTag
405 | * @param bool $escaped
406 | *
407 | * @return void
408 | */
409 | public function setContentTags(string $openTag, string $closeTag, bool $escaped = false)
410 | {
411 | $property = ($escaped === true) ? 'escapedTags' : 'contentTags';
412 |
413 | $this->{$property} = [preg_quote($openTag), preg_quote($closeTag)];
414 | }
415 |
416 | /**
417 | * Sets the escaped content tags used for the compiler.
418 | *
419 | * @param string $openTag
420 | * @param string $closeTag
421 | *
422 | * @return void
423 | */
424 | public function setEscapedContentTags(string $openTag, string $closeTag): void
425 | {
426 | $this->setContentTags($openTag, $closeTag, true);
427 | }
428 |
429 | /**
430 | * Gets the content tags used for the compiler.
431 | *
432 | * @return array
433 | */
434 | public function getContentTags(): array
435 | {
436 | return $this->getTags();
437 | }
438 |
439 | /**
440 | * Gets the escaped content tags used for the compiler.
441 | *
442 | * @return array
443 | */
444 | public function getEscapedContentTags(): array
445 | {
446 | return $this->getTags(true);
447 | }
448 |
449 | /**
450 | * Gets the tags used for the compiler.
451 | *
452 | * @param bool $escaped
453 | *
454 | * @return array
455 | */
456 | protected function getTags(bool $escaped = false): array
457 | {
458 | $tags = $escaped ? $this->escapedTags : $this->contentTags;
459 |
460 | return array_map('stripcslashes', $tags);
461 | }
462 |
463 | /**
464 | * Set the echo format to be used by the compiler.
465 | *
466 | * @param string $format
467 | *
468 | * @return void
469 | */
470 | public function setEchoFormat(string $format): void
471 | {
472 | $this->echoFormat = $format;
473 | }
474 | }
475 |
--------------------------------------------------------------------------------
/src/Compiler/EdgeCompilerInterface.php:
--------------------------------------------------------------------------------
1 | render();
69 | }
70 |
71 | /**
72 | * Get the data that should be supplied to the view.
73 | *
74 | * @return array
75 | * @author Brent Roose
76 | *
77 | * @author Freek Van der Herten
78 | */
79 | public function data(): array
80 | {
81 | $this->attributes ??= $this->newAttributeBag();
82 |
83 | return array_merge($this->extractPublicProperties(), $this->extractPublicMethods());
84 | }
85 |
86 | /**
87 | * Extract the public properties for the component.
88 | *
89 | * @return array
90 | */
91 | protected function extractPublicProperties(): array
92 | {
93 | $class = get_class($this);
94 |
95 | if (!isset(static::$propertyCache[$class])) {
96 | $reflection = new ReflectionClass($this);
97 |
98 | static::$propertyCache[$class] = collect($reflection->getProperties(ReflectionProperty::IS_PUBLIC))
99 | ->reject(
100 | function (ReflectionProperty $property) {
101 | return $property->isStatic();
102 | }
103 | )
104 | ->reject(
105 | function (ReflectionProperty $property) {
106 | return $this->shouldIgnore($property->getName());
107 | }
108 | )
109 | ->map(
110 | function (ReflectionProperty $property) {
111 | return $property->getName();
112 | }
113 | )->dump();
114 | }
115 |
116 | $values = [];
117 |
118 | foreach (static::$propertyCache[$class] as $property) {
119 | $values[$property] = $this->{$property};
120 | }
121 |
122 | return $values;
123 | }
124 |
125 | /**
126 | * Extract the public methods for the component.
127 | *
128 | * @return array
129 | * @throws \ReflectionException
130 | */
131 | protected function extractPublicMethods(): array
132 | {
133 | $class = get_class($this);
134 |
135 | if (!isset(static::$methodCache[$class])) {
136 | $reflection = new ReflectionClass($this);
137 |
138 | static::$methodCache[$class] = collect($reflection->getMethods(ReflectionMethod::IS_PUBLIC))
139 | ->reject(
140 | function (ReflectionMethod $method) {
141 | return $this->shouldIgnore($method->getName());
142 | }
143 | )
144 | ->map(
145 | function (ReflectionMethod $method) {
146 | return $method->getName();
147 | }
148 | );
149 | }
150 |
151 | $values = [];
152 |
153 | foreach (static::$methodCache[$class] as $method) {
154 | $values[$method] = $this->createVariableFromMethod(new ReflectionMethod($this, $method));
155 | }
156 |
157 | return $values;
158 | }
159 |
160 | /**
161 | * Create a callable variable from the given method.
162 | *
163 | * @param ReflectionMethod $method
164 | *
165 | * @return mixed
166 | */
167 | protected function createVariableFromMethod(ReflectionMethod $method)
168 | {
169 | return $method->getNumberOfParameters() === 0
170 | ? $this->createInvokableVariable($method->getName())
171 | : Closure::fromCallable([$this, $method->getName()]);
172 | }
173 |
174 | /**
175 | * Create an invokable, toStringable variable for the given component method.
176 | *
177 | * @param string $method
178 | *
179 | * @return InvokableComponentVariable
180 | */
181 | protected function createInvokableVariable(string $method): InvokableComponentVariable
182 | {
183 | return new InvokableComponentVariable(
184 | function () use ($method) {
185 | return $this->{$method}();
186 | }
187 | );
188 | }
189 |
190 | /**
191 | * Determine if the given property / method should be ignored.
192 | *
193 | * @param string $name
194 | *
195 | * @return bool
196 | */
197 | protected function shouldIgnore(string $name): bool
198 | {
199 | return str_starts_with($name, '__') || in_array($name, $this->ignoredMethods(), true);
200 | }
201 |
202 | /**
203 | * Get the methods that should be ignored.
204 | *
205 | * @return array
206 | */
207 | protected function ignoredMethods(): array
208 | {
209 | return array_merge(
210 | [
211 | 'data',
212 | 'render',
213 | 'resolveView',
214 | 'shouldRender',
215 | 'view',
216 | 'withName',
217 | 'withAttributes',
218 | ],
219 | $this->except
220 | );
221 | }
222 |
223 | /**
224 | * Set the component alias name.
225 | *
226 | * @param string $name
227 | *
228 | * @return $this
229 | */
230 | public function withName(string $name): static
231 | {
232 | $this->componentName = $name;
233 |
234 | return $this;
235 | }
236 |
237 | /**
238 | * Set the extra attributes that the component should make available.
239 | *
240 | * @param array $attributes
241 | *
242 | * @return static
243 | *
244 | * @deprecated 5.0 Use new method to merge attributes.
245 | */
246 | public function withAttributes(array $attributes, array|ComponentAttributes $binding = []): static
247 | {
248 | // if ($binding instanceof ComponentAttributes) {
249 | // $binding = $binding->getAttributes();
250 | // }
251 |
252 | $this->attributes = $this->attributes ?: $this->newAttributeBag();
253 |
254 | $this->attributes->setAttributes(
255 | [
256 | ...$this->attributes->getAttributes(),
257 | ...$attributes
258 | ]
259 | );
260 |
261 | return $this;
262 | }
263 |
264 | /**
265 | * Get a new attribute bag instance.
266 | *
267 | * @param array $attributes
268 | *
269 | * @return ComponentAttributes
270 | */
271 | protected function newAttributeBag(array $attributes = []): ComponentAttributes
272 | {
273 | return new ComponentAttributes($attributes);
274 | }
275 |
276 | /**
277 | * Determine if the component should be rendered.
278 | *
279 | * @return bool
280 | */
281 | public function shouldRender(): bool
282 | {
283 | return true;
284 | }
285 | }
286 |
--------------------------------------------------------------------------------
/src/Component/AnonymousComponent.php:
--------------------------------------------------------------------------------
1 | view;
27 | }
28 |
29 | /**
30 | * Get the data that should be supplied to the view.
31 | *
32 | * @return array
33 | */
34 | public function data(): array
35 | {
36 | $this->attributes ??= $this->newAttributeBag();
37 |
38 | $attributes = $this->data['attributes'] ?? [];
39 |
40 | if ($attributes instanceof ComponentAttributes) {
41 | $attributes = $attributes->getAttributes();
42 | }
43 |
44 | return array_merge(
45 | $attributes,
46 | $this->attributes->getAttributes(),
47 | $this->data,
48 | ['attributes' => $this->attributes]
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Component/AppendableAttributeValue.php:
--------------------------------------------------------------------------------
1 | value = $value;
24 | }
25 |
26 | /**
27 | * Get the string value.
28 | *
29 | * @return string
30 | */
31 | public function __toString(): string
32 | {
33 | return (string) $this->value;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Component/ComponentAttributes.php:
--------------------------------------------------------------------------------
1 | attributes = $attributes;
52 | }
53 |
54 | /**
55 | * Get the first attribute's value.
56 | *
57 | * @param mixed $default
58 | *
59 | * @return mixed
60 | */
61 | public function first(mixed $default = null): mixed
62 | {
63 | return $this->getIterator()->current() ?? value($default);
64 | }
65 |
66 | /**
67 | * Get a given attribute from the attribute array.
68 | *
69 | * @param string $key
70 | * @param mixed $default
71 | *
72 | * @return mixed
73 | */
74 | public function get(string $key, mixed $default = null): mixed
75 | {
76 | return $this->attributes[$key] ?? value($default);
77 | }
78 |
79 | /**
80 | * Determine if a given attribute exists in the attribute array.
81 | *
82 | * @param string $key
83 | *
84 | * @return bool
85 | */
86 | public function has(string $key): bool
87 | {
88 | return array_key_exists($key, $this->attributes);
89 | }
90 |
91 | /**
92 | * Only include the given attribute from the attribute array.
93 | *
94 | * @param mixed $keys
95 | *
96 | * @return static
97 | */
98 | public function only(mixed $keys): static
99 | {
100 | if (is_null($keys)) {
101 | $values = $this->attributes;
102 | } else {
103 | $keys = (array) $keys;
104 |
105 | $values = Arr::only($this->attributes, $keys);
106 | }
107 |
108 | return new static($values);
109 | }
110 |
111 | /**
112 | * Exclude the given attribute from the attribute array.
113 | *
114 | * @param mixed|array $keys
115 | *
116 | * @return static
117 | */
118 | public function except(mixed $keys): static
119 | {
120 | if (is_null($keys)) {
121 | $values = $this->attributes;
122 | } else {
123 | $keys = (array) $keys;
124 |
125 | $values = Arr::except($this->attributes, $keys);
126 | }
127 |
128 | return new static($values);
129 | }
130 |
131 | /**
132 | * Filter the attributes, returning a bag of attributes that pass the filter.
133 | *
134 | * @param callable $callback
135 | *
136 | * @return static
137 | */
138 | public function filter(callable $callback): static
139 | {
140 | return new static(collect($this->attributes)->filter($callback)->all());
141 | }
142 |
143 | /**
144 | * Return a bag of attributes that have keys starting with the given value / pattern.
145 | *
146 | * @param string $string
147 | *
148 | * @return static
149 | */
150 | public function whereStartsWith(string $string): static
151 | {
152 | return $this->filter(
153 | function ($value, $key) use ($string) {
154 | return str_starts_with($key, $string);
155 | }
156 | );
157 | }
158 |
159 | /**
160 | * Return a bag of attributes with keys that do not start with the given value / pattern.
161 | *
162 | * @param string $string
163 | *
164 | * @return static
165 | */
166 | public function whereDoesntStartWith(string $string): static
167 | {
168 | return $this->filter(
169 | function ($value, $key) use ($string) {
170 | return !str_starts_with($key, $string);
171 | }
172 | );
173 | }
174 |
175 | /**
176 | * Return a bag of attributes that have keys starting with the given value / pattern.
177 | *
178 | * @param string $string
179 | *
180 | * @return static
181 | */
182 | public function thatStartWith(string $string): static
183 | {
184 | return $this->whereStartsWith($string);
185 | }
186 |
187 | /**
188 | * Exclude the given attribute from the attribute array.
189 | *
190 | * @param mixed|array $keys
191 | *
192 | * @return static
193 | */
194 | public function exceptProps(mixed $keys): static
195 | {
196 | $props = [];
197 |
198 | foreach ($keys as $key => $defaultValue) {
199 | $key = is_numeric($key) ? $defaultValue : $key;
200 |
201 | $props[] = $key;
202 | $props[] = static::toKebabCase($key);
203 | }
204 |
205 | return $this->except($props);
206 | }
207 |
208 | public function props(...$keys): Collection
209 | {
210 | $ignore = [];
211 | $props = [];
212 |
213 | foreach ($keys as $key => $defaultValue) {
214 | if (is_numeric($key)) {
215 | $key = $defaultValue;
216 | $defaultValue = null;
217 | }
218 |
219 | $ignore[] = $key;
220 | $ignore[] = $pascalKey = static::toKebabCase($key);
221 |
222 | $props[$pascalKey] = $this->attributes[$key] ?? $this->attributes[$pascalKey] ?? $defaultValue;
223 | }
224 |
225 | $this->attributes = Arr::except($this->attributes, $ignore);
226 |
227 | return collect($props);
228 | }
229 |
230 | public function propsValues(...$keys): array
231 | {
232 | return $this->props(...$keys)->values()->dump();
233 | }
234 |
235 | /**
236 | * Conditionally merge classes into the attribute bag.
237 | *
238 | * @param mixed|array $classList
239 | *
240 | * @return static
241 | */
242 | public function class(mixed $classList): static
243 | {
244 | $classList = (array) $classList;
245 |
246 | $classes = [];
247 |
248 | foreach ($classList as $class => $constraint) {
249 | if (is_numeric($class)) {
250 | $classes[] = $constraint;
251 | } elseif ($constraint) {
252 | $classes[] = $class;
253 | }
254 | }
255 |
256 | return $this->merge(['class' => implode(' ', $classes)]);
257 | }
258 |
259 | /**
260 | * Merge additional attributes / values into the attribute bag.
261 | *
262 | * @param array $attributeDefaults
263 | * @param bool $escape
264 | *
265 | * @return static
266 | */
267 | public function merge(array $attributeDefaults = [], bool $escape = true)
268 | {
269 | $attributeDefaults = array_map(
270 | function ($value) use ($escape) {
271 | return $this->shouldEscapeAttributeValue($escape, $value)
272 | ? e($value)
273 | : $value;
274 | },
275 | $attributeDefaults
276 | );
277 |
278 | [$appendableAttributes, $nonAppendableAttributes] = collect($this->attributes)
279 | ->partition(
280 | function ($value, $key) use ($attributeDefaults) {
281 | return $key === 'class' || $key === 'style' ||
282 | (isset($attributeDefaults[$key]) &&
283 | $attributeDefaults[$key] instanceof AppendableAttributeValue);
284 | },
285 | true
286 | );
287 |
288 | $attributes = $appendableAttributes->mapWithKeys(
289 | function ($value, $key) use ($attributeDefaults, $escape) {
290 | $defaultsValue = isset($attributeDefaults[$key])
291 | && $attributeDefaults[$key] instanceof AppendableAttributeValue
292 | ? $this->resolveAppendableAttributeDefault($attributeDefaults, $key, $escape)
293 | : ($attributeDefaults[$key] ?? '');
294 |
295 | return [$key => implode(' ', array_unique(array_filter([$defaultsValue, $value])))];
296 | }
297 | )->merge($nonAppendableAttributes)->dump();
298 |
299 | return new static(array_merge($attributeDefaults, $attributes));
300 | }
301 |
302 | /**
303 | * Determine if the specific attribute value should be escaped.
304 | *
305 | * @param bool $escape
306 | * @param mixed $value
307 | *
308 | * @return bool
309 | */
310 | protected function shouldEscapeAttributeValue($escape, $value)
311 | {
312 | if (!$escape) {
313 | return false;
314 | }
315 |
316 | return !is_object($value) &&
317 | !is_null($value) &&
318 | !is_bool($value);
319 | }
320 |
321 | /**
322 | * Create a new appendable attribute value.
323 | *
324 | * @param mixed $value
325 | *
326 | * @return AppendableAttributeValue
327 | */
328 | public function prepends(mixed $value): AppendableAttributeValue
329 | {
330 | return new AppendableAttributeValue($value);
331 | }
332 |
333 | /**
334 | * Resolve an appendable attribute value default value.
335 | *
336 | * @param array $attributeDefaults
337 | * @param string $key
338 | * @param bool $escape
339 | *
340 | * @return mixed
341 | */
342 | protected function resolveAppendableAttributeDefault(array $attributeDefaults, string $key, bool $escape): mixed
343 | {
344 | if ($this->shouldEscapeAttributeValue($escape, $value = $attributeDefaults[$key]->value)) {
345 | $value = e($value);
346 | }
347 |
348 | return $value;
349 | }
350 |
351 | /**
352 | * Get all of the raw attributes.
353 | *
354 | * @return array
355 | */
356 | public function getAttributes(): array
357 | {
358 | return $this->attributes;
359 | }
360 |
361 | /**
362 | * Set the underlying attributes.
363 | *
364 | * @param array $attributes
365 | *
366 | * @return void
367 | */
368 | public function setAttributes(array $attributes): void
369 | {
370 | if (
371 | isset($attributes['attributes']) &&
372 | (
373 | $attributes['attributes'] instanceof self || is_array($attributes['attributes'])
374 | )
375 | ) {
376 | $parentBag = static::wrap($attributes['attributes']);
377 |
378 | unset($attributes['attributes']);
379 |
380 | $attributes = $parentBag->merge($attributes, $escape = false)->getAttributes();
381 | }
382 |
383 | $this->attributes = $attributes;
384 | }
385 |
386 | /**
387 | * Get content as a string of HTML.
388 | *
389 | * @return string
390 | */
391 | public function toHtml(): string
392 | {
393 | return (string) $this;
394 | }
395 |
396 | /**
397 | * Merge additional attributes / values into the attribute bag.
398 | *
399 | * @param array $attributeDefaults
400 | *
401 | * @return string
402 | */
403 | public function __invoke(array $attributeDefaults = []): string
404 | {
405 | return (string) $this->merge($attributeDefaults);
406 | }
407 |
408 | /**
409 | * Determine if the given offset exists.
410 | *
411 | * @param string $offset
412 | *
413 | * @return bool
414 | */
415 | public function offsetExists($offset): bool
416 | {
417 | return isset($this->attributes[$offset]);
418 | }
419 |
420 | /**
421 | * Get the value at the given offset.
422 | *
423 | * @param string $offset
424 | *
425 | * @return mixed
426 | */
427 | public function offsetGet($offset): mixed
428 | {
429 | return $this->get($offset);
430 | }
431 |
432 | /**
433 | * Set the value at a given offset.
434 | *
435 | * @param string $offset
436 | * @param mixed $value
437 | *
438 | * @return void
439 | */
440 | public function offsetSet($offset, $value): void
441 | {
442 | $this->attributes[$offset] = $value;
443 | }
444 |
445 | /**
446 | * Remove the value at the given offset.
447 | *
448 | * @param string $offset
449 | *
450 | * @return void
451 | */
452 | public function offsetUnset($offset): void
453 | {
454 | unset($this->attributes[$offset]);
455 | }
456 |
457 | /**
458 | * Get an iterator for the items.
459 | *
460 | * @return ArrayIterator
461 | */
462 | public function getIterator(): ArrayIterator
463 | {
464 | return new ArrayIterator($this->attributes);
465 | }
466 |
467 | /**
468 | * Implode the attributes into a single HTML ready string.
469 | *
470 | * @return string
471 | */
472 | public function __toString(): string
473 | {
474 | $string = '';
475 |
476 | foreach ($this->attributes as $key => $value) {
477 | if ($value === false || is_null($value)) {
478 | continue;
479 | }
480 |
481 | $string .= ' ' . $key;
482 |
483 | if ($value !== true) {
484 | // Do not print array / object data into attributes
485 | if (is_array($value)) {
486 | $value = 'Array()';
487 | }
488 |
489 | if (is_object($value)) {
490 | $value = sprintf('[Object %s]', get_class($value));
491 | }
492 |
493 | $string .= '="' . e(trim((string) $value)) . '"';
494 | }
495 | }
496 |
497 | return trim($string);
498 | }
499 |
500 | public static function toKebabCase(string $str): string
501 | {
502 | return static::$cacheStorage[$str] ??= StrNormalize::toKebabCase($str);
503 | }
504 | }
505 |
--------------------------------------------------------------------------------
/src/Component/ComponentExtension.php:
--------------------------------------------------------------------------------
1 | registerComponent('dynamic-component', DynamicComponent::class);
25 | }
26 |
27 | /**
28 | * @inheritDoc
29 | */
30 | public function getName(): string
31 | {
32 | return 'edge-component';
33 | }
34 |
35 | /**
36 | * @inheritDoc
37 | */
38 | public function getParsers(): array
39 | {
40 | return [
41 | [$this, 'parseComponents'],
42 | ];
43 | }
44 |
45 | public function parseComponents(string $content): string
46 | {
47 | $compiler = new ComponentTagCompiler($this->edge, $this->components);
48 |
49 | $content = $compiler->compile($content);
50 |
51 | return $content;
52 | }
53 |
54 | /**
55 | * @return array
56 | */
57 | public function getComponents(): array
58 | {
59 | return $this->components;
60 | }
61 |
62 | /**
63 | * @param array $components
64 | *
65 | * @return static Return self to support chaining.
66 | */
67 | public function setComponents(array $components): static
68 | {
69 | $this->components = $components;
70 |
71 | return $this;
72 | }
73 |
74 | public function registerComponent(string $name, string $class): static
75 | {
76 | $this->components[$name] = $class;
77 |
78 | return $this;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Component/ComponentTagCompiler.php:
--------------------------------------------------------------------------------
1 | compileSlots($value);
54 |
55 | return $this->compileTags($value);
56 | }
57 |
58 | /**
59 | * Compile the tags within the given string.
60 | *
61 | * @param string $value
62 | *
63 | * @return string
64 | *
65 | * @throws InvalidArgumentException
66 | */
67 | public function compileTags(string $value): string
68 | {
69 | $value = $this->compileSelfClosingTags($value);
70 | $value = $this->compileOpeningTags($value);
71 | $value = $this->compileClosingTags($value);
72 |
73 | return $value;
74 | }
75 |
76 | /**
77 | * Compile the opening tags within the given string.
78 | *
79 | * @param string $value
80 | *
81 | * @return string
82 | *
83 | * @throws InvalidArgumentException
84 | */
85 | protected function compileOpeningTags(string $value): string
86 | {
87 | $pattern = "/
88 | <
89 | \s*
90 | x[-\:]([\w\-\:\.]*)
91 | (?
92 | (?:
93 | \s+
94 | (?:
95 | (?:
96 | \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
97 | )
98 | |
99 | (?:
100 | [\w\-:.@]+
101 | (
102 | =
103 | (?:
104 | \\\"[^\\\"]*\\\"
105 | |
106 | \'[^\']*\'
107 | |
108 | [^\'\\\"=<>]+
109 | )
110 | )?
111 | )
112 | )
113 | )*
114 | \s*
115 | )
116 | (?
118 | /x";
119 |
120 | return preg_replace_callback(
121 | $pattern,
122 | function (array $matches) {
123 | $this->boundAttributes = [];
124 |
125 | $attributes = $this->getAttributesFromAttributeString($matches['attributes']);
126 |
127 | return $this->componentString($matches[1], $attributes);
128 | },
129 | $value
130 | );
131 | }
132 |
133 | /**
134 | * Compile the self-closing tags within the given string.
135 | *
136 | * @param string $value
137 | *
138 | * @return string
139 | *
140 | * @throws InvalidArgumentException
141 | */
142 | protected function compileSelfClosingTags(string $value): string
143 | {
144 | $pattern = "/
145 | <
146 | \s*
147 | x[-\:]([\w\-\:\.]*)
148 | \s*
149 | (?
150 | (?:
151 | \s+
152 | (?:
153 | (?:
154 | \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
155 | )
156 | |
157 | (?:
158 | [\w\-:.@]+
159 | (
160 | =
161 | (?:
162 | \\\"[^\\\"]*\\\"
163 | |
164 | \'[^\']*\'
165 | |
166 | [^\'\\\"=<>]+
167 | )
168 | )?
169 | )
170 | )
171 | )*
172 | \s*
173 | )
174 | \/>
175 | /x";
176 |
177 | return preg_replace_callback(
178 | $pattern,
179 | function (array $matches) {
180 | $this->boundAttributes = [];
181 |
182 | $attributes = $this->getAttributesFromAttributeString($matches['attributes']);
183 |
184 | return $this->componentString($matches[1], $attributes) . "\n@endComponentClass";
185 | },
186 | $value
187 | );
188 | }
189 |
190 | /**
191 | * Compile the Blade component string for the given component and attributes.
192 | *
193 | * @param string $component
194 | * @param array $attributes
195 | *
196 | * @return string
197 | *
198 | * @throws InvalidArgumentException
199 | */
200 | protected function componentString(string $component, array $attributes): string
201 | {
202 | $class = $this->componentClass($component);
203 |
204 | [$data, $attributes] = $this->partitionDataAndAttributes($class, $attributes);
205 |
206 | $data = $data->mapWithKeys(
207 | function ($value, $key) {
208 | return [StrNormalize::toCamelCase($key) => $value];
209 | }
210 | );
211 |
212 | // If the component doesn't exists as a class we'll assume it's a class-less
213 | // component and pass the component as a view parameter to the data so it
214 | // can be accessed within the component and we can render out the view.
215 | if (!class_exists($class)) {
216 | $parameters = [
217 | 'view' => "'$class'",
218 | 'data' => '[' . $this->attributesToString($data->dump(), escapeBound: false) . ']',
219 | ];
220 |
221 | $class = AnonymousComponent::class;
222 | } elseif (is_a($class, DynamicComponent::class, true)) {
223 | $data['edge'] = raw('$__edge');
224 |
225 | $parameters = $data->dump();
226 | } else {
227 | $parameters = $data->dump();
228 | }
229 |
230 | // Bind attributes
231 | if (isset($attributes[''])) {
232 | $attributes['attributes'] = $attributes[''];
233 |
234 | unset($attributes['']);
235 | }
236 |
237 | return "@component('{$class}', '{$component}', [" .
238 | $this->attributesToString(
239 | $parameters,
240 | false
241 | ) . '])
242 | withAttributes([' . $this->attributesToString(
243 | $attributes->dump(),
244 | is_a($class, DynamicComponent::class, true)
245 | ) . "]); ?>";
246 | }
247 |
248 | /**
249 | * Get the component class for a given component alias.
250 | *
251 | * @param string $component
252 | *
253 | * @return string
254 | *
255 | * @throws InvalidArgumentException
256 | */
257 | public function componentClass(string $component): string
258 | {
259 | $loader = $this->edge->getLoader();
260 |
261 | if (isset($this->components[$component])) {
262 | $class = $this->components[$component];
263 |
264 | if (class_exists($class)) {
265 | return $class;
266 | }
267 |
268 | // Component Alias
269 | if ($loader->has($class)) {
270 | return $class;
271 | }
272 |
273 | if ($loader->has($component)) {
274 | return $component;
275 | }
276 |
277 | throw new InvalidArgumentException(
278 | "Unable to locate class or view [{$class}] for component [{$component}]."
279 | );
280 | }
281 |
282 | if ($loader->has('@' . $component)) {
283 | return '@' . $component;
284 | }
285 |
286 | if ($loader->has($component)) {
287 | return $component;
288 | }
289 |
290 | throw new InvalidArgumentException(
291 | "Unable to locate a class or view for component [{$component}]."
292 | );
293 | }
294 |
295 | /**
296 | * Partition the data and extra attributes from the given array of attributes.
297 | *
298 | * @param string $class
299 | * @param array $attributes
300 | *
301 | * @return array
302 | */
303 | public function partitionDataAndAttributes(string $class, array $attributes)
304 | {
305 | // If the class doesn't exists, we'll assume it's a class-less component and
306 | // return all of the attributes as both data and attributes since we have
307 | // now way to partition them. The user can exclude attributes manually.
308 | if (!class_exists($class)) {
309 | return [collect($attributes), collect($attributes)];
310 | }
311 |
312 | $properties = (new ReflectionClass($class))->getProperties();
313 | $props = [];
314 |
315 | foreach ($properties as $property) {
316 | $attrs = $property->getAttributes(Prop::class, \ReflectionAttribute::IS_INSTANCEOF);
317 |
318 | if ($attrs) {
319 | $propName = StrNormalize::toKebabCase($property->getName());
320 |
321 | if (array_key_exists($propName, $attributes)) {
322 | $props[$property->getName()] = $attributes[$propName];
323 |
324 | unset($attributes[$propName]);
325 | }
326 | }
327 | }
328 |
329 | return [collect($props), collect($attributes)];
330 | }
331 |
332 | /**
333 | * Compile the closing tags within the given string.
334 | *
335 | * @param string $value
336 | *
337 | * @return string
338 | */
339 | protected function compileClosingTags(string $value): string
340 | {
341 | return preg_replace("/<\/\s*x[-\:][\w\-\:\.]*\s*>/", ' @endComponentClass', $value);
342 | }
343 |
344 | /**
345 | * Compile the slot tags within the given string.
346 | *
347 | * @param string $value
348 | *
349 | * @return string
350 | */
351 | public function compileSlots(string $value): string
352 | {
353 | $value = preg_replace_callback(
354 | '/<\s*x[\-\:]slot\s+(:?)name=(?(\"[^\"]+\"|\\\'[^\\\']+\\\'|[^\s>]+))\s*>/',
355 | function ($matches) {
356 | $name = $this->stripQuotes($matches['name']);
357 |
358 | if ($name === 'default') {
359 | $name = 'slot';
360 | }
361 |
362 | if ($matches[1] !== ':') {
363 | $name = "'{$name}'";
364 | }
365 |
366 | return " @slot({$name}) ";
367 | },
368 | $value
369 | );
370 |
371 | return preg_replace('/<\/\s*x[\-\:]slot[^>]*>/', ' @endslot', $value);
372 | }
373 |
374 | /**
375 | * Get an array of attributes from the given attribute string.
376 | *
377 | * @param string $attributeString
378 | *
379 | * @return array
380 | */
381 | protected function getAttributesFromAttributeString(string $attributeString): array
382 | {
383 | $attributeString = $this->parseAttributeBag($attributeString);
384 |
385 | $attributeString = $this->parseBindAttributes($attributeString);
386 |
387 | $pattern = '/
388 | (?[\w\-:.@]+)
389 | (
390 | =
391 | (?
392 | (
393 | \"[^\"]+\"
394 | |
395 | \\\'[^\\\']+\\\'
396 | |
397 | [^\s>]+
398 | )
399 | )
400 | )?
401 | /x';
402 |
403 | if (!preg_match_all($pattern, $attributeString, $matches, PREG_SET_ORDER)) {
404 | return [];
405 | }
406 |
407 | return collect($matches)->mapWithKeys(
408 | function ($match) {
409 | $attribute = $match['attribute'];
410 | $value = $match['value'] ?? null;
411 |
412 | if (is_null($value)) {
413 | $value = 'true';
414 |
415 | $attribute = Str::ensureLeft($attribute, 'bind:');
416 | }
417 |
418 | $value = $this->stripQuotes($value);
419 |
420 | if (Str::startsWith($attribute, 'bind:')) {
421 | $attribute = Str::removeLeft($attribute, 'bind:', 'ascii');
422 |
423 | $this->boundAttributes[$attribute] = true;
424 | } else {
425 | $value = "'" . $this->compileAttributeEchos($value) . "'";
426 | }
427 |
428 | if (Str::startsWith($attribute, '::')) {
429 | $attribute = substr($attribute, 1);
430 | }
431 |
432 | return [$attribute => $value];
433 | }
434 | )->dump();
435 | }
436 |
437 | /**
438 | * Parse the attribute bag in a given attribute string into its fully-qualified syntax.
439 | *
440 | * @param string $attributeString
441 | *
442 | * @return string
443 | */
444 | protected function parseAttributeBag(string $attributeString)
445 | {
446 | $pattern = "/
447 | (?:^|\s+) # start of the string or whitespace between attributes
448 | \{\{\s*(\\\$attributes(?:[^}]+?(?edge->getCompiler()?->compileEchos($attributeString);
485 |
486 | $value = $this->escapeSingleQuotesOutsideOfPhpBlocks($value);
487 |
488 | $value = str_replace(
489 | [''],
490 | ['\'.', '.\''],
491 | $value
492 | );
493 |
494 | return $value;
495 | }
496 |
497 | /**
498 | * Escape the single quotes in the given string that are outside of PHP blocks.
499 | *
500 | * @param string $value
501 | *
502 | * @return string
503 | */
504 | protected function escapeSingleQuotesOutsideOfPhpBlocks(string $value): string
505 | {
506 | return (string) collect(token_get_all($value))
507 | ->map(
508 | function ($token) {
509 | if (!is_array($token)) {
510 | return $token;
511 | }
512 |
513 | return $token[0] === T_INLINE_HTML
514 | ? str_replace("'", "\\'", $token[1])
515 | : $token[1];
516 | }
517 | )
518 | ->implode('');
519 | }
520 |
521 | /**
522 | * Convert an array of attributes to a string.
523 | *
524 | * @param array $attributes
525 | * @param bool $escapeBound
526 | *
527 | * @return string
528 | */
529 | protected function attributesToString(array $attributes, bool $escapeBound = true): string
530 | {
531 | return (string) collect($attributes)
532 | ->walk(
533 | function (string &$value, string $attribute) use ($escapeBound) {
534 | if ($value instanceof RawWrapper) {
535 | $value = $value();
536 |
537 | return;
538 | }
539 |
540 | $value = $escapeBound
541 | && isset($this->boundAttributes[$attribute])
542 | && $value !== 'true'
543 | // phpcs:disable
544 | && !is_numeric($value)
545 | ? "'{$attribute}' => \Windwalker\Edge\Compiler\EdgeCompiler::sanitizeComponentAttribute({$value})"
546 | : "'{$attribute}' => {$value}";
547 | // phpcs:enable
548 | }
549 | )
550 | ->implode(',');
551 | }
552 |
553 | /**
554 | * Strip any quotes from the given string.
555 | *
556 | * @param string $value
557 | *
558 | * @return string
559 | */
560 | public function stripQuotes(string $value): string
561 | {
562 | return Str::startsWith($value, '"') || Str::startsWith($value, '\'')
563 | ? substr($value, 1, -1)
564 | : $value;
565 | }
566 | }
567 |
--------------------------------------------------------------------------------
/src/Component/DynamicComponent.php:
--------------------------------------------------------------------------------
1 | getAttributes())->mapWithKeys(function ($value, $key) { return [Windwalker\Utilities\StrNormalize::toCamelCase(str_replace([':', '.'], ' ', $key)) => $value]; })->dump(), EXTR_SKIP); ?>
50 | {{ props }}
51 |
52 | {{ slots }}
53 | {{ defaultSlot }}
54 |
55 | EOF;
56 |
57 | // phpcs:enable
58 |
59 | return function ($edge, $data) use ($template) {
60 | $bindings = $this->bindings($class = $this->classForComponent());
61 |
62 | return str_replace(
63 | [
64 | '{{ is }}',
65 | '{{ props }}',
66 | '{{ bindings }}',
67 | '{{ attributes }}',
68 | '{{ slots }}',
69 | '{{ defaultSlot }}',
70 | ],
71 | [
72 | $this->is,
73 | $this->compileProps($bindings),
74 | $this->compileBindings($bindings),
75 | class_exists($class) ? '{{ $attributes }}' : '',
76 | $this->compileSlots($data['__edge_slots']),
77 | '{!! $slot ?? "" !!}',
78 | ],
79 | $template
80 | );
81 | };
82 | }
83 |
84 | /**
85 | * Compile the @props directive for the component.
86 | *
87 | * @param array $bindings
88 | *
89 | * @return string
90 | */
91 | protected function compileProps(array $bindings): string
92 | {
93 | if (empty($bindings)) {
94 | return '';
95 | }
96 |
97 | return '@props([\'' .
98 | implode(
99 | '\',\'',
100 | collect($bindings)->map(
101 | function ($dataKey) {
102 | return StrNormalize::toCamelCase($dataKey);
103 | }
104 | )
105 | ->dump()
106 | ) . '\'])';
107 | }
108 |
109 | /**
110 | * Compile the bindings for the component.
111 | *
112 | * @param array $bindings
113 | *
114 | * @return string
115 | */
116 | protected function compileBindings(array $bindings): string
117 | {
118 | return (string) collect($bindings)
119 | ->map(
120 | function ($key) {
121 | return ':' . $key . '="$' . StrNormalize::toCamelCase(str_replace([':', '.'], ' ', $key)) . '"';
122 | }
123 | )
124 | ->implode(' ');
125 | }
126 |
127 | /**
128 | * Compile the slots for the component.
129 | *
130 | * @param array $slots
131 | *
132 | * @return string
133 | */
134 | protected function compileSlots(array $slots): string
135 | {
136 | return (string) collect($slots)
137 | ->walk(
138 | function (&$slot, $name) {
139 | $slot = $name === '__default' ? null : '{{ $' . $name . ' }}';
140 | }
141 | )
142 | ->filter()
143 | ->implode(PHP_EOL);
144 | }
145 |
146 | /**
147 | * Get the class for the current component.
148 | *
149 | * @return string
150 | */
151 | protected function classForComponent(): string
152 | {
153 | if (isset(static::$componentClasses[$this->is])) {
154 | return static::$componentClasses[$this->is];
155 | }
156 |
157 | return static::$componentClasses[$this->is] =
158 | $this->compiler()->componentClass($this->is);
159 | }
160 |
161 | /**
162 | * Get the names of the variables that should be bound to the component.
163 | *
164 | * @param string $class
165 | *
166 | * @return array
167 | */
168 | protected function bindings(string $class): array
169 | {
170 | [$data, $attributes] = $this->compiler()->partitionDataAndAttributes(
171 | $class,
172 | $this->attributes->getAttributes()
173 | );
174 |
175 | return array_keys($data->dump());
176 | }
177 |
178 | /**
179 | * Get an instance of the Blade tag compiler.
180 | *
181 | * @return ComponentTagCompiler
182 | */
183 | protected function compiler(): ComponentTagCompiler
184 | {
185 | return $this->compiler ??= $this->createCompiler();
186 | }
187 |
188 | protected function createCompiler(): ComponentTagCompiler
189 | {
190 | $extension = $this->edge->getExtension('edge-component');
191 |
192 | if (!$extension) {
193 | foreach ($this->edge->getExtensions() as $extension) {
194 | if ($extension instanceof ComponentExtension) {
195 | break;
196 | }
197 | }
198 | }
199 |
200 | if (!$extension) {
201 | throw new LogicException(
202 | sprintf(
203 | 'Extension: %s not found.',
204 | ComponentExtension::class
205 | )
206 | );
207 | }
208 |
209 | return new ComponentTagCompiler($this->edge, $extension->getComponents());
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/src/Component/InvokableComponentVariable.php:
--------------------------------------------------------------------------------
1 | __invoke()->{$key};
33 | }
34 |
35 | /**
36 | * Dynamically proxy method access to the variable.
37 | *
38 | * @param string $method
39 | * @param array $args
40 | *
41 | * @return mixed
42 | */
43 | public function __call(string $method, array $args)
44 | {
45 | return $this->__invoke()->{$method}(...$args);
46 | }
47 |
48 | /**
49 | * Resolve the variable.
50 | *
51 | * @return mixed
52 | */
53 | public function __invoke()
54 | {
55 | return ($this->callable)();
56 | }
57 |
58 | /**
59 | * Resolve the variable as a string.
60 | *
61 | * @return string
62 | */
63 | public function __toString(): string
64 | {
65 | return (string) $this->__invoke();
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Concern/ManageComponentTrait.php:
--------------------------------------------------------------------------------
1 | componentStack[] = $name;
59 | $this->componentData[$this->currentComponent()] = $data;
60 | $this->slots[$this->currentComponent()] = [];
61 | }
62 | }
63 |
64 | /**
65 | * Render the current component.
66 | *
67 | * @return string
68 | * @throws EdgeException
69 | */
70 | public function renderComponent(): string
71 | {
72 | $staticSlot = ob_get_clean();
73 | $slot = $this->slots[$this->currentComponent()]['__MAIN__'] ?? null;
74 |
75 | if (trim($staticSlot) !== '') {
76 | $slot = new SlotWrapper(
77 | function (...$args) use ($staticSlot) {
78 | echo $staticSlot;
79 | }
80 | );
81 | }
82 |
83 | $view = array_pop($this->componentStack);
84 |
85 | return $this->render($view, $this->componentData($slot));
86 | }
87 |
88 | /**
89 | * Get the data for the given component.
90 | *
91 | * @param callable|null $slot
92 | *
93 | * @return array
94 | */
95 | protected function componentData(?callable $slot): array
96 | {
97 | $slots = array_merge(
98 | [
99 | '__default' => $slot,
100 | ],
101 | $this->slots[count($this->componentStack)]
102 | );
103 |
104 | return array_merge(
105 | $this->componentData[count($this->componentStack)],
106 | ['slot' => $slot],
107 | $this->slots[count($this->componentStack)],
108 | ['__edge_slots' => $slots]
109 | );
110 | }
111 |
112 | /**
113 | * Start the slot rendering process.
114 | *
115 | * @param string|null $name
116 | * @param string|null $content
117 | *
118 | * @return Closure
119 | */
120 | public function slot(?string $name = null, ?string $content = null): Closure
121 | {
122 | $name ??= '__MAIN__';
123 |
124 | if ($content !== null) {
125 | $this->slots[$this->currentComponent()][$name] = new SlotWrapper(
126 | function () use ($content) {
127 | echo $content;
128 | }
129 | );
130 | }
131 |
132 | return function ($renderer) use ($name) {
133 | $this->slots[$this->currentComponent()][$name] = new SlotWrapper($renderer);
134 | $this->slotStack[$this->currentComponent()][] = $name;
135 | };
136 | }
137 |
138 | /**
139 | * Save the slot content for rendering.
140 | *
141 | * @return void
142 | */
143 | public function endSlot(): void
144 | {
145 | end($this->componentStack);
146 |
147 | $currentSlot = array_pop(
148 | $this->slotStack[$this->currentComponent()]
149 | );
150 | }
151 |
152 | /**
153 | * Get the index for the current component.
154 | *
155 | * @return int
156 | */
157 | protected function currentComponent(): int
158 | {
159 | return count($this->componentStack) - 1;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/Concern/ManageEventTrait.php:
--------------------------------------------------------------------------------
1 | sectionStack[] = $section;
48 | }
49 | } else {
50 | $this->hasParents[$section] = str_contains($content, static::parentPlaceholder($section));
51 |
52 | $this->extendSection($section, $this->escape($content));
53 | }
54 | }
55 |
56 | /**
57 | * Inject inline content into a section.
58 | *
59 | * @param string $section
60 | * @param mixed $content
61 | *
62 | * @return void
63 | */
64 | public function inject(string $section, mixed $content): void
65 | {
66 | $this->startSection($section, $content);
67 | }
68 |
69 | /**
70 | * Stop injecting content into a section and return its contents.
71 | *
72 | * @return string
73 | */
74 | public function yieldSection(): string
75 | {
76 | if (empty($this->sectionStack)) {
77 | return '';
78 | }
79 |
80 | return $this->yieldContent($this->stopSection());
81 | }
82 |
83 | /**
84 | * Stop injecting content into a section.
85 | *
86 | * @param bool $overwrite
87 | *
88 | * @return string
89 | * @throws InvalidArgumentException
90 | */
91 | public function stopSection(bool $overwrite = false): string
92 | {
93 | if (empty($this->sectionStack)) {
94 | throw new InvalidArgumentException('Cannot end a section without first starting one.');
95 | }
96 |
97 | $last = array_pop($this->sectionStack);
98 |
99 | if ($overwrite) {
100 | $this->sections[$last] = ob_get_clean();
101 | } else {
102 | $content = ob_get_clean();
103 |
104 | $this->hasParents[$last] = str_contains($content, static::parentPlaceholder($last));
105 |
106 | $this->extendSection($last, $content);
107 | }
108 |
109 | return $last;
110 | }
111 |
112 | /**
113 | * Stop injecting content into a section and append it.
114 | *
115 | * @return string
116 | * @throws InvalidArgumentException
117 | */
118 | public function appendSection(): string
119 | {
120 | if (empty($this->sectionStack)) {
121 | throw new InvalidArgumentException('Cannot end a section without first starting one.');
122 | }
123 |
124 | $last = array_pop($this->sectionStack);
125 |
126 | if (isset($this->sections[$last])) {
127 | $this->sections[$last] .= ob_get_clean();
128 | } else {
129 | $this->sections[$last] = ob_get_clean();
130 | }
131 |
132 | return $last;
133 | }
134 |
135 | /**
136 | * Append content to a given section.
137 | *
138 | * @param string $section
139 | * @param string $content
140 | *
141 | * @return void
142 | */
143 | protected function extendSection(string $section, string $content): void
144 | {
145 | if (isset($this->sections[$section])) {
146 | $content = str_replace(static::parentPlaceholder($section), $content, $this->sections[$section]);
147 | }
148 |
149 | $this->sections[$section] = $content;
150 | }
151 |
152 | /**
153 | * hasParent
154 | *
155 | * @param string $section
156 | *
157 | * @return bool
158 | *
159 | * @since __DEPLOY_VERSION__
160 | */
161 | public function hasParent(string $section): bool
162 | {
163 | return !empty($this->hasParents[$section]) || !isset($this->sections[$section]);
164 | }
165 |
166 | /**
167 | * Get the string contents of a section.
168 | *
169 | * @param string $section
170 | * @param string $default
171 | *
172 | * @return string
173 | */
174 | public function yieldContent(string $section, string $default = ''): string
175 | {
176 | $sectionContent = $default;
177 |
178 | if (isset($this->sections[$section])) {
179 | $sectionContent = $this->sections[$section];
180 | $sectionContent = str_replace('@@parent', '--parent--holder--', $sectionContent);
181 |
182 | return str_replace(
183 | '--parent--holder--',
184 | '@parent',
185 | str_replace(static::parentPlaceholder($section), $default, $sectionContent)
186 | );
187 | }
188 |
189 | return $sectionContent;
190 | }
191 |
192 | /**
193 | * Get the parent placeholder for the current request.
194 | *
195 | * @param string $section
196 | *
197 | * @return string
198 | */
199 | public static function parentPlaceholder(string $section = ''): string
200 | {
201 | return static::$parentPlaceholder[$section] ??= '##parent-placeholder-' . sha1($section) . '##';
202 | }
203 |
204 | /**
205 | * Flush all of the section contents.
206 | *
207 | * @return void
208 | */
209 | public function flushSections(): void
210 | {
211 | $this->renderCount = 0;
212 |
213 | $this->sections = [];
214 | $this->sectionStack = [];
215 | $this->hasParents = [];
216 |
217 | $this->pushes = [];
218 | $this->pushStack = [];
219 | }
220 |
221 | /**
222 | * Flush all of the section contents if done rendering.
223 | *
224 | * @return void
225 | */
226 | public function flushSectionsIfDoneRendering(): void
227 | {
228 | if ($this->doneRendering()) {
229 | $this->flushSections();
230 | }
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/src/Concern/ManageStackTrait.php:
--------------------------------------------------------------------------------
1 | pushStack[] = $section;
43 | }
44 | } else {
45 | $this->extendPush($section, $content);
46 | }
47 | }
48 |
49 | /**
50 | * Stop injecting content into a push section.
51 | *
52 | * @return string
53 | * @throws InvalidArgumentException
54 | */
55 | public function stopPush(): string
56 | {
57 | if (empty($this->pushStack)) {
58 | throw new InvalidArgumentException('Cannot end a section without first starting one.');
59 | }
60 |
61 | $last = array_pop($this->pushStack);
62 |
63 | $this->extendPush($last, ob_get_clean());
64 |
65 | return $last;
66 | }
67 |
68 | /**
69 | * Append content to a given push section.
70 | *
71 | * @param string $section
72 | * @param string $content
73 | *
74 | * @return void
75 | */
76 | protected function extendPush(string $section, string $content): void
77 | {
78 | if (!isset($this->pushes[$section])) {
79 | $this->pushes[$section] = [];
80 | }
81 |
82 | if (!isset($this->pushes[$section][$this->renderCount])) {
83 | $this->pushes[$section][$this->renderCount] = $content;
84 | } else {
85 | $this->pushes[$section][$this->renderCount] .= $content;
86 | }
87 | }
88 |
89 | /**
90 | * Get the string contents of a push section.
91 | *
92 | * @param string $section
93 | * @param string $default
94 | *
95 | * @return string
96 | */
97 | public function yieldPushContent(string $section, string $default = ''): string
98 | {
99 | if (!isset($this->pushes[$section])) {
100 | return $default;
101 | }
102 |
103 | return implode(array_reverse($this->pushes[$section]));
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/Edge.php:
--------------------------------------------------------------------------------
1 | loader = $loader ?: new EdgeStringLoader();
112 | $this->compiler = $compiler ?: new EdgeCompiler();
113 | $this->cache = $cache ?: new EdgeArrayCache();
114 | }
115 |
116 | /**
117 | * renderWithContext
118 | *
119 | * @param string $layout
120 | * @param array $data
121 | * @param object|null $context
122 | *
123 | * @return string
124 | *
125 | * @throws EdgeException
126 | */
127 | public function renderWithContext(string $layout, array $data = [], ?object $context = null): string
128 | {
129 | $this->context = $context;
130 |
131 | $result = $this->render($layout, $data);
132 |
133 | $this->context = null;
134 |
135 | return $result;
136 | }
137 |
138 | /**
139 | * render
140 | *
141 | * @param string|callable $__layout
142 | * @param array $__data
143 | * @param array $__more
144 | *
145 | * @return string
146 | * @throws EdgeException
147 | */
148 | public function render(string|Closure $__layout, array $__data = [], array $__more = []): string
149 | {
150 | $this->level++;
151 |
152 | // TODO: Aliases
153 |
154 | $this->incrementRender();
155 |
156 | if ($__layout instanceof Closure) {
157 | $__path = $__layout;
158 | } else {
159 | $__path = $this->cacheStorage['layout:' . $__layout]
160 | ??= (is_file($__layout) ? $__layout : $this->loader->find($__layout));
161 |
162 | if ($this->cache->isExpired($__path)) {
163 | $compiled = $this->compile($this->loader->load($__path));
164 |
165 | $this->cache->store($__path, $compiled);
166 |
167 | unset($compiler, $compiled);
168 | }
169 | }
170 |
171 | $__data = array_merge($this->getGlobals(true), $__more, $__data);
172 |
173 | unset($__data['__path'], $__data['__data']);
174 |
175 | $closure = $this->getRenderFunction($__data);
176 |
177 | if ($this->getContext()) {
178 | $closure = $closure->bindTo($this->getContext(), $this->getContext());
179 | }
180 |
181 | ob_start();
182 |
183 | try {
184 | $closure($__path);
185 | } catch (Throwable $e) {
186 | ob_end_clean();
187 |
188 | $this->level--;
189 |
190 | if ($this->level === 0) {
191 | $this->wrapException($e, $__path, $__layout);
192 | } else {
193 | throw $e;
194 | }
195 |
196 | return '';
197 | }
198 |
199 | $result = ltrim(ob_get_clean());
200 |
201 | $this->decrementRender();
202 |
203 | $this->flushSectionsIfDoneRendering();
204 |
205 | $this->level--;
206 |
207 | return $result;
208 | }
209 |
210 | public function compile(string|Closure $path): string
211 | {
212 | $compiler = $this->prepareExtensions(clone $this->compiler);
213 |
214 | if ($path instanceof Closure) {
215 | $path = $path($this);
216 | }
217 |
218 | return $compiler->compile($path);
219 | }
220 |
221 | protected function getRenderFunction(array $data): Closure
222 | {
223 | $__data = $data;
224 | $__edge = $this;
225 |
226 | return function ($__path) use ($__data, $__edge) {
227 | extract($__data, EXTR_OVERWRITE);
228 |
229 | if ($__path instanceof Closure) {
230 | try {
231 | eval(' ?>' . $__edge->compile($__path($this, $__data)) . 'wrapEvalException($e, $__edge->compile($__path($this, $__data)), $__path);
234 | }
235 |
236 | return;
237 | }
238 |
239 | if ($__edge->getCache() instanceof EdgeFileCache) {
240 | include $__edge->getCache()->getCacheFile($__edge->getCache()->getCacheKey($__path));
241 | } else {
242 | try {
243 | eval(' ?>' . $__edge->getCache()->load($__path) . 'wrapEvalException($e, $__edge->getCache()->load($__path), $__path);
246 | }
247 | }
248 | };
249 | }
250 |
251 | public function wrapEvalException(Throwable $e, string $code, string|Closure $path): void
252 | {
253 | $lines = explode("\n", $code);
254 | $count = \count($lines);
255 |
256 | $line = $e->getLine();
257 | $start = $line - 3;
258 |
259 | if ($start <= 0) {
260 | $start = 0;
261 | }
262 |
263 | $end = $line + 3;
264 |
265 | if ($end > $count) {
266 | $end = $count;
267 | }
268 |
269 | $view = '';
270 |
271 | foreach (range($start, $end) as $i) {
272 | $l = trim(($lines[$i] ?? ''), "\n\r");
273 |
274 | $view .= $l . "\n";
275 | }
276 |
277 | if ($path instanceof Closure) {
278 | $path = '\Closure()';
279 | }
280 |
281 | $msg = <<getMessage()}
283 |
284 | ERROR ON: $path (line: {$e->getLine()})
285 | ---------
286 | $view
287 | ---------
288 | TEXT;
289 | throw new EdgeException(
290 | $msg,
291 | $e->getCode(),
292 | $e->getFile(),
293 | $e->getLine(),
294 | $e
295 | );
296 | }
297 |
298 | /**
299 | * wrapException
300 | *
301 | * @param Throwable $e
302 | * @param string|Closure $path
303 | * @param string|Closure $layout
304 | *
305 | * @return void
306 | *
307 | * @throws EdgeException
308 | */
309 | protected function wrapException(Throwable $e, string|Closure $path, string|Closure $layout): void
310 | {
311 | $msg = $e->getMessage();
312 |
313 | $layout = $layout instanceof Closure ? Assert::describeValue($layout) : $layout;
314 | $path = $path instanceof Closure ? Assert::describeValue($path) : $path;
315 |
316 | $msg .= sprintf("\n\n| View layout: %s (%s)", $path, $layout);
317 |
318 | $cache = $this->getCache();
319 |
320 | if ($cache instanceof EdgeFileCache) {
321 | if (str_starts_with(realpath($cache->getPath()), $e->getFile())) {
322 | throw new EdgeException($msg, $e->getCode(), $path, $e->getLine(), $e);
323 | }
324 | }
325 |
326 | throw new EdgeException($msg, $e->getCode(), $e->getFile(), $e->getLine(), $e);
327 | }
328 |
329 | /**
330 | * @return object|null
331 | */
332 | public function getContext(): ?object
333 | {
334 | return $this->context;
335 | }
336 |
337 | /**
338 | * @param object|null $context
339 | *
340 | * @return static Return self to support chaining.
341 | */
342 | public function setContext(?object $context): static
343 | {
344 | $this->context = $context;
345 |
346 | return $this;
347 | }
348 |
349 | /**
350 | * Normalize a view name.
351 | *
352 | * @param string $name
353 | *
354 | * @return string
355 | */
356 | protected function normalizeName(string $name): string
357 | {
358 | // TODO: Handle namespace
359 |
360 | return str_replace('/', '.', $name);
361 | }
362 |
363 | /**
364 | * escape
365 | *
366 | * @param mixed $string
367 | *
368 | * @return string
369 | */
370 | public function escape(mixed $string): string
371 | {
372 | if ($string instanceof RawWrapper) {
373 | return $string->get();
374 | }
375 |
376 | return htmlspecialchars((string) $string, ENT_COMPAT, 'UTF-8');
377 | }
378 |
379 | /**
380 | * Get the rendered contents of a partial from a loop.
381 | *
382 | * @param string $layout
383 | * @param array $data
384 | * @param string $iterator
385 | * @param string $empty
386 | *
387 | * @return string
388 | * @throws EdgeException
389 | */
390 | public function renderEach(string $layout, array $data, string $iterator, string $empty = 'raw|'): string
391 | {
392 | $result = '';
393 |
394 | // If is actually data in the array, we will loop through the data and append
395 | // an instance of the partial view to the final result HTML passing in the
396 | // iterated value of this data array, allowing the views to access them.
397 | if (count($data) > 0) {
398 | foreach ($data as $key => $value) {
399 | $data = ['key' => $key, $iterator => $value];
400 |
401 | $result .= $this->render($layout, $data);
402 | }
403 | } elseif (str_starts_with($empty, 'raw|')) {
404 | // If there is no data in the array, we will render the contents of the empty
405 | // view. Alternatively, the "empty view" could be a raw string that begins
406 | // with "raw|" for convenience and to let this know that it is a string.
407 | $result = substr($empty, 4);
408 | } else {
409 | $result = $this->render($empty);
410 | }
411 |
412 | return $result;
413 | }
414 |
415 | /**
416 | * Increment the rendering counter.
417 | *
418 | * @return void
419 | */
420 | public function incrementRender(): void
421 | {
422 | $this->renderCount++;
423 | }
424 |
425 | /**
426 | * Decrement the rendering counter.
427 | *
428 | * @return void
429 | */
430 | public function decrementRender(): void
431 | {
432 | $this->renderCount--;
433 | }
434 |
435 | /**
436 | * Check if there are no active render operations.
437 | *
438 | * @return bool
439 | */
440 | public function doneRendering(): bool
441 | {
442 | return $this->renderCount === 0;
443 | }
444 |
445 | /**
446 | * prepareDirectives
447 | *
448 | * @param EdgeCompilerInterface $compiler
449 | *
450 | * @return EdgeCompilerInterface
451 | */
452 | public function prepareExtensions(EdgeCompilerInterface $compiler): EdgeCompilerInterface
453 | {
454 | foreach ($this->getExtensions() as $extension) {
455 | if ($extension instanceof DirectivesExtensionInterface) {
456 | foreach ($extension->getDirectives() as $name => $directive) {
457 | $compiler->directive($name, $directive);
458 | }
459 | }
460 |
461 | if ($extension instanceof ParsersExtensionInterface) {
462 | foreach ($extension->getParsers() as $parser) {
463 | $compiler->parser($parser);
464 | }
465 | }
466 | }
467 |
468 | return $compiler;
469 | }
470 |
471 | /**
472 | * arrayExcept
473 | *
474 | * @param array $array
475 | * @param array $fields
476 | *
477 | * @return array
478 | */
479 | public function except(array $array, array $fields): array
480 | {
481 | return Arr::except($array, $fields);
482 | }
483 |
484 | /**
485 | * Method to get property Globals
486 | *
487 | * @param bool $withExtensions
488 | *
489 | * @return array
490 | */
491 | public function getGlobals(bool $withExtensions = false): array
492 | {
493 | $globals = $this->globals;
494 |
495 | if ($withExtensions) {
496 | $globals = $this->once(
497 | 'global.with.extensions',
498 | function () use ($globals) {
499 | $values = [];
500 |
501 | foreach ($this->getExtensions() as $extension) {
502 | if ($extension instanceof GlobalVariablesExtensionInterface) {
503 | $values[] = $extension->getGlobals();
504 | }
505 | }
506 |
507 | $values[] = $globals;
508 |
509 | return array_merge(...$values);
510 | }
511 | );
512 | }
513 |
514 | return $globals;
515 | }
516 |
517 | /**
518 | * addGlobal
519 | *
520 | * @param string $name
521 | * @param mixed $value
522 | *
523 | * @return static
524 | */
525 | public function addGlobal(string $name, mixed $value): static
526 | {
527 | $this->globals[$name] = $value;
528 |
529 | return $this;
530 | }
531 |
532 | /**
533 | * removeGlobal
534 | *
535 | * @param string $name
536 | *
537 | * @return static
538 | */
539 | public function removeGlobal(string $name): static
540 | {
541 | unset($this->globals[$name]);
542 |
543 | return $this;
544 | }
545 |
546 | public function getGlobal(string $name, $default = null)
547 | {
548 | if (array_key_exists($name, $this->globals)) {
549 | return $this->globals[$name];
550 | }
551 |
552 | return $default;
553 | }
554 |
555 | /**
556 | * Method to set property globals
557 | *
558 | * @param array $globals
559 | *
560 | * @return static Return self to support chaining.
561 | */
562 | public function setGlobals(array $globals): array
563 | {
564 | $this->globals = $globals;
565 |
566 | return $this;
567 | }
568 |
569 | /**
570 | * Method to get property Compiler
571 | *
572 | * @return EdgeCompilerInterface
573 | */
574 | public function getCompiler(): EdgeCompilerInterface
575 | {
576 | return $this->compiler;
577 | }
578 |
579 | /**
580 | * Method to set property compiler
581 | *
582 | * @param EdgeCompilerInterface $compiler
583 | *
584 | * @return static Return self to support chaining.
585 | */
586 | public function setCompiler(EdgeCompilerInterface $compiler): static
587 | {
588 | $this->compiler = $compiler;
589 |
590 | return $this;
591 | }
592 |
593 | /**
594 | * Method to get property Loader
595 | *
596 | * @return EdgeLoaderInterface
597 | */
598 | public function getLoader(): EdgeLoaderInterface
599 | {
600 | return $this->loader;
601 | }
602 |
603 | /**
604 | * Method to set property loader
605 | *
606 | * @param EdgeLoaderInterface $loader
607 | *
608 | * @return static Return self to support chaining.
609 | */
610 | public function setLoader(EdgeLoaderInterface $loader): static
611 | {
612 | $this->loader = $loader;
613 |
614 | return $this;
615 | }
616 |
617 | /**
618 | * addExtension
619 | *
620 | * @param EdgeExtensionInterface $extension
621 | * @param string|null $name
622 | *
623 | * @return static
624 | */
625 | public function addExtension(EdgeExtensionInterface $extension, ?string $name = null): static
626 | {
627 | if (!$name) {
628 | $name = $extension->getName();
629 | }
630 |
631 | $this->extensions[$name] = $extension;
632 |
633 | return $this;
634 | }
635 |
636 | /**
637 | * removeExtension
638 | *
639 | * @param string $name
640 | *
641 | * @return static
642 | */
643 | public function removeExtension(string $name): static
644 | {
645 | if (array_key_exists($name, $this->extensions)) {
646 | unset($this->extensions[$name]);
647 | }
648 |
649 | return $this;
650 | }
651 |
652 | /**
653 | * hasExtension
654 | *
655 | * @param string $name
656 | *
657 | * @return bool
658 | */
659 | public function hasExtension(string $name): bool
660 | {
661 | return array_key_exists($name, $this->extensions) && $this->extensions[$name] instanceof EdgeExtensionInterface;
662 | }
663 |
664 | /**
665 | * getExtension
666 | *
667 | * @param string $name
668 | *
669 | * @return EdgeExtensionInterface
670 | */
671 | public function getExtension(string $name): ?EdgeExtensionInterface
672 | {
673 | if ($this->hasExtension($name)) {
674 | return $this->extensions[$name];
675 | }
676 |
677 | return null;
678 | }
679 |
680 | /**
681 | * Method to get property Extensions
682 | *
683 | * @return Extension\EdgeExtensionInterface[]
684 | */
685 | public function getExtensions(): array
686 | {
687 | return $this->extensions;
688 | }
689 |
690 | /**
691 | * Method to set property extensions
692 | *
693 | * @param Extension\EdgeExtensionInterface[] $extensions
694 | *
695 | * @return static Return self to support chaining.
696 | */
697 | public function setExtensions(array $extensions): static
698 | {
699 | $this->extensions = $extensions;
700 |
701 | return $this;
702 | }
703 |
704 | /**
705 | * Method to get property Cache
706 | *
707 | * @return EdgeCacheInterface
708 | */
709 | public function getCache(): EdgeCacheInterface
710 | {
711 | return $this->cache;
712 | }
713 |
714 | /**
715 | * Method to set property cache
716 | *
717 | * @param EdgeCacheInterface $cache
718 | *
719 | * @return static Return self to support chaining.
720 | */
721 | public function setCache(EdgeCacheInterface $cache): static
722 | {
723 | $this->cache = $cache;
724 |
725 | return $this;
726 | }
727 |
728 | public function make(string $class, array $props = []): object
729 | {
730 | if ($class === AnonymousComponent::class || $class === DynamicComponent::class) {
731 | $object = new $class();
732 |
733 | foreach ($props as $key => $value) {
734 | ReflectAccessor::setValue($object, $key, $value);
735 | }
736 |
737 | return $object;
738 | }
739 |
740 | $object = $this->getObjectBuilder()->createObject($class, ...$props);
741 |
742 | $ref = new \ReflectionObject($object);
743 |
744 | foreach ($ref->getProperties() as $property) {
745 | $name = $property->getName();
746 |
747 | if (
748 | array_key_exists($name, $props)
749 | && $property->getAttributes(Prop::class, \ReflectionAttribute::IS_INSTANCEOF)
750 | ) {
751 | ReflectAccessor::setValue($object, $name, $props[$name]);
752 | }
753 | }
754 |
755 | return $object;
756 | }
757 | }
758 |
--------------------------------------------------------------------------------
/src/EdgeHelper.php:
--------------------------------------------------------------------------------
1 | $constraint) {
22 | if (is_numeric($class)) {
23 | $classes[] = $constraint;
24 | } elseif ($constraint) {
25 | $classes[] = $class;
26 | }
27 | }
28 |
29 | return implode(' ', $classes);
30 | }
31 |
32 | public static function toJS(mixed $data): string
33 | {
34 | $base64 = base64_encode(json_encode($data));
35 |
36 | return "JSON.parse(atob('$base64'))";
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Exception/EdgeException.php:
--------------------------------------------------------------------------------
1 | file = $file;
33 | }
34 |
35 | if ($line) {
36 | $this->line = $line;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Exception/LayoutNotFoundException.php:
--------------------------------------------------------------------------------
1 | paths = $paths;
42 |
43 | if ($extensions !== null) {
44 | $this->extensions = $extensions;
45 | }
46 | }
47 |
48 | /**
49 | * find
50 | *
51 | * @param string $key
52 | *
53 | * @return string
54 | */
55 | public function find(string $key): string
56 | {
57 | $filePath = $this->doFind($key);
58 |
59 | if ($filePath === null) {
60 | $paths = implode(" |\n ", $this->paths);
61 |
62 | throw new LayoutNotFoundException('View file not found: ' . $key . ".\n (Paths: " . $paths . ')', 13001);
63 | }
64 |
65 | return $filePath;
66 | }
67 |
68 | public function doFind(string $key): ?string
69 | {
70 | $key = $this->normalize($key);
71 |
72 | $filePath = null;
73 |
74 | foreach ($this->paths as $path) {
75 | foreach ($this->extensions as $ext) {
76 | if (is_file($path . '/' . $key . '.' . $ext)) {
77 | $filePath = $path . '/' . $key . '.' . $ext;
78 |
79 | break 2;
80 | }
81 | }
82 | }
83 |
84 | return $filePath;
85 | }
86 |
87 | /**
88 | * loadFile
89 | *
90 | * @param string $path
91 | *
92 | * @return string
93 | */
94 | public function load(string $path): string
95 | {
96 | return file_get_contents($path);
97 | }
98 |
99 | /**
100 | * addPath
101 | *
102 | * @param string $path
103 | *
104 | * @return static
105 | */
106 | public function addPath(string $path): static
107 | {
108 | $this->paths[] = $path;
109 |
110 | return $this;
111 | }
112 |
113 | /**
114 | * prependPath
115 | *
116 | * @param string $path
117 | *
118 | * @return static
119 | */
120 | public function prependPath(string $path): static
121 | {
122 | array_unshift($this->paths, $path);
123 |
124 | return $this;
125 | }
126 |
127 | /**
128 | * normalize
129 | *
130 | * @param string $path
131 | *
132 | * @return string
133 | */
134 | protected function normalize(string $path): string
135 | {
136 | return str_replace('.', '/', $path);
137 | }
138 |
139 | /**
140 | * Method to get property Paths
141 | *
142 | * @return array
143 | */
144 | public function getPaths(): array
145 | {
146 | return $this->paths;
147 | }
148 |
149 | /**
150 | * Method to set property paths
151 | *
152 | * @param array $paths
153 | *
154 | * @return static Return self to support chaining.
155 | */
156 | public function setPaths(array $paths): static
157 | {
158 | $this->paths = $paths;
159 |
160 | return $this;
161 | }
162 |
163 | /**
164 | * addExtension
165 | *
166 | * @param string $name
167 | *
168 | * @return static
169 | */
170 | public function addFileExtension(string $name): static
171 | {
172 | $this->extensions[] = $name;
173 |
174 | return $this;
175 | }
176 |
177 | /**
178 | * Method to get property Extensions
179 | *
180 | * @return array
181 | */
182 | public function getExtensions(): array
183 | {
184 | return $this->extensions;
185 | }
186 |
187 | /**
188 | * Method to set property extensions
189 | *
190 | * @param array $extensions
191 | *
192 | * @return static Return self to support chaining.
193 | */
194 | public function setExtensions(array $extensions): static
195 | {
196 | $this->extensions = $extensions;
197 |
198 | return $this;
199 | }
200 |
201 | /**
202 | * @inheritDoc
203 | */
204 | public function has(string $key): bool
205 | {
206 | try {
207 | return $this->doFind($key) !== null;
208 | } catch (LayoutNotFoundException) {
209 | return false;
210 | }
211 | }
212 | }
213 |
--------------------------------------------------------------------------------
/src/Loader/EdgeLoaderInterface.php:
--------------------------------------------------------------------------------
1 | content = $content;
29 | }
30 |
31 | /**
32 | * load
33 | *
34 | * @param string $key
35 | *
36 | * @return string
37 | */
38 | public function find(string $key): string
39 | {
40 | return $key;
41 | }
42 |
43 | /**
44 | * loadFile
45 | *
46 | * @param string $path
47 | *
48 | * @return string
49 | */
50 | public function load(string $path): string
51 | {
52 | return $path ?: $this->content;
53 | }
54 |
55 | /**
56 | * Method to get property Content
57 | *
58 | * @return string
59 | */
60 | public function getContent(): string
61 | {
62 | return $this->content;
63 | }
64 |
65 | /**
66 | * Method to set property content
67 | *
68 | * @param string $content
69 | *
70 | * @return static Return self to support chaining.
71 | */
72 | public function setContent(string $content): static
73 | {
74 | $this->content = $content;
75 |
76 | return $this;
77 | }
78 |
79 | /**
80 | * @inheritDoc
81 | */
82 | public function has(string $key): bool
83 | {
84 | return true;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Provider/EdgeProvider.php:
--------------------------------------------------------------------------------
1 | extend(
36 | CompositeRenderer::class,
37 | function (CompositeRenderer $renderer, Container $container) {
38 | return $renderer->extend(
39 | function (RendererInterface $renderer, array $options) use ($container) {
40 | if ($renderer instanceof EdgeRenderer) {
41 | $renderer = $this->doExtends($container, $renderer, $options);
42 | }
43 |
44 | return $renderer;
45 | }
46 | );
47 | }
48 | );
49 | }
50 |
51 | protected function doExtends(
52 | Container $container,
53 | EdgeRenderer $renderer,
54 | array $options
55 | ): EdgeRenderer {
56 | return $renderer->extend(
57 | function (Edge $edge, array $options) use ($container) {
58 | $edge->getObjectBuilder()
59 | ->setBuilder(
60 | function (string $class, ...$args) use ($container) {
61 | return $container->newInstance($class, $args);
62 | }
63 | );
64 |
65 | $this->prepareComponents($container, $edge, $options);
66 |
67 | $app = $container->get(ApplicationInterface::class);
68 |
69 | // Windwalker Extension should only work on level 3 or higher, and console web simulator.
70 | if (
71 | $app->getType() === AppType::CONSOLE
72 | || $container->getLevel() > 2
73 | ) {
74 | $edge->addExtension(
75 | $container->newInstance(WindwalkerExtension::class)
76 | );
77 | }
78 |
79 | $edge->setLoader(
80 | $container->newInstance(
81 | CoreFileLoader::class,
82 | [
83 | 'loader' => $edge->getLoader(),
84 | 'extensions' => $container->getParam('renderer.renderers.edge.1'),
85 | ]
86 | )
87 | );
88 |
89 | $cache = $edge->getCache();
90 |
91 | if ($cache instanceof EdgeFileCache) {
92 | $cache->setDebug(true);
93 | }
94 |
95 | return $edge;
96 | }
97 | );
98 | }
99 |
100 | protected function prepareComponents(
101 | Container $container,
102 | Edge $edge,
103 | array $options
104 | ): Edge {
105 | $extension = $container->createSharedObject(ComponentExtension::class, ['edge' => $edge]);
106 | $finder = $container->createSharedObject(ComponentFinder::class);
107 |
108 | foreach ((array) $container->getParam('renderer.edge.components') as $name => $class) {
109 | $extension->registerComponent($name, $class);
110 | }
111 |
112 | foreach ($finder->find() as $name => $class) {
113 | $extension->registerComponent($name, $class);
114 | }
115 |
116 | return $edge->addExtension($extension);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Wrapper/SlotWrapper.php:
--------------------------------------------------------------------------------
1 | slotCallback)(...$args);
29 |
30 | return ob_get_clean();
31 | }
32 |
33 | /**
34 | * Magic method {@see https://www.php.net/manual/en/language.oop5.magic.php}
35 | * called during serialization to string.
36 | *
37 | * @return string Returns string representation of the object that
38 | * implements this interface (and/or "__toString" magic method).
39 | */
40 | public function __toString(): string
41 | {
42 | return $this();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/bootstrap.php:
--------------------------------------------------------------------------------
1 |