├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── publish-to-redaxo-org.yml ├── .gitignore ├── package.yml ├── LICENSE ├── fragments └── uk3 │ ├── accordeon_tabs.php │ └── card.php ├── README.md └── lib └── FORHtml └── FORHtml.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: skerbis 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | vendor/autoload.php 3 | vendor/composer/* 4 | vendor/airmanbzh/php-html-generator/tests/* 5 | vendor/airmanbzh/php-html-generator/phpunit.xml 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /package.yml: -------------------------------------------------------------------------------- 1 | package: forhtml 2 | version: '1.1.0' 3 | author: 'Friends Of REDAXO' 4 | supportpage: https://github.com/FriendsOfREDAXO/forhtml 5 | 6 | requires: 7 | redaxo: '^5.14' 8 | php: 9 | version: '>=8.1' 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot config reference 2 | # https://help.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: composer 7 | directory: / 8 | versioning-strategy: increase 9 | schedule: 10 | interval: monthly 11 | open-pull-requests-limit: 15 12 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-redaxo-org.yml: -------------------------------------------------------------------------------- 1 | # Instructions: https://github.com/FriendsOfREDAXO/installer-action/ 2 | 3 | name: Publish to REDAXO.org 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | jobs: 10 | redaxo_publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - if: hashFiles('composer.json') != '' 15 | uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: "8.2" 18 | - if: hashFiles('composer.json') != '' 19 | uses: ramsey/composer-install@v2 20 | with: 21 | composer-options: "--no-dev" 22 | - uses: FriendsOfREDAXO/installer-action@v1 23 | with: 24 | myredaxo-username: ${{ secrets.MYREDAXO_USERNAME }} 25 | myredaxo-api-key: ${{ secrets.MYREDAXO_API_KEY }} 26 | description: ${{ github.event.release.body }} 27 | version: ${{ github.event.release.tag_name }} 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Friends Of REDAXO 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /fragments/uk3/accordeon_tabs.php: -------------------------------------------------------------------------------- 1 | help) && $this->help === true) { 9 | $help = []; 10 | $help['info'] = 'Nimmt ein Array an und erstellt eine Tab oder Akkordeon Liste'; 11 | $help['type'] = 'Bei 1 > Akkordeon, bei 2 Tabs'; 12 | $help['items'] = 'Array mit den Keys title und body'; 13 | dump($help); 14 | } 15 | $values = []; 16 | 17 | if (isset($this->items) && is_array($this->items)) { 18 | $values = array_filter($this->items); 19 | } 20 | $type = 1; 21 | if (isset($this->type)) { 22 | $type = $this->type; 23 | } 24 | ?> 25 | 26 |
27 | > $values */ 29 | foreach ($values as $value) : ?> 30 |
31 | 32 | 33 | 34 |
35 | 36 |

37 | 38 | 39 | setVar('media', $value['media'], false); 42 | echo $fragment->parse('/uk3/gallery.php'); 43 | }?> 44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 | > $values */ 53 | foreach ($values as $value) : ?> 54 | 55 |
56 | 57 | 58 |
59 |
60 | > $values */ 62 | foreach ($values as $value) : ?> 63 | 64 |
65 | 66 | 67 |
68 |
69 | 70 | -------------------------------------------------------------------------------- /fragments/uk3/card.php: -------------------------------------------------------------------------------- 1 | help) && $this->help === true) { 14 | $help = []; 15 | $help['info'] = 'Das Fragment ezeugt UiKit-Cards: https://getuikit.com/assets/uikit/tests/card.html'; 16 | $help['media'] = 'Nimmt Markup für ein Medium / uk-media an (String)'; 17 | $help['media_bottom'] = 'Definiert ob das Medium am Ende dargestellt werden soll (bool)'; 18 | $help['title'] = 'Titel bzw. Header (String)'; 19 | $help['body'] = 'Body (String)'; 20 | $help['body_prepend'] = 'vor Body (String)'; 21 | $help['body_append'] = 'nach Body (String)'; 22 | $help['footer'] = 'Footer (String)'; 23 | $help['main_attributes'] = 'Hier können Attribute zur uk-card ergänzt werden (array), bei class werden diese an .uk-card angehägnt '; 24 | $help['body_attributes'] = 'Hier können Attribute zum body ergänzt werden (array), bei class werden diese angehägnt '; 25 | dump($help); 26 | } 27 | 28 | // main check if media and position are set 29 | if (isset($this->media) && $this->media !== '') { 30 | $media = '
' . $this->media . '
'; 31 | 32 | if (isset($this->media_bottom) && $this->media_bottom === true) { 33 | $media = ''; 34 | $media_bottom = '
' . $this->media . '
'; 35 | } 36 | } 37 | 38 | // main check if footer isset 39 | if (isset($this->footer) && $this->footer !== '') { 40 | $footer = ''; 41 | } 42 | 43 | 44 | 45 | 46 | // default is allways uk-card 47 | $main_attributes = []; 48 | $main_attributes['class'] = 'uk-cover-container uk-card'; 49 | if (isset($this->main_attributes) && is_array($this->main_attributes)) { 50 | $attributes = $this->main_attributes; 51 | if (array_key_exists('class', $this->main_attributes)) { 52 | $class = $this->main_attributes['class']; 53 | $main_attributes['class'] = 'uk-cover-container uk-card ' . $class; 54 | } 55 | } 56 | $attributes_main = rex_string::buildAttributes($main_attributes); 57 | // default body is allways uk-card-body uk-padding-small 58 | $body_attributes = []; 59 | $body_attributes['class'] = 'uk-card-body uk-padding-small stretch_body'; 60 | if (isset($this->body_attributes) && is_array($this->body_attributes)) { 61 | $attributes = $this->body_attributes; 62 | if (array_key_exists('class', $this->body_attributes)) { 63 | $class = $this->body_attributes['class']; 64 | $body_attributes['class'] = 'uk-card-body ' . $class; 65 | } 66 | } 67 | $attributes_body = rex_string::buildAttributes($body_attributes); 68 | ?> 69 | > 70 |
71 | 72 | title) && is_string($this->title) && $this->title !== '') : ?> 73 |
74 |

title ?>

75 |
76 | 77 | body_prepend) && is_string($this->body_prepend) && $this->body_prepend !== '') : ?> 78 | body_prepend ?> 79 | 80 | body) && is_string($this->body) && $this->body !== '') : ?> 81 | > 82 | body ?> 83 |
84 | 85 | body_append) && is_string($this->body_append) && $this->body_append !== '') : ?> 86 | body_append ?> 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FORHtml 2 | 3 | PHP HTML-Generator für REDAXO CMS 4 | 5 | ## Installation 6 | 7 | Über das REDAXO-Backend installieren oder: 8 | 9 | ```bash 10 | composer require friends-of-redaxo/for-html 11 | ``` 12 | 13 | ## Grundlegende Verwendung 14 | 15 | ### HTML-Element erstellen 16 | 17 | ```php 18 | $div = FORHtml::createElement('div'); 19 | echo $div; // Ausgabe:
20 | 21 | $paragraph = FORHtml::createElement('p')->text('Inhalt'); 22 | echo $paragraph; // Ausgabe:

Inhalt

23 | ``` 24 | 25 | ### Verschachtelte Elemente 26 | 27 | ```php 28 | $container = FORHtml::createElement('div') 29 | ->addClass('container') 30 | ->addElement('a') 31 | ->set('href', './seite.php') 32 | ->text('Mein Link') 33 | ->getParent() // Zurück zum Container 34 | ->addElement('p') 35 | ->text('Mein Text'); 36 | 37 | // Ausgabe: 38 | //
39 | // Mein Link 40 | //

Mein Text

41 | //
42 | ``` 43 | 44 | ## Attribute verwalten 45 | 46 | ### Attribute setzen 47 | 48 | ```php 49 | $link = FORHtml::createElement('a') 50 | ->set('href', './beispiel.php') 51 | ->set('id', 'meinLink') 52 | ->text('Mein Link'); 53 | 54 | // Alternativ mehrere Attribute gleichzeitig: 55 | $link = FORHtml::createElement('a') 56 | ->set([ 57 | 'href' => './beispiel.php', 58 | 'id' => 'meinLink', 59 | 'title' => 'Mehr Information' 60 | ]) 61 | ->text('Mein Link'); 62 | ``` 63 | 64 | ### ID setzen (Kurzform) 65 | 66 | ```php 67 | $div = FORHtml::createElement('div') 68 | ->id('meinContainer'); 69 | ``` 70 | 71 | ### CSS-Klassen verwalten 72 | 73 | ```php 74 | $div = FORHtml::createElement('div') 75 | ->addClass('primär') 76 | ->addClass('hervorgehoben'); 77 | // Ausgabe:
78 | 79 | $div->removeClass('hervorgehoben'); 80 | // Ausgabe:
81 | ``` 82 | 83 | ## REDAXO-spezifische Funktionen 84 | 85 | ### Media Manager Integration 86 | 87 | ```php 88 | $bild = FORHtml::createElement('img') 89 | ->addClass('uk-width-1-1') 90 | ->set('alt', 'Produktbild') 91 | ->mmfile('thumbnail', 'produkt.jpg'); 92 | 93 | // Ausgabe: 94 | // Produktbild 96 | ``` 97 | 98 | ### Fragment-System 99 | 100 | ```php 101 | $card = FORHtml::createElement('div') 102 | ->addClass('card') 103 | ->parseFragment('/fragments/card.php', [ 104 | 'title' => 'Überschrift', 105 | 'text' => 'Beschreibung', 106 | 'image' => 'bild.jpg' 107 | ]); 108 | ``` 109 | 110 | ## Sicherheit und Konfiguration 111 | 112 | ### XSS-Schutz aktivieren 113 | 114 | ```php 115 | FORHtml::$avoidXSS = true; 116 | 117 | $div = FORHtml::createElement('div') 118 | ->text(''); 119 | // Text wird automatisch escaped 120 | ``` 121 | 122 | ### Ausgabeformat konfigurieren 123 | 124 | ```php 125 | // HTML5 (Standard) 126 | FORHtml::$outputLanguage = ENT_HTML5; 127 | 128 | // XML/XHTML 129 | FORHtml::$outputLanguage = ENT_XML1; 130 | ``` 131 | 132 | ## Element-Navigation 133 | 134 | ```php 135 | $container = FORHtml::createElement('div'); 136 | $first = $container->addElement('p')->text('Erster'); 137 | $second = $container->addElement('p')->text('Zweiter'); 138 | $third = $container->addElement('p')->text('Dritter'); 139 | 140 | // Navigation 141 | $element = $second 142 | ->getFirst(); // Ersten Absatz holen 143 | ->getNext(); // Nächsten Absatz 144 | ->getPrevious(); // Vorherigen Absatz 145 | ->getLast(); // Letzten Absatz 146 | ->getParent(); // Container 147 | ->getTop(); // Wurzelelement 148 | 149 | // Element entfernen 150 | $second->remove(); 151 | ``` 152 | 153 | ## Praktische Beispiele 154 | 155 | ### UIkit-Cards aus YForm-Daten 156 | 157 | ```php 158 | $tableUrlaubsziele = rex_yform_manager_table::get('rex_urlaubsziele'); 159 | $urlaubsziele = $tableUrlaubsziele->query()->find(); 160 | 161 | foreach ($urlaubsziele as $ziel) { 162 | $mediaList = isset($ziel->media) ? array_filter(explode(",", $ziel->media)) : []; 163 | 164 | if (!empty($mediaList)) { 165 | $media = FORHtml::createElement('img') 166 | ->addClass('uk-width-1-1') 167 | ->set('alt', 'Bild: ' . $ziel->title) 168 | ->set('uk-tooltip', '') 169 | ->mmfile('card_image', $mediaList[0]); 170 | } 171 | 172 | $card = FORHtml::createElement('div') 173 | ->parseFragment('/uk3/card.php', [ 174 | 'media' => $media ?? '', 175 | 'title' => $ziel->title, 176 | 'body' => $ziel->infotext, 177 | ]) 178 | ->addClass('uk-card-default'); 179 | 180 | $cards[] = $card; 181 | } 182 | 183 | // Container für alle Cards 184 | if ($cards) { 185 | echo FORHtml::createElement('div') 186 | ->addClass('uk-section uk-padding-large') 187 | ->addElement('div') 188 | ->addClass('uk-container') 189 | ->addElement('div') 190 | ->addClass('uk-child-width-1-3@m uk-grid-match') 191 | ->set('uk-grid', '') 192 | ->text(implode('', $cards)); 193 | } 194 | ``` 195 | 196 | ### Navigation mit NavigationArray 197 | 198 | ```php 199 | function generateUikit3Navigation(int $startCategoryId = 0, int $depth = 4): string 200 | { 201 | $navArray = BuildArray::create() 202 | ->setStart($startCategoryId) 203 | ->setDepth($depth) 204 | ->generate(); 205 | 206 | $nav = FORHtml::createElement('ul') 207 | ->addClass('uk-nav uk-nav-default'); 208 | 209 | foreach ($navArray as $item) { 210 | $li = $nav->addElement('li'); 211 | 212 | if ($item['active']) { 213 | $li->addClass('uk-active'); 214 | } 215 | 216 | $li->addElement('a') 217 | ->set('href', $item['url']) 218 | ->text($item['catName']); 219 | 220 | if (!empty($item['children'])) { 221 | $li->addElement(generateSubMenu($item['children'])); 222 | } 223 | } 224 | 225 | return $nav->toString(); 226 | } 227 | 228 | function generateSubMenu(array $children): FORHtml 229 | { 230 | $subNav = FORHtml::createElement('ul') 231 | ->addClass('uk-nav-sub'); 232 | 233 | foreach ($children as $child) { 234 | $li = $subNav->addElement('li'); 235 | 236 | if ($child['active']) { 237 | $li->addClass('uk-active'); 238 | } 239 | 240 | $li->addElement('a') 241 | ->set('href', $child['url']) 242 | ->text($child['catName']); 243 | 244 | if (!empty($child['children'])) { 245 | $li->addElement(generateSubMenu($child['children'])); 246 | } 247 | } 248 | 249 | return $subNav; 250 | } 251 | ``` 252 | 253 | ## Technische Details 254 | 255 | ### Selbstschließende Tags 256 | 257 | Folgende Tags werden automatisch als selbstschließend behandelt: 258 | - img, br, hr, input, area, link, meta 259 | - param, base, col, command, keygen 260 | - source, track, wbr 261 | 262 | ```php 263 | $img = FORHtml::createElement('img') 264 | ->set('src', 'bild.jpg'); 265 | // Ausgabe: 266 | ``` 267 | 268 | --- 269 | 270 | ## Credits 271 | 272 | Basierend auf [PHP HTML Generator](https://github.com/Airmanbzh/php-html-generator) von Airmanbzh. 273 | 274 | ## Support 275 | 276 | Bei Fragen oder Problemen bitte ein [GitHub Issue](https://github.com/FriendsOfREDAXO/for-html/issues) erstellen. 277 | -------------------------------------------------------------------------------- /lib/FORHtml/FORHtml.php: -------------------------------------------------------------------------------- 1 | tag = $tag; 31 | $this->top = $top; 32 | $this->autoclosed = in_array($this->tag, $this->autocloseTagsList); 33 | } 34 | 35 | public static function __callStatic(string $tag, array $content): FORHtml 36 | { 37 | return self::createElement($tag) 38 | ->attr(count($content) && is_array($content[0]) ? array_pop($content) : []) 39 | ->text(implode('', $content)); 40 | } 41 | 42 | public function __call(string $tag, array $content): FORHtml 43 | { 44 | return $this 45 | ->addElement($tag) 46 | ->attr(count($content) && is_array($content[0]) ? array_pop($content) : []) 47 | ->text(implode('', $content)); 48 | } 49 | 50 | public function __invoke(): FORHtml 51 | { 52 | return $this->getParent(); 53 | } 54 | 55 | public static function createElement(string $tag = ''): FORHtml 56 | { 57 | self::$instance = new static($tag); 58 | return self::$instance; 59 | } 60 | 61 | public function addElement(string $tag = ''): FORHtml 62 | { 63 | $htmlTag = (is_object($tag) && $tag instanceof self) ? clone $tag : new static($tag); 64 | $htmlTag->top = $this->getTop(); 65 | $htmlTag->parent = $this; 66 | $this->content[] = $htmlTag; 67 | return $htmlTag; 68 | } 69 | 70 | public function set(array|string $attribute, ?string $value = null): FORHtml 71 | { 72 | if (is_array($attribute)) { 73 | foreach ($attribute as $key => $value) { 74 | $this[$key] = $value; 75 | } 76 | } else { 77 | $this[$attribute] = $value; 78 | } 79 | return $this; 80 | } 81 | 82 | public function attr(array|string $attribute, ?string $value = null): FORHtml 83 | { 84 | return $this->set(...func_get_args()); 85 | } 86 | 87 | public function offsetExists(mixed $attribute): bool 88 | { 89 | return isset($this->attributeList[$attribute]); 90 | } 91 | 92 | public function offsetGet(mixed $attribute): mixed 93 | { 94 | return $this->attributeList[$attribute] ?? null; 95 | } 96 | 97 | public function offsetSet(mixed $attribute, mixed $value): void 98 | { 99 | $this->attributeList[$attribute] = $value; 100 | } 101 | 102 | public function offsetUnset(mixed $attribute): void 103 | { 104 | unset($this->attributeList[$attribute]); 105 | } 106 | 107 | public function text(string $value): FORHtml 108 | { 109 | $this->addElement('')->text = static::$avoidXSS ? static::unXSS($value) : $value; 110 | return $this; 111 | } 112 | 113 | public function getTop(): FORHtml 114 | { 115 | return $this->top ?? $this; 116 | } 117 | 118 | public function getParent(): ?FORHtml 119 | { 120 | return $this->parent; 121 | } 122 | 123 | public function getFirst(): ?FORHtml 124 | { 125 | return $this->parent->content[0] ?? null; 126 | } 127 | 128 | public function getPrevious(): FORHtml 129 | { 130 | $prev = $this; 131 | if ($this->parent !== null) { 132 | foreach ($this->parent->content as $c) { 133 | if ($c === $this) { 134 | break; 135 | } 136 | $prev = $c; 137 | } 138 | } 139 | return $prev; 140 | } 141 | 142 | public function getNext(): ?FORHtml 143 | { 144 | $next = null; 145 | if ($this->parent !== null) { 146 | $found = false; 147 | foreach ($this->parent->content as $c) { 148 | if ($found) { 149 | $next = $c; 150 | break; 151 | } 152 | if ($c === $this) { 153 | $found = true; 154 | } 155 | } 156 | } 157 | return $next; 158 | } 159 | 160 | public function getLast(): ?FORHtml 161 | { 162 | return $this->parent->content[array_key_last($this->parent->content)] ?? null; 163 | } 164 | 165 | public function remove(): ?FORHtml 166 | { 167 | if ($this->parent !== null) { 168 | foreach ($this->parent->content as $key => $value) { 169 | if ($value === $this) { 170 | unset($this->parent->content[$key]); 171 | } 172 | } 173 | } 174 | return null; 175 | } 176 | 177 | public function __toString(): string 178 | { 179 | return $this->getTop()->toString(); 180 | } 181 | 182 | public function toString(): string 183 | { 184 | $string = ''; 185 | if (!empty($this->tag)) { 186 | $string .= "<{$this->tag}" . $this->attributesToString(); 187 | $string .= $this->autoclosed ? '/>' : ">{$this->contentToString()}tag}>"; 188 | } else { 189 | $string .= $this->text . $this->contentToString(); 190 | } 191 | return $string; 192 | } 193 | 194 | protected function attributesToString(): string 195 | { 196 | $string = ''; 197 | $XMLConvention = in_array(static::$outputLanguage, [ENT_XML1, ENT_XHTML]); 198 | foreach ($this->attributeList as $key => $value) { 199 | if ($value !== null && ($value !== false || $XMLConvention)) { 200 | if (is_array($value)) { 201 | $value = implode(' ', $value); 202 | } 203 | $escapedValue = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); 204 | $string .= " {$key}=\"{$escapedValue}\""; 205 | } 206 | } 207 | return $string; 208 | } 209 | 210 | protected function contentToString(): string 211 | { 212 | return array_reduce($this->content, fn($carry, $c) => $carry . $c->toString(), ''); 213 | } 214 | 215 | public static function unXSS(string $input): string 216 | { 217 | return htmlentities($input, ENT_QUOTES | ENT_DISALLOWED | static::$outputLanguage); 218 | } 219 | 220 | // Methods from HtmlTag 221 | public function id(string $value): FORHtml 222 | { 223 | return $this->set('id', $value); 224 | } 225 | 226 | public function addClass(string $value): FORHtml 227 | { 228 | if (!isset($this->attributeList['class']) || is_null($this->attributeList['class'])) { 229 | $this->attributeList['class'] = []; 230 | } 231 | $this->attributeList['class'][] = $value; 232 | return $this; 233 | } 234 | 235 | public function removeClass(string $value): FORHtml 236 | { 237 | if (!is_null($this->attributeList['class'])) { 238 | unset($this->attributeList['class'][array_search($value, $this->attributeList['class'])]); 239 | } 240 | return $this; 241 | } 242 | 243 | public function mmfile(string $type = 'default', string $file =''): string 244 | { 245 | return $this->set('src', rex_media_manager::getUrl($type, $file)); 246 | } 247 | 248 | public function content(string $content =''): string 249 | { 250 | return $this->text($content); 251 | } 252 | 253 | // Neue Methode für Fragmente 254 | public function parseFragment(string $template, array $vars = []): FORHtml 255 | { 256 | $fragment = new rex_fragment(); 257 | foreach ($vars as $key => $value) { 258 | $fragment->setVar($key, $value, false); 259 | } 260 | $this->text($fragment->parse($template)); 261 | return $this; 262 | } 263 | } 264 | --------------------------------------------------------------------------------