├── tmp
└── .gitkeep
├── src
├── bootstrap.php
├── Concern
│ ├── ManageEventTrait.php
│ ├── ManageStackTrait.php
│ ├── ManageComponentTrait.php
│ └── ManageLayoutTrait.php
├── Exception
│ ├── LayoutNotFoundException.php
│ └── EdgeException.php
├── functions.php
├── Extension
│ ├── EdgeExtensionInterface.php
│ ├── ParsersExtensionInterface.php
│ ├── DirectivesExtensionInterface.php
│ └── GlobalVariablesExtensionInterface.php
├── Compiler
│ ├── Concern
│ │ ├── CompileCommentTrait.php
│ │ ├── CompileClassTrait.php
│ │ ├── CompileRawPhpTrait.php
│ │ ├── CompileStackTrait.php
│ │ ├── CompileJsonTrait.php
│ │ ├── CompileIncludeTrait.php
│ │ ├── CompileLoopTrait.php
│ │ ├── CompileLayoutTrait.php
│ │ ├── CompileEchoTrait.php
│ │ ├── CompileConditional.php
│ │ └── CompileComponentTrait.php
│ ├── EdgeCompilerInterface.php
│ └── EdgeCompiler.php
├── Component
│ ├── AppendableAttributeValue.php
│ ├── AnonymousComponent.php
│ ├── InvokableComponentVariable.php
│ ├── ComponentExtension.php
│ ├── DynamicComponent.php
│ ├── AbstractComponent.php
│ ├── ComponentAttributes.php
│ └── ComponentTagCompiler.php
├── Loader
│ ├── EdgeLoaderInterface.php
│ ├── EdgeStringLoader.php
│ └── EdgeFileLoader.php
├── EdgeHelper.php
├── Wrapper
│ └── SlotWrapper.php
├── Cache
│ ├── EdgeCacheInterface.php
│ ├── EdgeStorageCache.php
│ ├── EdgeArrayCache.php
│ └── EdgeFileCache.php
├── Provider
│ └── EdgeProvider.php
└── Edge.php
├── phpunit.ci.xml
├── LICENSE.md
├── README.md
└── composer.json
/tmp/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/bootstrap.php:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | test
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileCommentTrait.php:
--------------------------------------------------------------------------------
1 | contentTags[0], $this->contentTags[1]);
22 |
23 | return preg_replace($pattern, '', $value);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Compiler/Concern/CompileClassTrait.php:
--------------------------------------------------------------------------------
1 | \"";
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/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/Loader/EdgeLoaderInterface.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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Exception/EdgeException.php:
--------------------------------------------------------------------------------
1 | file = $file;
33 | }
34 |
35 | if ($line) {
36 | $this->line = $line;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/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/Cache/EdgeCacheInterface.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 | Edge
9 |
10 |
11 | Windwalker Edge package
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ### Installation
22 |
23 | ```bash
24 | composer require windwalker/edge ^4.0
25 | ```
26 |
27 | ### Resources
28 |
29 | - [Documentation](https://windwalker.io/documentation/components/edge/)
30 | - [Bug report](https://github.com/windwalker-io/framework)
31 | - [Contributing](https://github.com/windwalker-io/framework)
32 | - [Discussion](https://github.com/windwalker-io/framework/discussions)
33 |
34 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "windwalker/edge",
3 | "type": "windwalker-package",
4 | "description": "Windwalker Edge package",
5 | "keywords": [
6 | "windwalker",
7 | "framework",
8 | "edge",
9 | "template",
10 | "blade"
11 | ],
12 | "homepage": "https://github.com/windwalker-io/edge",
13 | "license": "MIT",
14 | "require": {
15 | "php": ">=8.4.6",
16 | "windwalker/data": "^4.0",
17 | "windwalker/utilities": "^4.0"
18 | },
19 | "require-dev": {
20 | "phpunit/phpunit": "^10.0||^11.0||^12.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.4.6"
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/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/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/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/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/Loader/EdgeStringLoader.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/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/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/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/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/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/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/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/Loader/EdgeFileLoader.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/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/Cache/EdgeFileCache.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/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/Concern/ManageLayoutTrait.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/Compiler/Concern/CompileConditional.php:
--------------------------------------------------------------------------------
1 | ";
37 | }
38 |
39 | /**
40 | * Compile the else-if statements into valid PHP.
41 | *
42 | * @param string $expression
43 | *
44 | * @return string
45 | */
46 | protected function compileElseif(string $expression): string
47 | {
48 | return "";
49 | }
50 |
51 | /**
52 | * Compile the unless statements into valid PHP.
53 | *
54 | * @param string $expression
55 | *
56 | * @return string
57 | */
58 | protected function compileUnless(string $expression): string
59 | {
60 | return "";
61 | }
62 |
63 | /**
64 | * Compile the end unless statements into valid PHP.
65 | *
66 | * @param string $expression
67 | *
68 | * @return string
69 | */
70 | protected function compileEndunless(string $expression): string
71 | {
72 | return '';
73 | }
74 |
75 | /**
76 | * Compile the else statements into valid PHP.
77 | *
78 | * @param string $expression
79 | *
80 | * @return string
81 | */
82 | protected function compileElse(string $expression): string
83 | {
84 | return '';
85 | }
86 |
87 | /**
88 | * Compile the end-if statements into valid PHP.
89 | *
90 | * @param string $expression
91 | *
92 | * @return string
93 | */
94 | protected function compileEndif(string $expression): string
95 | {
96 | return '';
97 | }
98 |
99 | /**
100 | * Compile the switch statements into valid PHP.
101 | *
102 | * @param string $expression
103 | *
104 | * @return string
105 | */
106 | protected function compileSwitch(string $expression): string
107 | {
108 | $this->firstCaseInSwitch = true;
109 |
110 | return "firstCaseInSwitch) {
123 | $this->firstCaseInSwitch = false;
124 |
125 | return "case {$expression}: ?>";
126 | }
127 |
128 | return "";
129 | }
130 |
131 | /**
132 | * Compile the default statements in switch case into valid PHP.
133 | *
134 | * @return string
135 | */
136 | protected function compileDefault(): string
137 | {
138 | return '';
139 | }
140 |
141 | /**
142 | * Compile the end switch statements into valid PHP.
143 | *
144 | * @return string
145 | */
146 | protected function compileEndSwitch(): string
147 | {
148 | return '';
149 | }
150 |
151 | protected function compileOnce(?string $id = null): string
152 | {
153 | $id = $id ? $this->stripParentheses($id) : "'" . uid() . "'";
154 |
155 | return "renderOnce[$id])): \$__edge->renderOnce[$id] = true; ?>";
156 | }
157 |
158 | protected function compileEndOnce(): string
159 | {
160 | return "";
161 | }
162 |
163 | protected function compilePushOnce(string $expression): string
164 | {
165 | $parts = explode(',', $this->stripParentheses($expression), 2);
166 |
167 | [$stack, $id] = [$parts[0], $parts[1] ?? ''];
168 |
169 | $id = trim($id) ?: "'" . uid() . "'";
170 |
171 | return "renderOnce[$id])): \$__edge->renderOnce[$id] = true; " .
172 | "\$__edge->startPush($stack); ?>";
173 | }
174 |
175 | protected function compileEndPushOnce(): string
176 | {
177 | return 'stopPush(); endif; ?>';
178 | }
179 |
180 | protected function compileBool($conditions): string
181 | {
182 | return "";
183 | }
184 |
185 | protected function compileChecked($conditions): string
186 | {
187 | return "";
188 | }
189 |
190 | protected function compileDisabled($conditions): string
191 | {
192 | return "";
193 | }
194 |
195 | protected function compileRequired($conditions): string
196 | {
197 | return "";
198 | }
199 |
200 | protected function compileReadonly($conditions): string
201 | {
202 | return "";
203 | }
204 |
205 | protected function compileSelected($conditions): string
206 | {
207 | return "";
208 | }
209 |
210 | protected function compilePushIf($expression): string
211 | {
212 | $parts = explode(',', $this->stripParentheses($expression), 2);
213 |
214 | return "startPush({$parts[1]}); ?>";
215 | }
216 |
217 | protected function compileElsePushIf($expression): string
218 | {
219 | $parts = explode(',', $this->stripParentheses($expression), 2);
220 |
221 | return "stopPush(); elseif({$parts[0]}): \$__edge->startPush({$parts[1]}); ?>";
222 | }
223 |
224 | protected function compileElsePush($expression): string
225 | {
226 | return "stopPush(); else: \$__edge->startPush{$expression}; ?>";
227 | }
228 |
229 | protected function compileEndPushIf(): string
230 | {
231 | return "stopPush(); endif; ?>";
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/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/Component/AbstractComponent.php:
--------------------------------------------------------------------------------
1 | render();
71 | }
72 |
73 | /**
74 | * Get the data that should be supplied to the view.
75 | *
76 | * @return array
77 | */
78 | public function data(): array
79 | {
80 | // Prepare attributes bag
81 | $this->getComponentAttributes();
82 |
83 | return array_merge($this->extractPublicProperties(), $this->extractPublicMethods());
84 | }
85 |
86 | protected function configureAttributes(\Closure $callback): ComponentAttributes
87 | {
88 | return $this->attributes = $callback($this->getComponentAttributes()) ?? $this->attributes;
89 | }
90 |
91 | protected function getComponentAttributes(): ComponentAttributes
92 | {
93 | return $this->attributes ??= $this->newAttributeBag();
94 | }
95 |
96 | /**
97 | * Extract the public properties for the component.
98 | *
99 | * @return array
100 | */
101 | protected function extractPublicProperties(): array
102 | {
103 | $class = get_class($this);
104 |
105 | if (!isset(static::$propertyCache[$class])) {
106 | $reflection = new ReflectionClass($this);
107 |
108 | static::$propertyCache[$class] = collect($reflection->getProperties(ReflectionProperty::IS_PUBLIC))
109 | ->reject(fn(ReflectionProperty $property) => $property->isStatic())
110 | ->reject(fn(ReflectionProperty $property) => $this->shouldIgnore($property->getName()))
111 | ->map(fn(ReflectionProperty $property) => $property->getName())
112 | ->dump();
113 | }
114 |
115 | $values = [];
116 |
117 | foreach (static::$propertyCache[$class] as $property) {
118 | $values[$property] = $this->{$property};
119 | }
120 |
121 | return $values;
122 | }
123 |
124 | /**
125 | * Extract the public methods for the component.
126 | *
127 | * @return array
128 | * @throws \ReflectionException
129 | */
130 | protected function extractPublicMethods(): array
131 | {
132 | $class = get_class($this);
133 |
134 | if (!isset(static::$methodCache[$class])) {
135 | $reflection = new ReflectionClass($this);
136 |
137 | static::$methodCache[$class] = collect($reflection->getMethods(ReflectionMethod::IS_PUBLIC))
138 | ->reject(
139 | function (ReflectionMethod $method) {
140 | return $this->shouldIgnore($method->getName());
141 | }
142 | )
143 | ->map(
144 | function (ReflectionMethod $method) {
145 | return $method->getName();
146 | }
147 | );
148 | }
149 |
150 | $values = [];
151 |
152 | foreach (static::$methodCache[$class] as $method) {
153 | $values[$method] = $this->createVariableFromMethod(new ReflectionMethod($this, $method));
154 | }
155 |
156 | return $values;
157 | }
158 |
159 | /**
160 | * Create a callable variable from the given method.
161 | *
162 | * @param ReflectionMethod $method
163 | *
164 | * @return mixed
165 | */
166 | protected function createVariableFromMethod(ReflectionMethod $method)
167 | {
168 | return $method->getNumberOfParameters() === 0
169 | ? $this->createInvokableVariable($method->getName())
170 | : Closure::fromCallable([$this, $method->getName()]);
171 | }
172 |
173 | /**
174 | * Create an invokable, toStringable variable for the given component method.
175 | *
176 | * @param string $method
177 | *
178 | * @return InvokableComponentVariable
179 | */
180 | protected function createInvokableVariable(string $method): InvokableComponentVariable
181 | {
182 | return new InvokableComponentVariable(
183 | function () use ($method) {
184 | return $this->{$method}();
185 | }
186 | );
187 | }
188 |
189 | /**
190 | * Determine if the given property / method should be ignored.
191 | *
192 | * @param string $name
193 | *
194 | * @return bool
195 | */
196 | protected function shouldIgnore(string $name): bool
197 | {
198 | return str_starts_with($name, '__') || in_array($name, $this->ignoredMethods(), true);
199 | }
200 |
201 | /**
202 | * Get the methods that should be ignored.
203 | *
204 | * @return array
205 | */
206 | protected function ignoredMethods(): array
207 | {
208 | return array_merge(
209 | [
210 | 'data',
211 | 'render',
212 | 'resolveView',
213 | 'shouldRender',
214 | 'view',
215 | 'withName',
216 | 'withAttributes',
217 | ],
218 | $this->except
219 | );
220 | }
221 |
222 | /**
223 | * Set the component alias name.
224 | *
225 | * @param string $name
226 | *
227 | * @return $this
228 | */
229 | public function withName(string $name): static
230 | {
231 | $this->componentName = $name;
232 |
233 | return $this;
234 | }
235 |
236 | /**
237 | * Set the extra attributes that the component should make available.
238 | *
239 | * @param array $attributes
240 | *
241 | * @return static
242 | *
243 | * @deprecated 5.0 Use new method to merge attributes.
244 | */
245 | public function withAttributes(array $attributes, array|ComponentAttributes $binding = []): static
246 | {
247 | // if ($binding instanceof ComponentAttributes) {
248 | // $binding = $binding->getAttributes();
249 | // }
250 |
251 | $this->attributes = $this->attributes ?: $this->newAttributeBag();
252 |
253 | $this->attributes->setAttributes(
254 | [
255 | ...$this->attributes->getAttributes(),
256 | ...$attributes
257 | ]
258 | );
259 |
260 | return $this;
261 | }
262 |
263 | /**
264 | * Get a new attribute bag instance.
265 | *
266 | * @param array $attributes
267 | *
268 | * @return ComponentAttributes
269 | */
270 | protected function newAttributeBag(array $attributes = []): ComponentAttributes
271 | {
272 | return new ComponentAttributes($attributes);
273 | }
274 |
275 | /**
276 | * Determine if the component should be rendered.
277 | *
278 | * @return bool
279 | */
280 | public function shouldRender(): bool
281 | {
282 | return true;
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/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/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/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/Edge.php:
--------------------------------------------------------------------------------
1 | loader = $loader ?: new EdgeStringLoader();
114 | $this->compiler = $compiler ?: new EdgeCompiler();
115 | $this->cache = $cache ?: new EdgeArrayCache();
116 | }
117 |
118 | /**
119 | * renderWithContext
120 | *
121 | * @param string $layout
122 | * @param array $data
123 | * @param object|null $context
124 | *
125 | * @return string
126 | *
127 | * @throws EdgeException
128 | */
129 | public function renderWithContext(string $layout, array $data = [], ?object $context = null): string
130 | {
131 | $this->context = $context;
132 |
133 | $result = $this->render($layout, $data);
134 |
135 | $this->context = null;
136 |
137 | return $result;
138 | }
139 |
140 | /**
141 | * render
142 | *
143 | * @param string|callable $__layout
144 | * @param array $__data
145 | * @param array $__more
146 | *
147 | * @return string
148 | * @throws EdgeException
149 | */
150 | public function render(string|Closure $__layout, array $__data = [], array $__more = []): string
151 | {
152 | $this->level++;
153 |
154 | // TODO: Aliases
155 |
156 | $this->incrementRender();
157 |
158 | if ($__layout instanceof Closure) {
159 | $__path = $__layout;
160 | } else {
161 | $__path = $this->cacheStorage['layout:' . $__layout]
162 | ??= (is_file($__layout) ? $__layout : $this->loader->find($__layout));
163 |
164 | if ($this->cache->isExpired($__path)) {
165 | $compiled = $this->compile($this->loader->load($__path));
166 |
167 | $this->cache->store($__path, $compiled);
168 |
169 | unset($compiler, $compiled);
170 | }
171 | }
172 |
173 | $__data = array_merge($this->getGlobals(true), $__more, $__data);
174 |
175 | unset($__data['__path'], $__data['__data']);
176 |
177 | $closure = $this->getRenderFunction($__data);
178 |
179 | if ($this->getContext()) {
180 | $closure = $closure->bindTo($this->getContext(), $this->getContext());
181 | }
182 |
183 | ob_start();
184 |
185 | try {
186 | $closure($__path);
187 | } catch (Throwable $e) {
188 | ob_end_clean();
189 |
190 | $this->level--;
191 |
192 | if ($this->level === 0) {
193 | $this->wrapException($e, $__path, $__layout);
194 | } else {
195 | throw $e;
196 | }
197 |
198 | return '';
199 | }
200 |
201 | $result = ltrim(ob_get_clean());
202 |
203 | $this->decrementRender();
204 |
205 | $this->flushSectionsIfDoneRendering();
206 |
207 | $this->level--;
208 |
209 | return $result;
210 | }
211 |
212 | public function compile(string|Closure $path): string
213 | {
214 | $compiler = $this->prepareExtensions(clone $this->compiler);
215 |
216 | if ($path instanceof Closure) {
217 | $path = $path($this);
218 | }
219 |
220 | return $compiler->compile($path);
221 | }
222 |
223 | protected function getRenderFunction(array $data): Closure
224 | {
225 | $__data = $data;
226 | $__edge = $this;
227 |
228 | return function ($__path) use ($__data, $__edge) {
229 | extract($__data, EXTR_OVERWRITE);
230 |
231 | if ($__path instanceof Closure) {
232 | try {
233 | eval(' ?>' . $__edge->compile($__path($this, $__data)) . 'wrapEvalException($e, $__edge->compile($__path($this, $__data)), $__path);
236 | }
237 |
238 | return;
239 | }
240 |
241 | if ($__edge->getCache() instanceof EdgeFileCache) {
242 | include $__edge->getCache()->getCacheFile($__edge->getCache()->getCacheKey($__path));
243 | } else {
244 | try {
245 | eval(' ?>' . $__edge->getCache()->load($__path) . 'wrapEvalException($e, $__edge->getCache()->load($__path), $__path);
248 | }
249 | }
250 | };
251 | }
252 |
253 | public function wrapEvalException(Throwable $e, string $code, string|Closure $path): void
254 | {
255 | $lines = explode("\n", $code);
256 | $count = \count($lines);
257 |
258 | $line = $e->getLine();
259 | $start = $line - 3;
260 |
261 | if ($start <= 0) {
262 | $start = 0;
263 | }
264 |
265 | $end = $line + 3;
266 |
267 | if ($end > $count) {
268 | $end = $count;
269 | }
270 |
271 | $view = '';
272 |
273 | foreach (range($start, $end) as $i) {
274 | $l = trim(($lines[$i] ?? ''), "\n\r");
275 |
276 | $view .= $l . "\n";
277 | }
278 |
279 | if ($path instanceof Closure) {
280 | $path = '\Closure()';
281 | }
282 |
283 | $msg = <<getMessage()}
285 |
286 | ERROR ON: $path (line: {$e->getLine()})
287 | ---------
288 | $view
289 | ---------
290 | TEXT;
291 | throw new EdgeException(
292 | $msg,
293 | $e->getCode(),
294 | $e->getFile(),
295 | $e->getLine(),
296 | $e
297 | );
298 | }
299 |
300 | /**
301 | * wrapException
302 | *
303 | * @param Throwable $e
304 | * @param string|Closure $path
305 | * @param string|Closure $layout
306 | *
307 | * @return void
308 | *
309 | * @throws EdgeException
310 | */
311 | protected function wrapException(Throwable $e, string|Closure $path, string|Closure $layout): void
312 | {
313 | $msg = $e->getMessage();
314 |
315 | $layout = $layout instanceof Closure ? Assert::describeValue($layout) : $layout;
316 | $path = $path instanceof Closure ? Assert::describeValue($path) : $path;
317 |
318 | $msg .= sprintf("\n\n| View layout: %s (%s)", $path, $layout);
319 |
320 | $cache = $this->getCache();
321 |
322 | if ($cache instanceof EdgeFileCache) {
323 | if (str_starts_with(realpath($cache->getPath()), $e->getFile())) {
324 | throw new EdgeException($msg, $e->getCode(), $path, $e->getLine(), $e);
325 | }
326 | }
327 |
328 | throw new EdgeException($msg, $e->getCode(), $e->getFile(), $e->getLine(), $e);
329 | }
330 |
331 | /**
332 | * @return object|null
333 | */
334 | public function getContext(): ?object
335 | {
336 | return $this->context;
337 | }
338 |
339 | /**
340 | * @param object|null $context
341 | *
342 | * @return static Return self to support chaining.
343 | */
344 | public function setContext(?object $context): static
345 | {
346 | $this->context = $context;
347 |
348 | return $this;
349 | }
350 |
351 | /**
352 | * Normalize a view name.
353 | *
354 | * @param string $name
355 | *
356 | * @return string
357 | */
358 | protected function normalizeName(string $name): string
359 | {
360 | // TODO: Handle namespace
361 |
362 | return str_replace('/', '.', $name);
363 | }
364 |
365 | /**
366 | * escape
367 | *
368 | * @param mixed $string
369 | *
370 | * @return string
371 | */
372 | public function escape(mixed $string): string
373 | {
374 | if ($string instanceof RawWrapper) {
375 | return $string->get();
376 | }
377 |
378 | return htmlspecialchars((string) $string, ENT_COMPAT, 'UTF-8');
379 | }
380 |
381 | /**
382 | * Get the rendered contents of a partial from a loop.
383 | *
384 | * @param string $layout
385 | * @param array $data
386 | * @param string $iterator
387 | * @param string $empty
388 | *
389 | * @return string
390 | * @throws EdgeException
391 | */
392 | public function renderEach(string $layout, array $data, string $iterator, string $empty = 'raw|'): string
393 | {
394 | $result = '';
395 |
396 | // If is actually data in the array, we will loop through the data and append
397 | // an instance of the partial view to the final result HTML passing in the
398 | // iterated value of this data array, allowing the views to access them.
399 | if (count($data) > 0) {
400 | foreach ($data as $key => $value) {
401 | $data = ['key' => $key, $iterator => $value];
402 |
403 | $result .= $this->render($layout, $data);
404 | }
405 | } elseif (str_starts_with($empty, 'raw|')) {
406 | // If there is no data in the array, we will render the contents of the empty
407 | // view. Alternatively, the "empty view" could be a raw string that begins
408 | // with "raw|" for convenience and to let this know that it is a string.
409 | $result = substr($empty, 4);
410 | } else {
411 | $result = $this->render($empty);
412 | }
413 |
414 | return $result;
415 | }
416 |
417 | /**
418 | * Increment the rendering counter.
419 | *
420 | * @return void
421 | */
422 | public function incrementRender(): void
423 | {
424 | $this->renderCount++;
425 | }
426 |
427 | /**
428 | * Decrement the rendering counter.
429 | *
430 | * @return void
431 | */
432 | public function decrementRender(): void
433 | {
434 | $this->renderCount--;
435 | }
436 |
437 | /**
438 | * Check if there are no active render operations.
439 | *
440 | * @return bool
441 | */
442 | public function doneRendering(): bool
443 | {
444 | return $this->renderCount === 0;
445 | }
446 |
447 | /**
448 | * prepareDirectives
449 | *
450 | * @param EdgeCompilerInterface $compiler
451 | *
452 | * @return EdgeCompilerInterface
453 | */
454 | public function prepareExtensions(EdgeCompilerInterface $compiler): EdgeCompilerInterface
455 | {
456 | foreach ($this->getExtensions() as $extension) {
457 | if ($extension instanceof DirectivesExtensionInterface) {
458 | foreach ($extension->getDirectives() as $name => $directive) {
459 | $compiler->directive($name, $directive);
460 | }
461 | }
462 |
463 | if ($extension instanceof ParsersExtensionInterface) {
464 | foreach ($extension->getParsers() as $parser) {
465 | $compiler->parser($parser);
466 | }
467 | }
468 | }
469 |
470 | return $compiler;
471 | }
472 |
473 | /**
474 | * arrayExcept
475 | *
476 | * @param array $array
477 | * @param array $fields
478 | *
479 | * @return array
480 | */
481 | public function except(array $array, array $fields): array
482 | {
483 | return Arr::except($array, $fields);
484 | }
485 |
486 | /**
487 | * Method to get property Globals
488 | *
489 | * @param bool $withExtensions
490 | *
491 | * @return array
492 | */
493 | public function getGlobals(bool $withExtensions = false): array
494 | {
495 | $globals = $this->globals;
496 |
497 | if ($withExtensions) {
498 | $globals = $this->once(
499 | 'global.with.extensions',
500 | function () use ($globals) {
501 | $values = [];
502 |
503 | foreach ($this->getExtensions() as $extension) {
504 | if ($extension instanceof GlobalVariablesExtensionInterface) {
505 | $values[] = $extension->getGlobals();
506 | }
507 | }
508 |
509 | $values[] = $globals;
510 |
511 | return array_merge(...$values);
512 | }
513 | );
514 | }
515 |
516 | return $globals;
517 | }
518 |
519 | /**
520 | * addGlobal
521 | *
522 | * @param string $name
523 | * @param mixed $value
524 | *
525 | * @return static
526 | */
527 | public function addGlobal(string $name, mixed $value): static
528 | {
529 | $this->globals[$name] = $value;
530 |
531 | return $this;
532 | }
533 |
534 | /**
535 | * removeGlobal
536 | *
537 | * @param string $name
538 | *
539 | * @return static
540 | */
541 | public function removeGlobal(string $name): static
542 | {
543 | unset($this->globals[$name]);
544 |
545 | return $this;
546 | }
547 |
548 | public function getGlobal(string $name, $default = null)
549 | {
550 | if (array_key_exists($name, $this->globals)) {
551 | return $this->globals[$name];
552 | }
553 |
554 | return $default;
555 | }
556 |
557 | /**
558 | * Method to set property globals
559 | *
560 | * @param array $globals
561 | *
562 | * @return static Return self to support chaining.
563 | */
564 | public function setGlobals(array $globals): array
565 | {
566 | $this->globals = $globals;
567 |
568 | return $this;
569 | }
570 |
571 | /**
572 | * Method to get property Compiler
573 | *
574 | * @return EdgeCompilerInterface
575 | */
576 | public function getCompiler(): EdgeCompilerInterface
577 | {
578 | return $this->compiler;
579 | }
580 |
581 | /**
582 | * Method to set property compiler
583 | *
584 | * @param EdgeCompilerInterface $compiler
585 | *
586 | * @return static Return self to support chaining.
587 | */
588 | public function setCompiler(EdgeCompilerInterface $compiler): static
589 | {
590 | $this->compiler = $compiler;
591 |
592 | return $this;
593 | }
594 |
595 | /**
596 | * Method to get property Loader
597 | *
598 | * @return EdgeLoaderInterface
599 | */
600 | public function getLoader(): EdgeLoaderInterface
601 | {
602 | return $this->loader;
603 | }
604 |
605 | /**
606 | * Method to set property loader
607 | *
608 | * @param EdgeLoaderInterface $loader
609 | *
610 | * @return static Return self to support chaining.
611 | */
612 | public function setLoader(EdgeLoaderInterface $loader): static
613 | {
614 | $this->loader = $loader;
615 |
616 | return $this;
617 | }
618 |
619 | /**
620 | * addExtension
621 | *
622 | * @param EdgeExtensionInterface $extension
623 | * @param string|null $name
624 | *
625 | * @return static
626 | */
627 | public function addExtension(EdgeExtensionInterface $extension, ?string $name = null): static
628 | {
629 | if (!$name) {
630 | $name = $extension->getName();
631 | }
632 |
633 | $this->extensions[$name] = $extension;
634 |
635 | return $this;
636 | }
637 |
638 | /**
639 | * removeExtension
640 | *
641 | * @param string $name
642 | *
643 | * @return static
644 | */
645 | public function removeExtension(string $name): static
646 | {
647 | if (array_key_exists($name, $this->extensions)) {
648 | unset($this->extensions[$name]);
649 | }
650 |
651 | return $this;
652 | }
653 |
654 | /**
655 | * hasExtension
656 | *
657 | * @param string $name
658 | *
659 | * @return bool
660 | */
661 | public function hasExtension(string $name): bool
662 | {
663 | return array_key_exists($name, $this->extensions) && $this->extensions[$name] instanceof EdgeExtensionInterface;
664 | }
665 |
666 | /**
667 | * getExtension
668 | *
669 | * @param string $name
670 | *
671 | * @return EdgeExtensionInterface
672 | */
673 | public function getExtension(string $name): ?EdgeExtensionInterface
674 | {
675 | if ($this->hasExtension($name)) {
676 | return $this->extensions[$name];
677 | }
678 |
679 | return null;
680 | }
681 |
682 | /**
683 | * Method to get property Extensions
684 | *
685 | * @return Extension\EdgeExtensionInterface[]
686 | */
687 | public function getExtensions(): array
688 | {
689 | return $this->extensions;
690 | }
691 |
692 | /**
693 | * Method to set property extensions
694 | *
695 | * @param Extension\EdgeExtensionInterface[] $extensions
696 | *
697 | * @return static Return self to support chaining.
698 | */
699 | public function setExtensions(array $extensions): static
700 | {
701 | $this->extensions = $extensions;
702 |
703 | return $this;
704 | }
705 |
706 | /**
707 | * Method to get property Cache
708 | *
709 | * @return EdgeCacheInterface
710 | */
711 | public function getCache(): EdgeCacheInterface
712 | {
713 | return $this->cache;
714 | }
715 |
716 | /**
717 | * Method to set property cache
718 | *
719 | * @param EdgeCacheInterface $cache
720 | *
721 | * @return static Return self to support chaining.
722 | */
723 | public function setCache(EdgeCacheInterface $cache): static
724 | {
725 | $this->cache = $cache;
726 |
727 | return $this;
728 | }
729 |
730 | public function make(string $class, array $props = []): object
731 | {
732 | if ($class === AnonymousComponent::class || $class === DynamicComponent::class) {
733 | $object = new $class();
734 |
735 | foreach ($props as $key => $value) {
736 | ReflectAccessor::setValue($object, $key, $value);
737 | }
738 |
739 | return $object;
740 | }
741 |
742 | $object = $this->getObjectBuilder()->createObject($class, ...$props);
743 |
744 | $ref = new \ReflectionObject($object);
745 |
746 | foreach ($ref->getProperties() as $property) {
747 | $name = $property->getName();
748 |
749 | if (
750 | array_key_exists($name, $props)
751 | && $property->getAttributes(Prop::class, \ReflectionAttribute::IS_INSTANCEOF)
752 | ) {
753 | ReflectAccessor::setValue($object, $name, $props[$name]);
754 | }
755 | }
756 |
757 | return $object;
758 | }
759 | }
760 |
--------------------------------------------------------------------------------