├── .github
└── workflows
│ └── cla.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── examples
├── native-ssr
│ ├── public
│ │ └── index.php
│ └── resources
│ │ └── my-header.php
└── wasm-ssr
│ ├── public
│ └── index.php
│ └── resources
│ └── my-header.mjs
├── phpcs.xml
├── phpunit.xml
├── src
├── Elements.php
├── EnhanceWASM.php
├── Enhancer.php
└── ShadyStyles.php
└── test
├── fixtures
└── templates
│ ├── e-button.html
│ ├── e-tag.html
│ ├── multiple-slots.html
│ ├── my-bad-xml.php
│ ├── my-content.html
│ ├── my-context-child.php
│ ├── my-context-parent.php
│ ├── my-counter.php
│ ├── my-custom-heading-with-named-slot.html
│ ├── my-custom-heading.html
│ ├── my-external-script.html
│ ├── my-header.php
│ ├── my-heading.html
│ ├── my-id.php
│ ├── my-instance-id.php
│ ├── my-link-node-first.html
│ ├── my-link-node-second.html
│ ├── my-link.php
│ ├── my-list-container.php
│ ├── my-list-debug.php
│ ├── my-list.php
│ ├── my-multiples.html
│ ├── my-outline.html
│ ├── my-page.php
│ ├── my-paragraph.html
│ ├── my-pre-page.php
│ ├── my-pre.php
│ ├── my-slot-as.html
│ ├── my-store-data.php
│ ├── my-style-import-first.html
│ ├── my-style-import-second.html
│ ├── my-style-transform.html
│ ├── my-super-heading.html
│ ├── my-title.html
│ ├── my-transform-script.html
│ ├── my-transform-style.html
│ ├── my-unnamed.html
│ └── my-wrapped-heading.html
└── tests
├── ElementsTest.php
├── EnhancerTest.php
└── IsCustomElementTests.php
/.github/workflows/cla.yml:
--------------------------------------------------------------------------------
1 | name: "CLA Assistant"
2 | on:
3 | issue_comment:
4 | types: [created]
5 | pull_request_target:
6 | types: [opened,closed,synchronize]
7 |
8 | permissions:
9 | actions: write
10 | contents: write
11 | pull-requests: write
12 | statuses: write
13 |
14 | jobs:
15 | CLAAssistant:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: "CLA Assistant"
19 | if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
20 | uses: contributor-assistant/github-action@v2.4.0
21 | env:
22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 | PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
24 | with:
25 | path-to-signatures: 'signatures/v1/cla.json'
26 | path-to-document: 'https://github.com/enhance-dev/.github/blob/main/CLA.md'
27 | branch: 'main'
28 | allowlist: brianleroux,colepeters,kristoferjoseph,macdonst,ryanbethel,ryanblock,tbeseda
29 | remote-organization-name: enhance-dev
30 | remote-repository-name: .github
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 | enhance-ssr.wasm
4 | .phpunit.result.cache
5 | .phpunit.cache
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Enhance SSR PHP
2 |
3 | A library for server rendering web components in PHP that is compatible with Enhance SSR ([Enhance.dev](https://enhance.dev)).
4 |
5 | ## Runtime: WASM or native PHP
6 |
7 | This package includes both a WASM and native PHP version of enhance ssr.
8 | The WASM version allows component definitions written in JavaScript, but has specific requirements for the hosting environment that may be challenging.
9 | The examples directory includes examples of both versions.
10 |
11 | ## Install
12 | This package can be managed and installed with Composer:
13 |
14 | ```sh
15 | composer require enhance-dev/ssr
16 | ```
17 |
18 | The Extism dependency currently requires `"minimum-stability":"dev"` in the `composer.json` file.
19 |
20 | ## Run Examples
21 | To run the native and WASM examples run `composer serve-native` or `composer serve-wasm` respectively.
22 |
23 |
24 | ## Usage:
25 | See usage examples for native PHP and WASM in the examples directory.
26 |
27 | ### Native PHP
28 |
29 | ```php
30 |
31 | $elements,
43 | "initialState" => [],
44 | "styleTransforms" => [[$scopeMyStyle, "styleTransform"]],
45 | "enhancedAttr" => true,
46 | "bodyContent" => false,
47 | ]);
48 |
49 | $htmlString = <<
51 |
52 |
53 |
54 |
55 | Hello World
56 |
57 |
58 | HTMLDOC;
59 |
60 | $output = $enhance->ssr($htmlString);
61 |
62 | echo $output;
63 |
64 | ```
65 |
66 | ### WASM
67 |
68 | ```php
69 |
70 | true]);
78 | $enhance = new EnhanceWASM(["elements" => $elements->wasmElements]);
79 |
80 | $input = [
81 | "markup" => "Hello World ",
82 | "initialState" => [],
83 | ];
84 |
85 | $output = $enhance->ssr($input);
86 |
87 | $htmlDocument = $output->document;
88 |
89 | echo $htmlDocument . "\n";
90 |
91 | ```
92 |
93 |
94 |
95 |
96 | ## Install Extism Runtime Dependency (for WASM only)
97 |
98 | For the WASM version there are additional requirements.
99 | For this library, you first need to install the Extism Runtime by following the instructions in the [PHP SDK Repository](https://github.com/extism/php-sdk#install-the-extism-runtime-dependency).
100 |
101 |
102 | ## Acknowledgements
103 |
104 | Thank you @mariohamann for prototyping a PHP example in using [Extism](https://extism.org) https://github.com/mariohamann/enhance-ssr-wasm/tree/experiment/extism.
105 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "enhance-dev/ssr",
3 | "description": "A PHP library for server-side rendering with Enhance",
4 | "type": "library",
5 | "license": "Apache-2.0",
6 | "autoload": {
7 | "psr-4": {
8 | "Enhance\\": "src/"
9 | }
10 | },
11 | "autoload-dev": {
12 | "psr-4": {
13 | "Enhance\\Tests\\": "test/tests/"
14 | }
15 | },
16 | "authors": [
17 | {
18 | "name": "Ryan Bethel",
19 | "email": "ryan.bethel@begin.com"
20 | }
21 | ],
22 | "minimum-stability": "dev",
23 | "require": {
24 | "extism/extism": "dev-main",
25 | "sabberworm/php-css-parser": "^9.0@dev"
26 | },
27 | "scripts": {
28 | "serve-wasm": "php -d ffi.enable=true -S localhost:8000 -t examples/wasm-ssr/public",
29 | "serve-native": "php -S localhost:8000 -t examples/native-ssr/public",
30 | "test": "phpunit",
31 | "test-filter": "phpunit --filter $1",
32 | "post-install-cmd": ["@composer addEnhanceSsrWasmToVendor"],
33 | "post-update-cmd": ["@composer addEnhanceSsrWasmToVendor"],
34 | "addEnhanceSsrWasmToVendor": [
35 | "mkdir -p vendor/enhance/ssr-wasm && cd \"$_\" && curl -L https://github.com/enhance-dev/enhance-ssr-wasm/releases/download/v0.0.4/enhance-ssr.wasm.gz | gunzip > enhance-ssr.wasm"
36 | ]
37 | },
38 | "require-dev": {
39 | "phpunit/phpunit": "^11.1@dev",
40 | "squizlabs/php_codesniffer": "*"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/examples/native-ssr/public/index.php:
--------------------------------------------------------------------------------
1 | $elements,
13 | "initialState" => [],
14 | "styleTransforms" => [[$scopeMyStyle, "styleTransform"]],
15 | "enhancedAttr" => true,
16 | "bodyContent" => false,
17 | ]);
18 |
19 | $htmlString = <<
21 |
22 |
23 |
24 |
25 | Hello World
26 |
27 |
28 | HTMLDOC;
29 |
30 | $output = $enhance->ssr($htmlString);
31 |
32 | echo $output;
33 |
--------------------------------------------------------------------------------
/examples/native-ssr/resources/my-header.php:
--------------------------------------------------------------------------------
1 |
6 | h1 {
7 | color: red;
8 | }
9 |
10 |
11 | HTML;
12 | }
13 |
--------------------------------------------------------------------------------
/examples/wasm-ssr/public/index.php:
--------------------------------------------------------------------------------
1 | true]);
9 | $enhance = new EnhanceWASM(["elements" => $elements->wasmElements]);
10 |
11 | $input = [
12 | "markup" => "Hello World ",
13 | "initialState" => [],
14 | ];
15 |
16 | $output = $enhance->ssr($input);
17 |
18 | $htmlDocument = $output->document;
19 |
20 | echo $htmlDocument . "\n";
21 |
--------------------------------------------------------------------------------
/examples/wasm-ssr/resources/my-header.mjs:
--------------------------------------------------------------------------------
1 | function MyHeader({ html }) {
2 | return html`
7 | `;
8 | }
9 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Coding style description
4 |
5 | */test/fixtures/*/*\.(inc|css|js|html)$
6 | */vendor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | 0
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 | test/tests
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/Elements.php:
--------------------------------------------------------------------------------
1 | wasmElements[$key] = $content;
22 | }
23 | }
24 | closedir($dirHandle);
25 | }
26 | return;
27 | } elseif ($path && is_dir($path)) {
28 | // Load PHP files
29 | foreach (glob("$path/*.php") as $file) {
30 | require_once $file;
31 | $functionName = $this->convertToSnakeCase(
32 | basename($file, ".php")
33 | );
34 | $camelFunctionName = $this->convertToCamelCase(
35 | basename($file, ".php")
36 | );
37 | $pascalFunctionName = ucfirst($camelFunctionName);
38 | if (function_exists($functionName)) {
39 | $this->elements[$functionName] = $functionName;
40 | } elseif (function_exists($camelFunctionName)) {
41 | $this->elements[$functionName] = $camelFunctionName;
42 | } elseif (function_exists($pascalFunctionName)) {
43 | $this->elements[$functionName] = $pascalFunctionName;
44 | } else {
45 | throw new Exception(
46 | "Element function '$functionName' does not exist."
47 | );
48 | }
49 | }
50 |
51 | // Load HTML files
52 | foreach (glob("$path/*.html") as $file) {
53 | $functionName = $this->convertToSnakeCase(
54 | basename($file, ".html")
55 | );
56 | $this->elements[$functionName] = function ($state = null) use (
57 | $file
58 | ) {
59 | return file_get_contents($file);
60 | };
61 | }
62 | }
63 | }
64 |
65 | public function execute($name, $state = null)
66 | {
67 | $functionName = $this->convertToSnakeCase($name);
68 | if (isset($this->elements[$functionName])) {
69 | return call_user_func($this->elements[$functionName], $state);
70 | }
71 | // Handle the case where the function does not exist
72 | throw new Exception("Element function '$name' does not exist.");
73 | }
74 |
75 | private function convertToSnakeCase($name)
76 | {
77 | return strtolower(preg_replace("/-/", "_", $name));
78 | }
79 | private function convertToCamelCase($name)
80 | {
81 | return lcfirst(
82 | str_replace(" ", "", ucwords(str_replace("-", " ", $name)))
83 | );
84 | }
85 |
86 | public function exists($name)
87 | {
88 | $functionName = $this->convertToSnakeCase($name);
89 | return isset($this->elements[$functionName]);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/EnhanceWASM.php:
--------------------------------------------------------------------------------
1 | config->elements = json_encode($config["elements"]);
22 | }
23 | $this->enhance = new Plugin($manifest, true);
24 | }
25 |
26 | public function ssr($input)
27 | {
28 | $payload = [
29 | "markup" => $input["markup"] ?? "",
30 | "initialState" => $input["initialState"] ?? [],
31 | ];
32 | if (isset($input["elements"])) {
33 | $payload["elements"] = $input["elements"];
34 | }
35 | $jsonPayload = json_encode($input, JSON_PRETTY_PRINT);
36 | $output = $this->enhance->call("ssr", $jsonPayload);
37 | $document = json_decode($output);
38 | return $document;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Enhancer.php:
--------------------------------------------------------------------------------
1 | new Elements(),
25 | "initialState" => [],
26 | "scriptTransforms" => [],
27 | "styleTransforms" => [],
28 | "uuidFunction" => function () {
29 | return $this->generateRandomString(15);
30 | },
31 | "bodyContent" => false,
32 | "enhancedAttr" => true,
33 | ];
34 |
35 | if (
36 | isset($options["elements"]) &&
37 | $options["elements"] instanceof Elements
38 | ) {
39 | $defaultOptions["elements"] = $options["elements"];
40 | }
41 | $this->options = array_merge($defaultOptions, $options);
42 | if (!($this->options["elements"] instanceof Elements)) {
43 | throw new \Exception(
44 | "The 'elements' option must be an instance of Elements."
45 | );
46 | }
47 | $this->elements = $this->options["elements"];
48 | $this->store = $this->options["initialState"];
49 | }
50 |
51 | public function ssr($htmlString)
52 | {
53 | $doc = new DOMDocument(1.0, "UTF-8");
54 | // $convmap = array(0x80, 0xFFFF, 0, 0xFFFF);
55 | // $encodedHtml = mb_encode_numericentity($htmlString, $convmap, 'UTF-8');
56 | // @$doc->loadHTML($encodedHtml, LIBXML_HTML_NODEFDTD | LIBXML_NOERROR );
57 | @$doc->loadHTML('' . $htmlString);
58 |
59 |
60 | $htmlElement = $doc->getElementsByTagName("html")->item(0);
61 | $bodyElement = $htmlElement
62 | ? $doc->getElementsByTagName("body")->item(0)
63 | : null;
64 | $headElement = $htmlElement
65 | ? $doc->getElementsByTagName("head")->item(0)
66 | : null;
67 |
68 | $collected = [
69 | "collectedStyles" => [],
70 | "collectedScripts" => [],
71 | "collectedLinks" => [],
72 | ];
73 | $collected = $this->processCustomElements($bodyElement);
74 | if ($bodyElement) {
75 | if (count($collected["collectedScripts"]) > 0) {
76 | $flattenedScripts = $this->flattenArray(
77 | $collected["collectedScripts"]
78 | );
79 | $uniqueScripts = $this->uniqueTags($flattenedScripts);
80 |
81 | $this->appendNodes($bodyElement, $uniqueScripts);
82 | }
83 |
84 | if ($this->options["bodyContent"]) {
85 | $bodyContents = "";
86 | foreach ($bodyElement->childNodes as $childNode) {
87 | $bodyContents .= $doc->saveHTML($childNode);
88 | }
89 | return $bodyContents;
90 | }
91 | }
92 | if ($headElement && !$this->options["bodyContent"]) {
93 | if (count($collected["collectedStyles"]) > 0) {
94 | $flattenedStyles = $this->flattenArray(
95 | $collected["collectedStyles"]
96 | );
97 | $uniqueStyles = $this->uniqueTags($flattenedStyles);
98 | $cssBlocks = [];
99 | foreach ($uniqueStyles as $style) {
100 | $cssBlocks[] = $style->childNodes[0]->nodeValue;
101 | }
102 | $values = array_values($cssBlocks);
103 | usort($values, function ($a, $b) {
104 | $aStart = substr(trim($a), 0, 7);
105 | $bStart = substr(trim($b), 0, 7);
106 | if ($aStart === "@import" && $bStart !== "@import") {
107 | return -1;
108 | }
109 | if ($aStart !== "@import" && $bStart === "@import") {
110 | return 1;
111 | }
112 | return 0;
113 | });
114 | $mergedCssString = implode("\n", $values);
115 | $mergedStyles = $mergedCssString ?
116 | " "
117 | : "";
118 |
119 | $tempDoc = new DOMDocument(1.0, "UTF-8");
120 | $tempDoc->loadHTML($mergedStyles, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
121 | $loadedStyle = $tempDoc->getElementsByTagName("style")->item(0);
122 | $importedStyles = $headElement->ownerDocument->importNode(
123 | $loadedStyle,
124 | true
125 | );
126 | $headElement->appendChild($importedStyles);
127 | }
128 | }
129 |
130 | if ($headElement && !$this->options["bodyContent"]) {
131 | if (count($collected["collectedLinks"]) > 0) {
132 | $flattenedLinks = $this->flattenArray(
133 | $collected["collectedLinks"]
134 | );
135 | $uniqueLinks = $this->uniqueAttributeSet($flattenedLinks);
136 |
137 | $this->appendNodes($headElement, $uniqueLinks);
138 | }
139 | }
140 |
141 | $doc->encoding = "UTF-8";
142 | return "" . $doc->saveHTML($doc->documentElement);
143 | }
144 |
145 | private function processCustomElements(&$node)
146 | {
147 | $collectedStyles = [];
148 | $collectedScripts = [];
149 | $collectedLinks = [];
150 | $context = [];
151 |
152 | $this->walk($node, function ($child) use (
153 | &$collectedStyles,
154 | &$collectedScripts,
155 | &$collectedLinks,
156 | &$context
157 | ) {
158 | if (
159 | $child instanceof DOMElement &&
160 | $this->isCustomElement($child->tagName)
161 | ) {
162 | if ($this->elements->exists($child->tagName)) {
163 | $expandedTemplate = $this->expandTemplate([
164 | "node" => $child,
165 | "elements" => $this->elements,
166 | "state" => [
167 | "context" => $context,
168 | "instanceID" => $this->options["uuidFunction"](),
169 | "store" => $this->store,
170 | ],
171 | "styleTransforms" => $this->options["styleTransforms"],
172 | "scriptTransforms" =>
173 | $this->options["scriptTransforms"],
174 | ]);
175 |
176 | if ($this->options["enhancedAttr"] === true) {
177 | $child->setAttribute("enhanced", "✨");
178 | }
179 |
180 | $collectedScripts = array_merge(
181 | $collectedScripts,
182 | $expandedTemplate["scripts"]
183 | );
184 | $collectedStyles = array_merge(
185 | $collectedStyles,
186 | $expandedTemplate["styles"]
187 | );
188 | $collectedLinks = array_merge(
189 | $collectedLinks,
190 | $expandedTemplate["links"]
191 | );
192 |
193 | $this->fillSlots($expandedTemplate["frag"], $child);
194 | $importedFrag = $child->ownerDocument->importNode(
195 | $expandedTemplate["frag"],
196 | true
197 | );
198 | // $child->appendChild($importedFrag);
199 | // return $child;
200 | }
201 | }
202 | });
203 |
204 | return [
205 | "collectedStyles" => $collectedStyles,
206 | "collectedScripts" => $collectedScripts,
207 | "collectedLinks" => $collectedLinks,
208 | ];
209 | }
210 |
211 | // For Debugging
212 | private function printNodes($nodeArray)
213 | {
214 | $array = $nodeArray;
215 | if ($nodeArray instanceof DOMNodeList) {
216 | $array = [];
217 | foreach ($nodeArray as $item) {
218 | $array[] = $item;
219 | }
220 | }
221 | return array_map(function ($node) {
222 | return $node->ownerDocument->saveHTML($node);
223 | }, $array);
224 | }
225 |
226 | private function fillSlots($template, $node)
227 | {
228 | $slots = $this->findSlots($template);
229 | $inserts = $this->findInserts($node);
230 |
231 | $usedSlots = [];
232 | $usedInserts = [];
233 | $unnamedSlots = [];
234 |
235 | foreach ($slots as $slot) {
236 | $hasSlotName = false;
237 | $slotName = $slot->getAttribute("name");
238 |
239 | if ($slotName) {
240 | $hasSlotName = true;
241 | foreach ($inserts as $insert) {
242 | $insertSlot = $insert->getAttribute("slot");
243 | if ($insertSlot === $slotName) {
244 | if ($slot->parentNode) {
245 | $importedInsert = $slot->ownerDocument->importNode(
246 | $insert,
247 | true
248 | );
249 | $slot->parentNode->replaceChild(
250 | $importedInsert,
251 | $slot
252 | );
253 | }
254 | $usedSlots[] = $slot;
255 | $usedInserts[] = $insert;
256 | }
257 | }
258 | }
259 | if (!$hasSlotName) {
260 | $unnamedSlots[] = $slot;
261 | }
262 | }
263 |
264 | foreach ($unnamedSlots as $slot) {
265 | $unnamedChildren = [];
266 | foreach ($node->childNodes as $child) {
267 | if (!in_array($child, $usedInserts, true)) {
268 | $unnamedChildren[] = $child;
269 | }
270 | }
271 |
272 | $slotDocument = $slot->ownerDocument;
273 | $slotParent = $slot->parentNode;
274 | if (count($unnamedChildren)) {
275 | foreach ($unnamedChildren as $child) {
276 | $importedNode = $slotDocument->importNode($child, true);
277 | $slotParent->insertBefore($importedNode, $slot);
278 | }
279 | } else {
280 | foreach (iterator_to_array($slot->childNodes) as $child) {
281 | $slotParent->insertBefore($child, $slot);
282 | }
283 | }
284 | $slotParent->removeChild($slot);
285 | }
286 | $unusedSlots = [];
287 | foreach ($slots as $slot) {
288 | $isUsed = false;
289 | foreach ($usedSlots as $usedSlot) {
290 | if ($slot->isSameNode($usedSlot)) {
291 | $isUsed = true;
292 | break;
293 | }
294 | }
295 | if (!$isUsed) {
296 | $unusedSlots[] = $slot;
297 | }
298 | }
299 | $this->replaceSlots($template, $unusedSlots);
300 | while ($node->firstChild) {
301 | $node->removeChild($node->firstChild);
302 | }
303 |
304 | foreach ($template->childNodes as $childNode) {
305 | $importedNode = $node->ownerDocument->importNode($childNode, true);
306 | $node->appendChild($importedNode);
307 | }
308 | }
309 |
310 | private function findSlots(DOMNode $node)
311 | {
312 | $xpath = new DOMXPath($node->ownerDocument);
313 | $slots = $xpath->query(".//slot", $node);
314 | $slotArray = [];
315 | foreach ($slots as $slot) {
316 | $slotArray[] = $slot;
317 | }
318 | return $slotArray;
319 | return $slots;
320 | }
321 |
322 | private function findInserts(DOMNode $node)
323 | {
324 | $inserts = [];
325 | foreach ($node->childNodes as $child) {
326 | if ($child instanceof DOMElement && $child->hasAttribute("slot")) {
327 | $inserts[] = $child;
328 | }
329 | }
330 | return $inserts;
331 | }
332 |
333 | private function replaceSlots(DOMNode $node, $slots)
334 | {
335 | foreach ($slots as $slot) {
336 | $value = $slot->getAttribute("name");
337 | $asTag = $slot->hasAttribute("as")
338 | ? $slot->getAttribute("as")
339 | : "span";
340 |
341 | $slotChildren = [];
342 | foreach ($slot->childNodes as $child) {
343 | if (!($child instanceof DOMText)) {
344 | $slotChildren[] = $child;
345 | }
346 | }
347 |
348 | if ($value) {
349 | $doc = $slot->ownerDocument;
350 | $wrapper = $doc->createElement($asTag);
351 | $wrapper->setAttribute("slot", $value);
352 |
353 | if (count($slotChildren) === 0 || count($slotChildren) > 1) {
354 | foreach ($slot->childNodes as $child) {
355 | $wrapper->appendChild($child->cloneNode(true));
356 | }
357 | } elseif (count($slotChildren) === 1) {
358 | $slotChildren[0]->setAttribute("slot", $value);
359 | $slot->parentNode->insertBefore(
360 | $slotChildren[0]->cloneNode(true),
361 | $slot
362 | );
363 | }
364 |
365 | if ($wrapper->hasChildNodes()) {
366 | $slot->parentNode->replaceChild($wrapper, $slot);
367 | } else {
368 | $slot->parentNode->removeChild($slot);
369 | }
370 | }
371 | }
372 | return $node;
373 | }
374 |
375 | public function expandTemplate($params)
376 | {
377 | $node = $params["node"];
378 | $elements = $params["elements"];
379 | $state = $params["state"];
380 | $styleTransforms = $params["styleTransforms"];
381 | $scriptTransforms = $params["scriptTransforms"];
382 | $tagName = $node->tagName;
383 | $frag = $this->renderTemplate([
384 | "name" => $tagName,
385 | "elements" => $elements,
386 | "attrs" => $this->getNodeAttributes($node),
387 | "state" => $state,
388 | ]);
389 |
390 | $styles = [];
391 | $scripts = [];
392 | $links = [];
393 |
394 | foreach ($frag->childNodes as $childNode) {
395 | if ($childNode->nodeName === "script") {
396 | $transformedScript = $this->applyScriptTransforms([
397 | "node" => $childNode,
398 | "scriptTransforms" => $scriptTransforms,
399 | "tagName" => $tagName,
400 | ]);
401 | if ($transformedScript) {
402 | $scripts[] = $transformedScript;
403 | }
404 | } elseif ($childNode->nodeName === "style") {
405 | $transformedStyle = $this->applyStyleTransforms([
406 | "node" => $childNode,
407 | "styleTransforms" => $styleTransforms,
408 | "tagName" => $tagName,
409 | "context" => "markup",
410 | ]);
411 | if ($transformedStyle) {
412 | $styles[] = $transformedStyle;
413 | }
414 | } elseif ($childNode->nodeName === "link") {
415 | $links[] = $childNode;
416 | }
417 | }
418 |
419 | foreach (array_merge($scripts, $styles, $links) as $part) {
420 | $part->parentNode->removeChild($part);
421 | }
422 |
423 | return [
424 | "frag" => $frag,
425 | "styles" => $styles,
426 | "scripts" => $scripts,
427 | "links" => $links,
428 | ];
429 | }
430 |
431 | private function applyScriptTransforms($params)
432 | {
433 | $node = $params["node"];
434 | $scriptTransforms = $params["scriptTransforms"];
435 | $tagName = $params["tagName"];
436 |
437 | $attrs = $this->getNodeAttributes($node);
438 |
439 | if ($node->hasChildNodes()) {
440 | $raw = $node->firstChild->nodeValue;
441 | $out = $raw;
442 | foreach ($scriptTransforms as $transform) {
443 | $out = $transform([
444 | "attrs" => $attrs,
445 | "raw" => $out,
446 | "tagName" => $tagName,
447 | ]);
448 | }
449 |
450 | if (!empty($out)) {
451 | $node->firstChild->nodeValue = $out;
452 | }
453 | }
454 | return $node;
455 | }
456 |
457 | private function applyStyleTransforms($params)
458 | {
459 | $node = $params["node"];
460 | $styleTransforms = $params["styleTransforms"];
461 | $tagName = $params["tagName"];
462 | $context = $params["context"] ?? "";
463 |
464 | $attrs = $this->getNodeAttributes($node);
465 |
466 | if ($node->hasChildNodes()) {
467 | $raw = $node->firstChild->nodeValue;
468 | $out = $raw;
469 | foreach ($styleTransforms as $transform) {
470 | $out = $transform([
471 | "attrs" => $attrs,
472 | "raw" => $out,
473 | "tagName" => $tagName,
474 | "context" => $context,
475 | ]);
476 | }
477 | if (!empty($out)) {
478 | $node->firstChild->nodeValue = $out;
479 | }
480 | }
481 | return $node;
482 | }
483 |
484 | private static function appendNodes($target, $nodes)
485 | {
486 | foreach ($nodes as $node) {
487 | $importedNode = $target->ownerDocument->importNode($node, true);
488 | $target->appendChild($importedNode);
489 | }
490 | }
491 |
492 | private static function getNodeAttributes($node)
493 | {
494 | $attrs = [];
495 | if ($node->hasAttributes()) {
496 | foreach ($node->attributes as $attr) {
497 | $attrs[$attr->nodeName] = $attr->nodeValue;
498 | }
499 | }
500 | return $attrs;
501 | }
502 |
503 |
504 | public function renderTemplate($params)
505 | {
506 | $name = $params["name"];
507 | $elements = $params["elements"];
508 | $attrs = $params["attrs"] ?? [];
509 | $state = $params["state"] ?? [];
510 |
511 | $state["attrs"] = $attrs;
512 | $doc = new DOMDocument(1.0, "UTF-8");
513 | $rendered = $elements->execute($name, $state);
514 | libxml_use_internal_errors(true);
515 | $cleanRendered = <<
517 |
518 |
519 |
520 |
521 | {$rendered}
522 |
523 | HTMLDOC;
524 |
525 | $doc->encoding = "UTF-8";
526 | $doc->loadHTML(
527 | $cleanRendered,
528 | LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
529 | );
530 |
531 | $fragment = $doc->createDocumentFragment();
532 |
533 | $template = $doc->getElementsByTagName("template")->item(0);
534 | $children = iterator_to_array($template->childNodes);
535 | foreach ($children as $child) {
536 | $fragment->appendChild($child);
537 | }
538 | return $fragment;
539 | }
540 |
541 | private function walk($node, $callback)
542 | {
543 | if ($callback($node) === false) {
544 | return false;
545 | }
546 | foreach ($node->childNodes as $childNode) {
547 | if ($this->walk($childNode, $callback) === false) {
548 | return false;
549 | }
550 | }
551 | }
552 |
553 | private static function generateRandomString($length = 10)
554 | {
555 | return bin2hex(random_bytes(intdiv($length, 2)));
556 | }
557 |
558 | public static function isCustomElement($tagName)
559 | {
560 | $regex = '/^[a-z]
561 | [-.0-9_a-z\p{Pc}\p{Pd}\p{Mn}\x{00B7}\x{00C0}-\x{00D6}\x{00D8}-\x{00F6}\x{00F8}-\x{037D}\x{037F}-\x{1FFF}\x{200C}-\x{200D}\x{203F}-\x{2040}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}]*
562 | -
563 | [-.0-9_a-z\p{Pc}\p{Pd}\p{Mn}\x{00B7}\x{00C0}-\x{00D6}\x{00D8}-\x{00F6}\x{00F8}-\x{037D}\x{037F}-\x{1FFF}\x{200C}-\x{200D}\x{203F}-\x{2040}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}]*
564 | $/ux';
565 | $reservedTags = [
566 | "annotation-xml",
567 | "color-profile",
568 | "font-face",
569 | "font-face-src",
570 | "font-face-uri",
571 | "font-face-format",
572 | "font-face-name",
573 | "missing-glyph",
574 | ];
575 |
576 | if (in_array($tagName, $reservedTags)) {
577 | return false;
578 | }
579 |
580 | return preg_match($regex, $tagName) === 1;
581 | }
582 |
583 | private function uniqueAttributeSet($tags)
584 | {
585 | if (count($tags, COUNT_RECURSIVE) > 0) {
586 | $hashTable = [];
587 | foreach ($tags as $tagNode) {
588 | $attributes = $this->getNodeAttributes($tagNode);
589 | ksort($attributes);
590 | $attributesString = json_encode($attributes);
591 | $hash = md5($attributesString);
592 | if (!array_key_exists($hash, $hashTable)) {
593 | $hashTable[$hash] = $tagNode;
594 | }
595 | }
596 | return array_values($hashTable);
597 | } else {
598 | return $tags;
599 | }
600 | }
601 | private static function uniqueTags($tags)
602 | {
603 | if (count($tags, COUNT_RECURSIVE) > 0) {
604 | $hashTable = [];
605 | foreach ($tags as $tagNode) {
606 | $tagDoc = $tagNode->ownerDocument;
607 | $tagString = $tagDoc->saveHTML($tagNode);
608 | $hash = md5($tagString);
609 | if (!array_key_exists($hash, $hashTable)) {
610 | $hashTable[$hash] = $tagNode;
611 | }
612 | }
613 | return array_values($hashTable);
614 | } else {
615 | return $tags;
616 | }
617 | }
618 | private function flattenArray($array, &$flatArray = [])
619 | {
620 | foreach ($array as $element) {
621 | if (is_array($element)) {
622 | $this->flattenArray($element, $flatArray);
623 | } else {
624 | $flatArray[] = $element;
625 | }
626 | }
627 | return $flatArray;
628 | }
629 | }
630 |
--------------------------------------------------------------------------------
/src/ShadyStyles.php:
--------------------------------------------------------------------------------
1 | processBlock($raw, $tagName);
27 | } else {
28 | // for context === 'template' and any other case
29 | return $raw;
30 | }
31 | }
32 |
33 | private function processBlock($css, $scopeTo)
34 | {
35 | if (!$scopeTo) {
36 | return $css;
37 | }
38 |
39 | $parser = new \Sabberworm\CSS\Parser($css);
40 | $doc = $parser->parse();
41 |
42 | foreach ($doc->getAllDeclarationBlocks() as $block) {
43 | $selectors = $block->getSelectors();
44 | foreach ($selectors as $selector) {
45 | $newSelector = $this->selectorConverter(
46 | $selector->getSelector(),
47 | $scopeTo
48 | );
49 | $selector->setSelector($newSelector);
50 | }
51 | }
52 | return $doc->render();
53 | }
54 |
55 | private function selectorConverter($selector, $scopeTo)
56 | {
57 | // Apply transformations
58 | $selector = preg_replace(
59 | "/(::slotted)\(\s*(.+?)\s*\)/",
60 | '$2',
61 | $selector
62 | );
63 | $selector = preg_replace(
64 | "/(:host-context)\(\s*(.+?)\s*\)/",
65 | '$2 __TAGNAME__',
66 | $selector
67 | );
68 | $selector = preg_replace(
69 | "/(:host)\(\s*(.+?)\s*\)/",
70 | '__TAGNAME__$2',
71 | $selector
72 | );
73 | $selector = preg_replace(
74 | "/([a-zA-Z0-9_-]*)(::part)\(\s*(.+?)\s*\)/",
75 | '$1 [part*="$3"][part*="$1"]',
76 | $selector
77 | );
78 | $selector = str_replace(":host", "__TAGNAME__", $selector);
79 |
80 | if (strpos($selector, "__TAGNAME__") !== false) {
81 | $selector = preg_replace(
82 | "/(.*)__TAGNAME__(.*)/",
83 | "$1" . $scopeTo . "$2",
84 | $selector
85 | );
86 | } elseif (strpos($selector, "&") === 0) {
87 | // No change if it starts with '&'
88 | } else {
89 | $selector = "{$scopeTo} {$selector}";
90 | }
91 |
92 | return $selector;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/test/fixtures/templates/e-button.html:
--------------------------------------------------------------------------------
1 |
8 |
9 |
--------------------------------------------------------------------------------
/test/fixtures/templates/e-tag.html:
--------------------------------------------------------------------------------
1 |
10 |
11 |
--------------------------------------------------------------------------------
/test/fixtures/templates/multiple-slots.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-bad-xml.php:
--------------------------------------------------------------------------------
1 |
6 | My list
7 |
8 |
9 |
10 | HTMLDOC;
11 | }
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-content.html:
--------------------------------------------------------------------------------
1 | My Content
2 |
3 |
4 | Title
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-context-child.php:
--------------------------------------------------------------------------------
1 | {$message}";
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-context-parent.php:
--------------------------------------------------------------------------------
1 | ";
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-counter.php:
--------------------------------------------------------------------------------
1 | Count: {$count}";
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-custom-heading-with-named-slot.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-custom-heading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-external-script.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-header.php:
--------------------------------------------------------------------------------
1 |
6 | h1 {
7 | color: red;
8 | }
9 |
10 |
11 | HTML;
12 | }
13 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-heading.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-id.php:
--------------------------------------------------------------------------------
1 | ";
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-instance-id.php:
--------------------------------------------------------------------------------
1 | {$instanceID}
";
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-link-node-first.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-link-node-second.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-link.php:
--------------------------------------------------------------------------------
1 | {$text}
8 |
19 | HTML;
20 | }
21 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-list-container.php:
--------------------------------------------------------------------------------
1 | My List Container
8 |
9 |
10 | Title
11 |
12 |
13 |
14 | Content List
15 |
16 |
27 | HTML;
28 | }
29 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-list-debug.php:
--------------------------------------------------------------------------------
1 | {$title}";
12 | }, $items)
13 | );
14 | }
15 |
16 | return <<
18 | My list
19 |
20 | HTMLDOC;
21 | }
22 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-list.php:
--------------------------------------------------------------------------------
1 | {$title}";
12 | }, $items)
13 | );
14 | }
15 |
16 | return <<
18 | My list
19 |
20 |
23 |
34 | HTMLDOC;
35 | }
36 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-multiples.html:
--------------------------------------------------------------------------------
1 |
2 | My default text
3 | A smaller heading
4 | Random text
5 | a code block
6 |
7 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-outline.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-page.php:
--------------------------------------------------------------------------------
1 | My Page
7 |
8 | YOLO
9 |
10 | HTML;
11 | }
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-paragraph.html:
--------------------------------------------------------------------------------
1 | My default text
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-pre-page.php:
--------------------------------------------------------------------------------
1 | ';
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-pre.php:
--------------------------------------------------------------------------------
1 | {$item0}";
6 | }
7 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-slot-as.html:
--------------------------------------------------------------------------------
1 | stuff
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-store-data.php:
--------------------------------------------------------------------------------
1 |
11 | {$name}
12 | {$id}
13 |
14 | HTML;
15 | }
16 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-style-import-first.html:
--------------------------------------------------------------------------------
1 |
4 |
9 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-style-import-second.html:
--------------------------------------------------------------------------------
1 |
4 |
9 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-style-transform.html:
--------------------------------------------------------------------------------
1 |
10 |
11 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-super-heading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-title.html:
--------------------------------------------------------------------------------
1 | Default title
2 |
9 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-transform-script.html:
--------------------------------------------------------------------------------
1 | My Transform Script
2 |
10 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-transform-style.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
12 |
13 | My Transform Style
14 |
22 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-unnamed.html:
--------------------------------------------------------------------------------
1 | Default Content
2 |
--------------------------------------------------------------------------------
/test/fixtures/templates/my-wrapped-heading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test/tests/ElementsTest.php:
--------------------------------------------------------------------------------
1 | assertSame(
16 | strip($allElements->execute("my-paragraph")),
17 | strip('My default text
'),
18 | "Loaded HTML from Elements"
19 | );
20 | }
21 | public function testExecuteFunction()
22 | {
23 | global $allElements;
24 | $this->assertSame(
25 | strip($allElements->execute("my-pre")),
26 | strip(" "),
27 | "Loaded Function form from Elements"
28 | );
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/test/tests/EnhancerTest.php:
--------------------------------------------------------------------------------
1 | $allElements,
19 | "initialState" => ["message" => "Hello, World!"],
20 | "enhancedAttr" => false,
21 | ]);
22 |
23 | $htmlString =
24 | "Test Content";
25 | $expectedString =
26 | "Test Content";
27 |
28 | $this->assertSame(
29 | strip($expectedString),
30 | strip($enhancer->ssr($htmlString)),
31 | "The html doc matches."
32 | );
33 |
34 | $htmlString = "Fragment content";
35 | $expectedString = "Fragment content
";
36 |
37 | $this->assertSame(
38 | strip($expectedString),
39 | strip($enhancer->ssr($htmlString)),
40 | "html, and body are added."
41 | );
42 |
43 | $htmlString =
44 | " ";
45 | $expectedString =
46 | " ";
47 |
48 | $this->assertSame(
49 | strip($expectedString),
50 | strip($enhancer->ssr($htmlString)),
51 | "Custom Element Expansion."
52 | );
53 | }
54 | public function testEmptySlot()
55 | {
56 | global $allElements;
57 | $enhancer = new Enhancer([
58 | "elements" => $allElements,
59 | "bodyContent" => true,
60 | "enhancedAttr" => false,
61 | ]);
62 |
63 | $htmlString = " ";
64 | $expectedString =
65 | "My default text
";
66 |
67 | $this->assertSame(
68 | strip($expectedString),
69 | strip($enhancer->ssr($htmlString)),
70 | "by gum, i do believe that it does expand that template with slotted default content"
71 | );
72 | }
73 | public function testTemplateExpansion()
74 | {
75 | global $allElements;
76 | $enhancer = new Enhancer([
77 | "elements" => $allElements,
78 | "bodyContent" => true,
79 | "enhancedAttr" => false,
80 | ]);
81 |
82 | $htmlString =
83 | "I'm in a slot ";
84 | $expectedString =
85 | "I'm in a slot
";
86 |
87 | $this->assertSame(
88 | strip($expectedString),
89 | strip($enhancer->ssr($htmlString)),
90 | "slotted content is added to the template"
91 | );
92 | }
93 | public function testAddEnhancedAttr()
94 | {
95 | global $allElements;
96 | $enhancer = new Enhancer([
97 | "elements" => $allElements,
98 | "bodyContent" => true,
99 | "enhancedAttr" => true,
100 | ]);
101 |
102 | $htmlString =
103 | "I'm in a slot ";
104 | $expectedString =
105 | "I'm in a slot
";
106 |
107 | $this->assertSame(
108 | strip($expectedString),
109 | strip($enhancer->ssr($htmlString)),
110 | "Enhanced attribute is added to the template"
111 | );
112 | }
113 | public function testPassStateThroughLevels()
114 | {
115 | global $allElements;
116 | $enhancer = new Enhancer([
117 | "elements" => $allElements,
118 | "initialState" => ["items" => ["test"]],
119 | "bodyContent" => true,
120 | "enhancedAttr" => false,
121 | ]);
122 |
123 | $htmlString = " ";
124 | $expectedString = <<
126 |
127 | test
128 |
129 |
130 | HTMLCONTENT;
131 |
132 | $this->assertSame(
133 | strip($expectedString),
134 | strip($enhancer->ssr($htmlString)),
135 | "Enhanced attribute is added to the template"
136 | );
137 | }
138 |
139 | public function testShouldRenderAsDivWithSlotName()
140 | {
141 | global $allElements;
142 | $enhancer = new Enhancer([
143 | "elements" => $allElements,
144 | "bodyContent" => true,
145 | "enhancedAttr" => false,
146 | ]);
147 |
148 | $htmlString = "> ";
149 | $expectedString = <<
151 |
152 | My default text
153 |
154 |
155 | A smaller heading
156 |
157 |
158 |
159 | Random text
160 |
161 | a code block
162 |
163 |
164 | HTMLCONTENT;
165 |
166 | $this->assertSame(
167 | strip($expectedString),
168 | strip($enhancer->ssr($htmlString)),
169 | "It renders slot as div tag with slot name added"
170 | );
171 | }
172 |
173 | public function testShouldNotDuplicateSlottedContent()
174 | {
175 | global $allElements;
176 | $enhancer = new Enhancer([
177 | "elements" => $allElements,
178 | "bodyContent" => true,
179 | "enhancedAttr" => false,
180 | ]);
181 |
182 | $htmlString = <<
184 | things
185 |
186 | HTML;
187 | $expectedString = <<
189 | things
190 |
191 | HTMLCONTENT;
192 |
193 | $this->assertSame(
194 | strip($expectedString),
195 | strip($enhancer->ssr($htmlString)),
196 | "It does not duplicate slotted content"
197 | );
198 | }
199 |
200 | public function testFillNamedSlots()
201 | {
202 | global $allElements;
203 | $enhancer = new Enhancer([
204 | "elements" => $allElements,
205 | "bodyContent" => true,
206 | "enhancedAttr" => false,
207 | ]);
208 | $htmlString = <<
210 | Slotted
211 |
212 | HTML;
213 | $expectedString = <<
215 | Slotted
216 |
217 | HTMLCONTENT;
218 |
219 | $this->assertSame(
220 | strip($expectedString),
221 | strip($enhancer->ssr($htmlString)),
222 | "It fills named slots"
223 | );
224 | }
225 |
226 | public function testShouldRenderDefaultContentInUnnamedSlots()
227 | {
228 | global $allElements;
229 | $enhancer = new Enhancer([
230 | "elements" => $allElements,
231 | "bodyContent" => true,
232 | "enhancedAttr" => false,
233 | ]);
234 |
235 | $htmlString = ' ';
236 | $expectedString = 'Default Content ';
237 |
238 | $this->assertSame(
239 | strip($expectedString),
240 | strip($enhancer->ssr($htmlString)),
241 | "It fills named slots"
242 | );
243 | }
244 | public function testShouldNotRenderDefaultContentInUnnamedSlotsWithWhiteSpace()
245 | {
246 | global $allElements;
247 | $enhancer = new Enhancer([
248 | "elements" => $allElements,
249 | "bodyContent" => true,
250 | "enhancedAttr" => false,
251 | ]);
252 |
253 | $htmlString = ' ';
254 | $expectedString = ' ';
255 |
256 | $this->assertSame(
257 | strip($expectedString),
258 | strip($enhancer->ssr($htmlString)),
259 | "It fills named slots"
260 | );
261 | }
262 |
263 | public function testAddAuthoredChildrenToUnnamedSlot()
264 | {
265 | global $allElements;
266 | $enhancer = new Enhancer([
267 | "elements" => $allElements,
268 | "bodyContent" => true,
269 | "enhancedAttr" => false,
270 | ]);
271 |
272 | $htmlString = <<
274 | Custom title
275 |
276 | HTML;
277 |
278 | $expectedString = <<
280 | My Content
281 | Custom title
282 |
283 | HTML;
284 |
285 | $this->assertSame(
286 | strip($expectedString),
287 | strip($enhancer->ssr($htmlString)),
288 | "It adds authored children to unnamed slot"
289 | );
290 | }
291 |
292 | public function testPassAttributesAsState()
293 | {
294 | global $allElements;
295 | $enhancer = new Enhancer([
296 | "elements" => $allElements,
297 | "bodyContent" => false,
298 | "enhancedAttr" => false,
299 | ]);
300 |
301 | $head = HeadTag();
302 |
303 | $htmlString = <<
306 | HTML;
307 |
308 | $expectedString = <<
310 |
311 |
312 |
313 |
314 | sketchy
315 |
316 |
326 |
327 |
328 | HTML;
329 |
330 | $this->assertSame(
331 | strip($expectedString),
332 | strip($enhancer->ssr($htmlString)),
333 | "passes attributes as a state object when executing template functions"
334 | );
335 | }
336 |
337 | public function testBadXML()
338 | {
339 | global $allElements;
340 | $enhancer = new Enhancer([
341 | "elements" => $allElements,
342 | "bodyContent" => true,
343 | "enhancedAttr" => false,
344 | ]);
345 |
346 | $htmlString = <<
348 | HTML;
349 |
350 | $expectedString = <<
352 | My list
353 |
354 |
355 |
356 | HTMLDOC;
357 |
358 | $this->assertSame(
359 | strip($expectedString),
360 | strip($enhancer->ssr($htmlString)),
361 | "Poorly formed html that does not meet xml standards"
362 | );
363 | }
364 | public function testPassArrayValuesDoesnt()
365 | {
366 | global $allElements;
367 | $enhancer = new Enhancer([
368 | "elements" => $allElements,
369 | "bodyContent" => false,
370 | "enhancedAttr" => false,
371 | "initialState" => [
372 | "items" => [
373 | ["title" => "one"],
374 | ["title" => "two"],
375 | ["title" => "three"],
376 | ],
377 | ],
378 | ]);
379 |
380 | $head = HeadTag();
381 |
382 | $htmlString = <<
385 | HTML;
386 |
387 | $expectedString = <<
389 |
390 |
391 |
392 |
393 | My list
394 |
395 | one
396 | two
397 | three
398 |
399 |
400 |
410 |
411 |
412 | HTMLDOC;
413 |
414 | $this->assertSame(
415 | strip($expectedString),
416 | strip($enhancer->ssr($htmlString)),
417 | "Expands list items from state"
418 | );
419 | }
420 |
421 | public function testDeeplyNestedSlots()
422 | {
423 | global $allElements;
424 | $enhancer = new Enhancer([
425 | "elements" => $allElements,
426 | "bodyContent" => true,
427 | "enhancedAttr" => false,
428 | ]);
429 |
430 | $htmlString = <<
432 |
433 | Second
434 |
435 | Third
436 |
437 |
438 |
439 | HTML;
440 |
441 | $expectedString = <<
443 | My Content
444 |
445 | Title
446 |
447 |
448 | My Content
449 | Second
450 |
451 | My Content
452 | Third
453 |
454 |
455 |
456 | HTMLDOC;
457 |
458 | $this->assertSame(
459 | strip($expectedString),
460 | strip($enhancer->ssr($htmlString)),
461 | "Fills deeply nested slots"
462 | );
463 | }
464 |
465 | public function testFillNestedRenderedSlots()
466 | {
467 | //TODO: This tests is modified from the JS version to use the Store. We need to reconcile the two tests
468 | global $allElements;
469 | $enhancer = new Enhancer([
470 | "elements" => $allElements,
471 | "bodyContent" => false,
472 | "enhancedAttr" => false,
473 | "initialState" => [
474 | "items" => [
475 | ["title" => "one"],
476 | ["title" => "two"],
477 | ["title" => "three"],
478 | ],
479 | ],
480 | ]);
481 | $head = HeadTag();
482 |
483 | $htmlString = <<
486 | YOLO
487 |
488 | HTML;
489 |
490 | $expectedString = <<
492 |
493 |
494 |
495 |
496 | My List Container
497 |
498 | YOLO
499 |
500 |
501 | Content List
502 |
503 | one
504 | two
505 | three
506 |
507 |
508 |
509 |
520 |
531 |
532 |
533 | HTMLDOC;
534 |
535 | $this->assertSame(
536 | strip($expectedString),
537 | strip($enhancer->ssr($htmlString)),
538 | "Wow it renders nested custom elements by passing that handy render function when executing template functions"
539 | );
540 | }
541 |
542 | public function testAllowCustomHeadTag()
543 | {
544 | global $allElements;
545 | $enhancer = new Enhancer([
546 | "elements" => $allElements,
547 | "bodyContent" => false,
548 | "enhancedAttr" => false,
549 | ]);
550 |
551 | $htmlString = <<
553 |
554 |
555 | Yolo!
556 |
557 |
558 |
559 | HTML;
560 |
561 | $expectedString = <<
563 |
564 |
565 |
566 | Yolo!
567 |
568 |
569 |
570 | Count: 3
571 |
572 |
573 | HTMLDOC;
574 |
575 | $this->assertSame(
576 | strip($expectedString),
577 | strip($enhancer->ssr($htmlString)),
578 | "It allows custom head tag"
579 | );
580 | }
581 |
582 | public function testShouldPassStoreToTemplate()
583 | {
584 | // test('should pass store to template', t => {
585 | global $allElements;
586 | $enhancer = new Enhancer([
587 | "elements" => $allElements,
588 | "bodyContent" => false,
589 | "enhancedAttr" => false,
590 | "initialState" => [
591 | "apps" => [
592 | [
593 | "id" => 1,
594 | "name" => "one",
595 | "users" => [
596 | [
597 | "id" => 1,
598 | "name" => "jim",
599 | ],
600 | [
601 | "id" => 2,
602 | "name" => "kim",
603 | ],
604 | [
605 | "id" => 3,
606 | "name" => "phillip",
607 | ],
608 | ],
609 | ],
610 | ],
611 | ],
612 | ]);
613 |
614 | $head = HeadTag();
615 |
616 | $htmlString = <<
619 | HTML;
620 |
621 | $expectedString = <<
623 |
624 |
625 |
626 |
627 |
628 |
kim
629 | 2
630 |
631 |
632 |
633 |
634 | HTMLDOC;
635 |
636 | $this->assertSame(
637 | strip($expectedString),
638 | strip($enhancer->ssr($htmlString)),
639 | "Store data is passed to template"
640 | );
641 | }
642 | public function testRunScriptTransform()
643 | {
644 | global $allElements;
645 | $enhancer = new Enhancer([
646 | "elements" => $allElements,
647 | "bodyContent" => false,
648 | "enhancedAttr" => false,
649 | "scriptTransforms" => [
650 | function ($params) {
651 | $raw = $params["raw"];
652 | $tagName = $params["tagName"];
653 | return "{$raw}\n{$tagName}";
654 | },
655 | ],
656 | ]);
657 |
658 | $head = HeadTag();
659 |
660 | $htmlString = <<
663 |
664 | HTML;
665 |
666 | $expectedString = <<
668 |
669 |
670 |
671 |
672 | My Transform Script
673 |
674 |
675 | My Transform Script
676 |
677 |
686 |
687 |
688 | HTMLDOC;
689 |
690 | $this->assertSame(
691 | strip($expectedString),
692 | strip($enhancer->ssr($htmlString)),
693 | "Script Transform is run"
694 | );
695 | }
696 |
697 | public function testShouldNotAddDuplicateStyleTags()
698 | {
699 | global $allElements;
700 | $enhancer = new Enhancer([
701 | "elements" => $allElements,
702 | "bodyContent" => false,
703 | "enhancedAttr" => false,
704 | "styleTransforms" => [
705 | function ($params) {
706 | $attrs = $params["attrs"];
707 | $raw = $params["raw"];
708 | $tagName = $params["tagName"];
709 | $context = $params["context"];
710 | $globalScope =
711 | isset($attrs["scope"]) && $attrs["scope"] === "global";
712 | if ($globalScope && $context === "template") {
713 | return "";
714 | }
715 | // Otherwise, return the raw content and additional comment
716 | return <<
732 |
733 | HTML;
734 |
735 | $expectedString = <<
737 |
738 |
739 |
756 |
757 |
758 |
759 | My Transform Style
760 |
761 |
762 | My Transform Style
763 |
764 |
772 |
773 |
774 | HTMLDOC;
775 |
776 | $this->assertSame(
777 | strip($expectedString),
778 | strip($enhancer->ssr($htmlString)),
779 | "Removed Duplicate Style Tags"
780 | );
781 | }
782 |
783 | public function testShouldRespectAsAttribute()
784 | {
785 | global $allElements;
786 | $enhancer = new Enhancer([
787 | "elements" => $allElements,
788 | "bodyContent" => true,
789 | "enhancedAttr" => false,
790 | ]);
791 |
792 | $htmlString = <<
794 | HTML;
795 |
796 | $expectedString = <<
798 |
799 | stuff
800 |
801 |
802 | HTMLDOC;
803 |
804 | $this->assertSame(
805 | strip($expectedString),
806 | strip($enhancer->ssr($htmlString)),
807 | "Respects as attribute"
808 | );
809 | }
810 |
811 | public function testShouldAddMultipleExternalScritps()
812 | {
813 | global $allElements;
814 | $enhancer = new Enhancer([
815 | "elements" => $allElements,
816 | "bodyContent" => false,
817 | "enhancedAttr" => false,
818 | "scriptTransforms" => [
819 | function ($params) {
820 | return "{$params["raw"]}\n{$params["tagName"]}";
821 | },
822 | ],
823 | ]);
824 |
825 | $head = HeadTag();
826 |
827 | $htmlString = <<
830 |
831 | HTML;
832 |
833 | $expectedString = <<
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 | HTMLDOC;
850 |
851 | $this->assertSame(
852 | strip($expectedString),
853 | strip($enhancer->ssr($htmlString)),
854 | "Adds multiple external scripts"
855 | );
856 | }
857 |
858 | public function testShouldSupportUnnamedSlotWithoutWhitespace()
859 | {
860 | global $allElements;
861 | $enhancer = new Enhancer([
862 | "elements" => $allElements,
863 | "bodyContent" => true,
864 | "enhancedAttr" => false,
865 | ]);
866 |
867 | $head = HeadTag();
868 |
869 | $htmlString = <<My Text
871 | HTML;
872 |
873 | $expectedString = <<My Text
875 | HTMLDOC;
876 |
877 | $this->assertSame(
878 | strip($expectedString),
879 | strip($enhancer->ssr($htmlString)),
880 | "Unnamed slot without whitespace"
881 | );
882 | }
883 |
884 | public function testShouldSupportNestedCustomElementWithNestedSlot()
885 | {
886 | global $allElements;
887 | $enhancer = new Enhancer([
888 | "elements" => $allElements,
889 | "bodyContent" => false,
890 | "enhancedAttr" => false,
891 | ]);
892 |
893 | $head = HeadTag();
894 |
895 | $htmlString = <<
897 |
898 |
899 | ✨
900 |
901 | My Heading
902 |
903 |
904 | HTML;
905 |
906 | $expectedString = <<
908 |
909 |
910 | ✨
911 |
912 |
913 |
914 | My Heading
915 |
916 |
917 |
918 |
919 | HTMLDOC;
920 |
921 | $this->assertSame(
922 | strip($expectedString),
923 | strip($enhancer->ssr($htmlString)),
924 | "Renders nested slots in nested custom elements"
925 | );
926 | }
927 |
928 | public function testShouldNotFailWhenPassedCustomElementWithoutTemplate()
929 | {
930 | global $allElements;
931 | $enhancer = new Enhancer([
932 | "elements" => $allElements,
933 | "bodyContent" => true,
934 | "enhancedAttr" => false,
935 | ]);
936 |
937 | $head = HeadTag();
938 |
939 | $htmlString = <<
941 | HTML;
942 |
943 | $expectedString = <<
945 | HTMLDOC;
946 |
947 | $this->assertSame(
948 | strip($expectedString),
949 | strip($enhancer->ssr($htmlString)),
950 | "Does not fail when passed custom element without template"
951 | );
952 | }
953 |
954 | public function testShouldSupplyInstanceID()
955 | {
956 | global $allElements;
957 | $enhancer = new Enhancer([
958 | "elements" => $allElements,
959 | "bodyContent" => true,
960 | "uuidFunction" => function () {
961 | return "abcd1234";
962 | },
963 | "enhancedAttr" => false,
964 | ]);
965 |
966 | $head = HeadTag();
967 |
968 | $htmlString = <<
970 | HTML;
971 |
972 | $expectedString = <<
974 | abcd1234
975 |
976 | HTMLDOC;
977 |
978 | $this->assertSame(
979 | strip($expectedString),
980 | strip($enhancer->ssr($htmlString)),
981 | "Has access to instance ID"
982 | );
983 | }
984 |
985 | public function testShouldSupplyContext()
986 | {
987 | global $allElements;
988 | $enhancer = new Enhancer([
989 | "elements" => $allElements,
990 | "bodyContent" => true,
991 | "enhancedAttr" => false,
992 | ]);
993 |
994 | $head = HeadTag();
995 |
996 | $htmlString = <<
998 |
999 |
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 | HTML;
1008 |
1009 | $expectedString = <<
1011 |
1012 |
1013 |
1014 | hmmm
1015 |
1016 |
1017 |
1018 |
1019 |
1020 | sure
1021 |
1022 |
1023 |
1024 | HTMLDOC;
1025 |
1026 | $this->assertSame(
1027 | strip($expectedString),
1028 | strip($enhancer->ssr($htmlString)),
1029 | "Passes context data to child elements"
1030 | );
1031 | }
1032 |
1033 | public function testShouldMoveLinkElementsToTheHead()
1034 | {
1035 | global $allElements;
1036 | $enhancer = new Enhancer([
1037 | "elements" => $allElements,
1038 | "bodyContent" => false,
1039 | "enhancedAttr" => false,
1040 | ]);
1041 |
1042 | $head = HeadTag();
1043 |
1044 | $htmlString = <<first
1047 | second
1048 | first again
1049 | HTML;
1050 |
1051 | $expectedString = <<
1053 |
1054 |
1055 |
1056 |
1057 |
1058 |
1059 | first
1060 | second
1061 | first again
1062 |
1063 |
1064 | HTMLDOC;
1065 |
1066 | $this->assertSame(
1067 | strip($expectedString),
1068 | strip($enhancer->ssr($htmlString)),
1069 | "Moves deduplicated link elements to the head"
1070 | );
1071 | }
1072 |
1073 | public function testShouldHoistCssImports()
1074 | {
1075 | global $allElements;
1076 | $enhancer = new Enhancer([
1077 | "elements" => $allElements,
1078 | "bodyContent" => false,
1079 | "enhancedAttr" => false,
1080 | ]);
1081 |
1082 | $head = HeadTag();
1083 |
1084 | $htmlString = <<
1087 |
1088 | HTML;
1089 |
1090 | $expectedString = <<
1092 |
1093 |
1094 |
1100 |
1101 |
1102 |
1103 |
1104 |
1105 |
1106 | HTMLDOC;
1107 |
1108 | $this->assertSame(
1109 | strip($expectedString),
1110 | strip($enhancer->ssr($htmlString)),
1111 | "CSS imports are hoisted"
1112 | );
1113 | }
1114 |
1115 | public function testShouldRenderNestedSlotsInsideUnnamedSlot()
1116 | {
1117 | global $allElements;
1118 | $enhancer = new Enhancer([
1119 | "elements" => $allElements,
1120 | "bodyContent" => true,
1121 | "enhancedAttr" => false,
1122 | ]);
1123 |
1124 | $head = HeadTag();
1125 |
1126 | $htmlString = <<
1128 | Here's my text
1129 |
1130 | HTML;
1131 |
1132 | $expectedString = <<
1134 |
1135 |
1136 | Here's my text
1137 |
1138 |
1139 |
1140 | HTMLDOC;
1141 |
1142 | $this->assertSame(
1143 | strip($expectedString),
1144 | strip($enhancer->ssr($htmlString)),
1145 | "Renders nested named slot inside unnamed slot"
1146 | );
1147 | }
1148 | public function testMultipleSlotsWithUnnamedSlotFirst()
1149 | {
1150 | global $allElements;
1151 | $enhancer = new Enhancer([
1152 | "elements" => $allElements,
1153 | "bodyContent" => true,
1154 | "enhancedAttr" => true,
1155 | ]);
1156 |
1157 | $head = HeadTag();
1158 |
1159 | $htmlString = <<unnamed slotslot One
1161 | HTML;
1162 |
1163 | $expectedString = <<
1165 | unnamed slotslot One
1166 |
1167 | HTMLDOC;
1168 |
1169 | $this->assertSame(
1170 | strip($expectedString),
1171 | strip($enhancer->ssr($htmlString)),
1172 | "Unnamed and named slots work together"
1173 | );
1174 | }
1175 |
1176 | public function testStyleTransform()
1177 | {
1178 | global $allElements;
1179 | $scopeMyStyle = new ShadyStyles();
1180 | $enhancer = new Enhancer([
1181 | "elements" => $allElements,
1182 | "bodyContent" => false,
1183 | "enhancedAttr" => false,
1184 | "styleTransforms" => [
1185 | function ($params) use ($scopeMyStyle) {
1186 | return $scopeMyStyle->styleTransform($params);
1187 | },
1188 | ],
1189 | ]);
1190 |
1191 | $head = HeadTag();
1192 |
1193 | $htmlString = <<Hello
1196 | HTML;
1197 |
1198 | $expectedString = <<
1200 |
1201 |
1202 |
1210 |
1211 |
1212 | Hello
1213 |
1214 |
1215 | HTMLDOC;
1216 |
1217 | $this->assertSame(
1218 | strip($expectedString),
1219 | strip($enhancer->ssr($htmlString)),
1220 | "Style transform worked"
1221 | );
1222 | }
1223 |
1224 | public function testMyHeaderStyle()
1225 | {
1226 | global $allElements;
1227 | $scopeMyStyle = new ShadyStyles();
1228 | $enhancer = new Enhancer([
1229 | "elements" => $allElements,
1230 | "bodyContent" => false,
1231 | "enhancedAttr" => false,
1232 | "styleTransforms" => [
1233 | function ($params) use ($scopeMyStyle) {
1234 | return $scopeMyStyle->styleTransform($params);
1235 | },
1236 | ],
1237 | ]);
1238 |
1239 | $head = HeadTag();
1240 |
1241 | $htmlString = <<Hello World
1244 | HTML;
1245 |
1246 | $expectedString = <<
1248 |
1249 |
1250 |
1255 |
1256 |
1257 | Hello World
1258 |
1259 |
1260 | HTMLDOC;
1261 |
1262 | $this->assertSame(
1263 | strip($expectedString),
1264 | strip($enhancer->ssr($htmlString)),
1265 | "My Header style worked"
1266 | );
1267 | }
1268 | public function testEntityEncodedCaracters()
1269 | {
1270 | global $allElements;
1271 | $scopeMyStyle = new ShadyStyles();
1272 | $enhancer = new Enhancer([
1273 | "elements" => $allElements,
1274 | "bodyContent" => false,
1275 | "enhancedAttr" => false,
1276 | "styleTransforms" => [
1277 | function ($params) use ($scopeMyStyle) {
1278 | return $scopeMyStyle->styleTransform($params);
1279 | },
1280 | ],
1281 | ]);
1282 |
1283 | $head = HeadTag();
1284 |
1285 | $htmlString = <<&×
1288 | HTML;
1289 |
1290 | $expectedString = <<
1292 |
1293 |
1294 |
1299 |
1300 |
1301 | &×
1302 |
1303 |
1304 | HTMLDOC;
1305 |
1306 | $this->assertSame(
1307 | strip($expectedString),
1308 | strip($enhancer->ssr($htmlString)),
1309 | "My Header style worked"
1310 | );
1311 | }
1312 |
1313 | public function testCssNestingGlobalStyles()
1314 | {
1315 | global $allElements;
1316 | $scopeMyStyle = new ShadyStyles();
1317 | $enhancer = new Enhancer([
1318 | "elements" => $allElements,
1319 | "bodyContent" => false,
1320 | "enhancedAttr" => false,
1321 | "styleTransforms" => [
1322 | function ($params) use ($scopeMyStyle) {
1323 | return $scopeMyStyle->styleTransform($params);
1324 | },
1325 | ],
1326 | ]);
1327 |
1328 | $head = HeadTag();
1329 |
1330 | $htmlString = <<Test
1333 | HTML;
1334 |
1335 | $expectedString = <<
1337 |
1338 |
1339 |
1348 |
1349 | Test
1350 |
1351 | HTMLDOC;
1352 |
1353 | $this->assertSame(
1354 | strip($expectedString),
1355 | strip($enhancer->ssr($htmlString)),
1356 | "Global Styles with nesting worked"
1357 | );
1358 | }
1359 |
1360 | public function testUnicodeInCss()
1361 | {
1362 | global $allElements;
1363 | $scopeMyStyle = new ShadyStyles();
1364 | $enhancer = new Enhancer([
1365 | "elements" => $allElements,
1366 | "bodyContent" => false,
1367 | "enhancedAttr" => false,
1368 | "styleTransforms" => [
1369 | function ($params) use ($scopeMyStyle) {
1370 | return $scopeMyStyle->styleTransform($params);
1371 | },
1372 | ],
1373 | ]);
1374 |
1375 | $head = HeadTag();
1376 |
1377 | $htmlString = <<×
1380 | HTML;
1381 |
1382 | $expectedString = <<
1384 |
1385 |
1386 |
1389 |
1390 | ×
1391 |
1392 | HTMLDOC;
1393 |
1394 |
1395 | $this->assertSame(
1396 | strip($expectedString),
1397 | strip($enhancer->ssr($htmlString)),
1398 | "Global Styles with nesting worked"
1399 | );
1400 | }
1401 | public function testUnknownEncoding()
1402 | {
1403 | global $allElements;
1404 | $scopeMyStyle = new ShadyStyles();
1405 | $enhancer = new Enhancer([
1406 | "elements" => $allElements,
1407 | "bodyContent" => false,
1408 | "enhancedAttr" => false,
1409 | "styleTransforms" => [
1410 | function ($params) use ($scopeMyStyle) {
1411 | return $scopeMyStyle->styleTransform($params);
1412 | },
1413 | ],
1414 | ]);
1415 |
1416 |
1417 | $htmlString = 'hiسلام
の家庭に、9 ☆';
1418 |
1419 | $expectedString = <<
1421 |
1422 |
1423 | hi
سلام
の家庭に、9 ☆
1424 |
1425 |
1426 | HTMLDOC;
1427 |
1428 |
1429 | $this->assertSame(
1430 | strip($expectedString),
1431 | strip($enhancer->ssr($htmlString)),
1432 | "Global Styles with nesting worked"
1433 | );
1434 | }
1435 | }
1436 | function HeadTag()
1437 | {
1438 | return <<
1440 |
1441 | HTML;
1442 | }
1443 |
1444 | function loadFixtureHTML($name)
1445 | {
1446 | return file_get_contents(__DIR__ . "/fixtures/templates/$name");
1447 | }
1448 | function strip($str)
1449 | {
1450 | return preg_replace('/\r?\n|\r|\s\s+/u', "", $str);
1451 | }
1452 |
--------------------------------------------------------------------------------
/test/tests/IsCustomElementTests.php:
--------------------------------------------------------------------------------
1 | assertFalse(
13 | Enhancer::isCustomElement("Tag-Name"),
14 | "catches uppercase"
15 | );
16 | $this->assertFalse(
17 | Enhancer::isCustomElement("-tag-Name"),
18 | "catches starting dash"
19 | );
20 | $this->assertFalse(
21 | Enhancer::isCustomElement("1tag-name"),
22 | "catches digit"
23 | );
24 | $this->assertFalse(
25 | Enhancer::isCustomElement("1tag-name"),
26 | "catches digit"
27 | );
28 | $this->assertFalse(
29 | Enhancer::isCustomElement("font-face"),
30 | "catches reserved word"
31 | );
32 | $this->assertTrue(
33 | Enhancer::isCustomElement("tag-name"),
34 | "valid custom element"
35 | );
36 | $this->assertTrue(
37 | Enhancer::isCustomElement("tag-😊"),
38 | "unicode character"
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------