├── LICENSE ├── autoload.php ├── composer.json ├── docs-generator.php └── src ├── HTMLServerComponent.php └── HTMLServerComponentsCompiler.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Ivo Petkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | __DIR__ . '/src/HTMLServerComponent.php', 12 | 'IvoPetkov\HTMLServerComponentsCompiler' => __DIR__ . '/src/HTMLServerComponentsCompiler.php' 13 | ); 14 | 15 | spl_autoload_register(function ($class) use ($classes): void { 16 | if (isset($classes[$class])) { 17 | require $classes[$class]; 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ivopetkov/html-server-components-compiler", 3 | "description": "HTML Server Components compiler", 4 | "keywords": [ 5 | "HTML Server Components compiler", 6 | "HTML" 7 | ], 8 | "homepage": "https://github.com/ivopetkov/html-server-components-compiler", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Ivo Petkov", 13 | "email": "ivo@ivopetkov.com", 14 | "homepage": "http://ivopetkov.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "8.0.*|8.1.*|8.2.*|8.3.*|8.4.*", 19 | "ivopetkov/html5-dom-document-php": "^2.5" 20 | }, 21 | "require-dev": { 22 | "ivopetkov/docs-generator": "0.1.*" 23 | }, 24 | "autoload": { 25 | "files": [ 26 | "autoload.php" 27 | ] 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "": "tests/utilities/" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /docs-generator.php: -------------------------------------------------------------------------------- 1 | generateMarkdown(__DIR__ . '/docs/markdown'); 14 | -------------------------------------------------------------------------------- /src/HTMLServerComponent.php: -------------------------------------------------------------------------------- 1 | attributes[$name]) ? (string) $this->attributes[$name] : $defaultValue; 50 | } 51 | 52 | /** 53 | * Sets a new value for attribute specified. 54 | * 55 | * @param string $name The name of the attribute. 56 | * @param string $value The new value for attribute. 57 | * @return void No value is returned. 58 | */ 59 | public function setAttribute(string $name, $value) 60 | { 61 | $this->attributes[strtolower($name)] = $value; 62 | } 63 | 64 | /** 65 | * Removes an attribute. 66 | * 67 | * @param string $name The name of the attribute. 68 | * @return void No value is returned. 69 | */ 70 | public function removeAttribute(string $name) 71 | { 72 | $name = strtolower($name); 73 | if (isset($this->attributes[$name])) { 74 | unset($this->attributes[$name]); 75 | } 76 | } 77 | 78 | /** 79 | * Returns an array containing all attributes. 80 | * 81 | * @return array An associative array containing all attributes. 82 | */ 83 | public function getAttributes(): array 84 | { 85 | return $this->attributes; 86 | } 87 | 88 | /** 89 | * Sets the attributes specified. 90 | * 91 | * @param array $attributes An associative array containing the attributes to set. 92 | * @return void 93 | */ 94 | public function setAttributes(array $attributes) 95 | { 96 | foreach ($attributes as $name => $value) { 97 | $this->attributes[strtolower($name)] = $value; 98 | } 99 | } 100 | 101 | /** 102 | * Removes the attributes specified. 103 | * 104 | * @param array $attributes An array containing the names of the attributes to remove. 105 | * @return void 106 | */ 107 | public function removeAttributes(array $attributes) 108 | { 109 | foreach ($attributes as $name) { 110 | $name = strtolower($name); 111 | if (isset($this->attributes[$name])) { 112 | unset($this->attributes[$name]); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * Provides access to the component attributes via properties. 119 | * 120 | * @param string $name The name of the attribute. 121 | * @return string|null The value of the attribute or null if missing. 122 | */ 123 | public function __get($name) 124 | { 125 | $name = strtolower($name); 126 | return isset($this->attributes[$name]) ? (string) $this->attributes[$name] : null; 127 | } 128 | 129 | /** 130 | * Provides access to the component attributes via properties. 131 | * 132 | * @param string $name The name of the attribute. 133 | * @param string $value The new value of the attribute. 134 | * @return void No value is returned. 135 | */ 136 | public function __set(string $name, $value) 137 | { 138 | $this->attributes[strtolower($name)] = $value; 139 | } 140 | 141 | /** 142 | * Provides access to the component attributes via properties. 143 | * 144 | * @param string $name The name of the attribute. 145 | * @return boolean TRUE if the attribute exists, FALSE otherwise. 146 | */ 147 | public function __isset(string $name): bool 148 | { 149 | return isset($this->attributes[strtolower($name)]); 150 | } 151 | 152 | /** 153 | * Provides access to the component attributes via properties. 154 | * 155 | * @param string $name The name of the attribute. 156 | * @return void No value is returned. 157 | */ 158 | public function __unset(string $name) 159 | { 160 | $name = strtolower($name); 161 | if (isset($this->attributes[$name])) { 162 | unset($this->attributes[$name]); 163 | } 164 | } 165 | 166 | /** 167 | * Returns a HTML representation of the component. 168 | * 169 | * @return string A HTML representation of the component. 170 | */ 171 | public function __toString(): string 172 | { 173 | $html = '<' . $this->tagName; 174 | foreach ($this->attributes as $name => $value) { 175 | $html .= ' ' . $name . '="' . htmlspecialchars((string)$value) . '"'; 176 | } 177 | return $html . '>' . $this->innerHTML . 'tagName . '>'; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/HTMLServerComponentsCompiler.php: -------------------------------------------------------------------------------- 1 | aliases[$alias] = $original; 49 | } 50 | 51 | /** 52 | * Defines a new tag. 53 | * 54 | * @param string $tagName The tag name. 55 | * @param string $src The tag source. 56 | * @return void No value is returned. 57 | * @throws \InvalidArgumentException 58 | */ 59 | public function addTag(string $tagName, string $src) 60 | { 61 | if (preg_match('/^[a-z0-9\-]+$/', $tagName) !== 1) { 62 | throw new \InvalidArgumentException('The tag name provided is not valid! It may contain letters (a-z), numbers (0-9) and dashes (-).'); 63 | } 64 | $this->tags[strtolower(trim($tagName))] = $src; 65 | } 66 | 67 | /** 68 | * Converts components code (if any) into HTML code. 69 | * 70 | * @param string|\IvoPetkov\HTMLServerComponent $content The content to be processed. 71 | * @param array $options Compiler options. 72 | * @return string The result HTML code. 73 | */ 74 | public function process($content, array $options = []) 75 | { 76 | $tagNames = array_keys($this->tags); 77 | $tagNames[] = 'component'; 78 | if (is_string($content)) { 79 | $found = false; 80 | foreach ($tagNames as $tagName) { 81 | if (strpos($content, '<' . $tagName) !== false) { 82 | $found = true; 83 | break; 84 | } 85 | } 86 | if (!$found) { 87 | return $content; 88 | } 89 | } elseif (!($content instanceof \IvoPetkov\HTMLServerComponent)) { 90 | throw new \InvalidArgumentException(''); 91 | } 92 | 93 | $getComponentFileContent = static function ($file, $component, $variables) { 94 | if (is_file($file)) { 95 | $__componentFile = $file; 96 | unset($file); 97 | if (!empty($variables)) { 98 | extract($variables, EXTR_SKIP); 99 | } 100 | unset($variables); 101 | ob_start(); 102 | include $__componentFile; 103 | $content = ob_get_clean(); 104 | return $content; 105 | } else { 106 | throw new \Exception('Component file cannot be found (' . $file . ')'); 107 | } 108 | }; 109 | 110 | $getComponentResultHTML = function ($component) use (&$getComponentFileContent, $options) { 111 | $srcAttributeValue = $component->getAttribute('src'); 112 | if ($srcAttributeValue === null) { 113 | if (isset($this->tags[$component->tagName])) { 114 | $srcAttributeValue = $this->tags[$component->tagName]; 115 | } else { 116 | throw new \Exception('Component tag name is not defined at ' . (string) $component . '!'); 117 | } 118 | } 119 | if ($srcAttributeValue !== null) { 120 | // todo check alias of alias 121 | $sourceParts = explode(':', isset($this->aliases[$srcAttributeValue]) ? $this->aliases[$srcAttributeValue] : $srcAttributeValue, 2); 122 | if (isset($sourceParts[0], $sourceParts[1])) { 123 | $scheme = $sourceParts[0]; 124 | if ($scheme === 'data') { 125 | if (substr($sourceParts[1], 0, 7) === 'base64,') { 126 | return base64_decode(substr($sourceParts[1], 7)); //$this->process(, isset($componentOptions) ? $componentOptions : $options); 127 | } 128 | throw new \Exception('Components data URI scheme only supports base64 (data:base64,ABCD...)!'); 129 | } elseif ($scheme === 'file') { 130 | return $getComponentFileContent(urldecode($sourceParts[1]), $component, isset($options['variables']) && is_array($options['variables']) ? $options['variables'] : []); //$this->process(isset($componentOptions) ? $componentOptions : $options); 131 | } 132 | throw new \Exception('Components URI scheme not valid! It must be \'file:\', \'data:\' or an alias.'); 133 | } 134 | throw new \Exception('Components URI scheme or alias not found at ' . (string) $component . '!'); 135 | } 136 | throw new \Exception('Component src attribute is missing at ' . (string) $component . '!'); 137 | }; 138 | 139 | $disableLevelProcessing = false; 140 | $domDocument = new HTML5DOMDocument(); 141 | if ($content instanceof \IvoPetkov\HTMLServerComponent) { 142 | $domDocument->loadHTML($getComponentResultHTML($content), HTML5DOMDocument::ALLOW_DUPLICATE_IDS); 143 | if (isset($options['recursive']) && $options['recursive'] === false) { 144 | $disableLevelProcessing = true; 145 | } 146 | } else { 147 | $domDocument->loadHTML($content, HTML5DOMDocument::ALLOW_DUPLICATE_IDS); 148 | } 149 | if (!$disableLevelProcessing) { 150 | $tagsQuerySelector = implode(',', $tagNames); 151 | for ($level = 0; $level < 1000; $level++) { 152 | $componentElements = $domDocument->querySelectorAll($tagsQuerySelector); 153 | if ($componentElements->length === 0) { 154 | break; 155 | } 156 | $insertHTMLSources = []; 157 | $list = []; // Save the elements into an array because removeChild() messes up the NodeList 158 | foreach ($componentElements as $index => $componentElement) { 159 | $parentNode = $componentElement->parentNode; 160 | $list[$index] = [$componentElement, $parentNode, []]; // The last one will contain the parents tag names 161 | while ($parentNode !== null && isset($parentNode->tagName)) { 162 | $tagName = $parentNode->tagName; 163 | $list[$index][2][] = $tagName; 164 | if ($tagName === 'head' || $tagName === 'body') { 165 | break; 166 | } 167 | $parentNode = $parentNode->parentNode; 168 | } 169 | } 170 | foreach ($list as $index => $componentData) { 171 | if (!empty(array_intersect($componentData[2], $tagNames))) { 172 | continue; 173 | } 174 | $componentElement = $componentData[0]; 175 | $component = $this->makeComponent($componentElement->getAttributes(), $componentElement->innerHTML, $componentElement->tagName); 176 | $componentResultHTML = $getComponentResultHTML($component); 177 | if (array_search('body', $componentData[2]) !== false) { 178 | $insertTargetName = 'html-server-components-compiler-insert-target-' . $index; 179 | $componentData[1]->insertBefore($domDocument->createInsertTarget($insertTargetName), $componentElement); 180 | $componentData[1]->removeChild($componentElement); // must be before insertHTML because a duplicate elements IDs can occur. 181 | $insertHTMLSources[] = ['source' => $componentResultHTML, 'target' => $insertTargetName]; 182 | } else { 183 | $componentData[1]->removeChild($componentElement); 184 | $insertHTMLSources[] = ['source' => $componentResultHTML]; 185 | } 186 | } 187 | $domDocument->insertHTMLMulti($insertHTMLSources); 188 | if (isset($options['recursive']) && $options['recursive'] === false) { 189 | break; 190 | } 191 | } 192 | } 193 | 194 | $domDocument->modify(HTML5DOMDocument::FIX_MULTIPLE_TITLES | HTML5DOMDocument::FIX_DUPLICATE_METATAGS | HTML5DOMDocument::FIX_MULTIPLE_HEADS | HTML5DOMDocument::FIX_MULTIPLE_BODIES | HTML5DOMDocument::OPTIMIZE_HEAD | HTML5DOMDocument::FIX_DUPLICATE_STYLES); 195 | return $domDocument->saveHTML(); 196 | } 197 | 198 | /** 199 | * Constructs a component object. 200 | * 201 | * @param array $attributes The attributes of the component object. 202 | * @param string $innerHTML The innerHTML of the component object. 203 | * @param string $tagName The tag name of the component object. 204 | * @return \IvoPetkov\HTMLServerComponent A component object. 205 | */ 206 | public function makeComponent(array $attributes = [], string $innerHTML = '', string $tagName = 'component') 207 | { 208 | if (self::$newComponentCache === null) { 209 | self::$newComponentCache = new \IvoPetkov\HTMLServerComponent(); 210 | } 211 | $component = clone (self::$newComponentCache); 212 | $component->setAttributes($attributes); 213 | $component->innerHTML = $innerHTML; 214 | $component->tagName = $tagName; 215 | return $component; 216 | } 217 | } 218 | --------------------------------------------------------------------------------