├── 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 | Windwalker 5 |
6 |

7 | 8 |

Edge

9 | 10 |

11 | Windwalker Edge package 12 |

13 | 14 |

15 | GitHub 16 | GitHub Workflow Status 17 | Packagist Downloads 18 | Packagist Version 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 |