HTML::comment()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 13
ccs 7
cts 7
cp 1
crap 2
rs 10
1
<?php
2
3
/**
4
 * @author Marwan Al-Soltany <[email protected]>
5
 * @copyright Marwan Al-Soltany 2021
6
 * For the full copyright and license information, please view
7
 * the LICENSE file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace MAKS\Velox\Frontend;
13
14
use MAKS\Velox\Helper\Misc;
15
16
/**
17
 * A class that serves as a fluent interface to write HTML in PHP. It also helps with creating HTML elements on the fly.
18
 *
19
 * Example:
20
 * ```
21
 * // return an HTML element by using some tag name as a static method
22
 * // div can be replaced by any other html element name
23
 * $html = HTML::div('This is a div!', ['class' => 'container']);
24
 *
25
 * // structuring deeply nested elements using wrapping methods
26
 * // ->condition() to make the next action conditional
27
 * // ->execute() to execute some logic (loops, complex if-statements)
28
 * // ->open() and ->close() for containing elements
29
 * // ->{$tagName}() or ->element() for elements, ->entity() for entities, ->comment() for comments
30
 * // ->echo() or ->return() for retrieving the final result
31
 * (new HTML())
32
 *     ->element('h1', 'HTML Forms', ['class' => 'title'])
33
 *     ->open('form', ['method' => 'POST'])
34
 *         ->h2('Example', ['class' => 'subtitle'])
35
 *         ->p('This is an example form.')
36
 *         ->br(null)
37
 *         ->condition($someVar === true)->div('The var was true')
38
 *         ->open('fieldset')
39
 *             ->legend('Form 1', ['style' => 'color: #555;'])
40
 *             ->label('Message: ', ['class' => 'text'])
41
 *             ->input(null, ['type' => 'text', 'required'])
42
 *             ->entity('nbsp', 2)
43
 *             ->input(null, ['type' => 'submit', 'value' => 'Submit'])
44
 *         ->close()
45
 *         ->condition(count($errors))
46
 *         ->open('ul', ['class' => 'errors'])
47
 *             ->execute(function () use ($errors) {
48
 *                 foreach ($errors as $error) {
49
 *                     $this->li($error);
50
 *                 }
51
 *             })
52
 *         ->close()
53
 *     ->close()
54
 * ->echo();
55
 * ```
56
 *
57
 * @package Velox\Frontend
58
 * @since 1.0.0
59
 * @api
60
 *
61
 * @method HTML a(?string $content = '', array $attributes = [])
62
 * @method HTML abbr(?string $content = '', array $attributes = [])
63
 * @method HTML address(?string $content = '', array $attributes = [])
64
 * @method HTML area(?string $content = '', array $attributes = [])
65
 * @method HTML article(?string $content = '', array $attributes = [])
66
 * @method HTML aside(?string $content = '', array $attributes = [])
67
 * @method HTML audio(?string $content = '', array $attributes = [])
68
 * @method HTML b(?string $content = '', array $attributes = [])
69
 * @method HTML base(?string $content = '', array $attributes = [])
70
 * @method HTML bdi(?string $content = '', array $attributes = [])
71
 * @method HTML bdo(?string $content = '', array $attributes = [])
72
 * @method HTML blockquote(?string $content = '', array $attributes = [])
73
 * @method HTML body(?string $content = '', array $attributes = [])
74
 * @method HTML br(?string $content = '', array $attributes = [])
75
 * @method HTML button(?string $content = '', array $attributes = [])
76
 * @method HTML canvas(?string $content = '', array $attributes = [])
77
 * @method HTML caption(?string $content = '', array $attributes = [])
78
 * @method HTML cite(?string $content = '', array $attributes = [])
79
 * @method HTML code(?string $content = '', array $attributes = [])
80
 * @method HTML col(?string $content = '', array $attributes = [])
81
 * @method HTML colgroup(?string $content = '', array $attributes = [])
82
 * @method HTML data(?string $content = '', array $attributes = [])
83
 * @method HTML datalist(?string $content = '', array $attributes = [])
84
 * @method HTML dd(?string $content = '', array $attributes = [])
85
 * @method HTML del(?string $content = '', array $attributes = [])
86
 * @method HTML details(?string $content = '', array $attributes = [])
87
 * @method HTML dfn(?string $content = '', array $attributes = [])
88
 * @method HTML dialog(?string $content = '', array $attributes = [])
89
 * @method HTML div(?string $content = '', array $attributes = [])
90
 * @method HTML dl(?string $content = '', array $attributes = [])
91
 * @method HTML dt(?string $content = '', array $attributes = [])
92
 * @method HTML em(?string $content = '', array $attributes = [])
93
 * @method HTML embed(?string $content = '', array $attributes = [])
94
 * @method HTML fieldset(?string $content = '', array $attributes = [])
95
 * @method HTML figcaption(?string $content = '', array $attributes = [])
96
 * @method HTML figure(?string $content = '', array $attributes = [])
97
 * @method HTML footer(?string $content = '', array $attributes = [])
98
 * @method HTML form(?string $content = '', array $attributes = [])
99
 * @method HTML h1(?string $content = '', array $attributes = [])
100
 * @method HTML h2(?string $content = '', array $attributes = [])
101
 * @method HTML h3(?string $content = '', array $attributes = [])
102
 * @method HTML h4(?string $content = '', array $attributes = [])
103
 * @method HTML h5(?string $content = '', array $attributes = [])
104
 * @method HTML h6(?string $content = '', array $attributes = [])
105
 * @method HTML head(?string $content = '', array $attributes = [])
106
 * @method HTML header(?string $content = '', array $attributes = [])
107
 * @method HTML hr(?string $content = '', array $attributes = [])
108
 * @method HTML html(?string $content = '', array $attributes = [])
109
 * @method HTML i(?string $content = '', array $attributes = [])
110
 * @method HTML iframe(?string $content = '', array $attributes = [])
111
 * @method HTML img(?string $content = '', array $attributes = [])
112
 * @method HTML input(?string $content = '', array $attributes = [])
113
 * @method HTML ins(?string $content = '', array $attributes = [])
114
 * @method HTML kbd(?string $content = '', array $attributes = [])
115
 * @method HTML label(?string $content = '', array $attributes = [])
116
 * @method HTML legend(?string $content = '', array $attributes = [])
117
 * @method HTML li(?string $content = '', array $attributes = [])
118
 * @method HTML link(?string $content = '', array $attributes = [])
119
 * @method HTML main(?string $content = '', array $attributes = [])
120
 * @method HTML map(?string $content = '', array $attributes = [])
121
 * @method HTML mark(?string $content = '', array $attributes = [])
122
 * @method HTML meta(?string $content = '', array $attributes = [])
123
 * @method HTML meter(?string $content = '', array $attributes = [])
124
 * @method HTML nav(?string $content = '', array $attributes = [])
125
 * @method HTML noscript(?string $content = '', array $attributes = [])
126
 * @method HTML object(?string $content = '', array $attributes = [])
127
 * @method HTML p(?string $content = '', array $attributes = [])
128
 * @method HTML param(?string $content = '', array $attributes = [])
129
 * @method HTML picture(?string $content = '', array $attributes = [])
130
 * @method HTML pre(?string $content = '', array $attributes = [])
131
 * @method HTML progress(?string $content = '', array $attributes = [])
132
 * @method HTML q(?string $content = '', array $attributes = [])
133
 * @method HTML rp(?string $content = '', array $attributes = [])
134
 * @method HTML rt(?string $content = '', array $attributes = [])
135
 * @method HTML ruby(?string $content = '', array $attributes = [])
136
 * @method HTML s(?string $content = '', array $attributes = [])
137
 * @method HTML samp(?string $content = '', array $attributes = [])
138
 * @method HTML script(?string $content = '', array $attributes = [])
139
 * @method HTML section(?string $content = '', array $attributes = [])
140
 * @method HTML select(?string $content = '', array $attributes = [])
141
 * @method HTML small(?string $content = '', array $attributes = [])
142
 * @method HTML source(?string $content = '', array $attributes = [])
143
 * @method HTML span(?string $content = '', array $attributes = [])
144
 * @method HTML strong(?string $content = '', array $attributes = [])
145
 * @method HTML style(?string $content = '', array $attributes = [])
146
 * @method HTML sub(?string $content = '', array $attributes = [])
147
 * @method HTML summary(?string $content = '', array $attributes = [])
148
 * @method HTML sup(?string $content = '', array $attributes = [])
149
 * @method HTML svg(?string $content = '', array $attributes = [])
150
 * @method HTML table(?string $content = '', array $attributes = [])
151
 * @method HTML tbody(?string $content = '', array $attributes = [])
152
 * @method static string td(?string $content = '', array $attributes = [])
153
 * @method HTML td(?string $content = '', array $attributes = [])
154
 * @method HTML template(?string $content = '', array $attributes = [])
155
 * @method HTML textarea(?string $content = '', array $attributes = [])
156
 * @method HTML tfoot(?string $content = '', array $attributes = [])
157
 * @method HTML th(?string $content = '', array $attributes = [])
158
 * @method HTML thead(?string $content = '', array $attributes = [])
159
 * @method HTML time(?string $content = '', array $attributes = [])
160
 * @method HTML title(?string $content = '', array $attributes = [])
161
 * @method HTML tr(?string $content = '', array $attributes = [])
162
 * @method HTML track(?string $content = '', array $attributes = [])
163
 * @method HTML u(?string $content = '', array $attributes = [])
164
 * @method HTML ul(?string $content = '', array $attributes = [])
165
 * @method HTML var(?string $content = '', array $attributes = [])
166
 * @method HTML video(?string $content = '', array $attributes = [])
167
 * @method HTML wbr(?string $content = '', array $attributes = [])
168
 * @method static string a(?string $content = '', array $attributes = [])
169
 * @method static string abbr(?string $content = '', array $attributes = [])
170
 * @method static string address(?string $content = '', array $attributes = [])
171
 * @method static string area(?string $content = '', array $attributes = [])
172
 * @method static string article(?string $content = '', array $attributes = [])
173
 * @method static string aside(?string $content = '', array $attributes = [])
174
 * @method static string audio(?string $content = '', array $attributes = [])
175
 * @method static string b(?string $content = '', array $attributes = [])
176
 * @method static string base(?string $content = '', array $attributes = [])
177
 * @method static string bdi(?string $content = '', array $attributes = [])
178
 * @method static string bdo(?string $content = '', array $attributes = [])
179
 * @method static string blockquote(?string $content = '', array $attributes = [])
180
 * @method static string body(?string $content = '', array $attributes = [])
181
 * @method static string br(?string $content = '', array $attributes = [])
182
 * @method static string button(?string $content = '', array $attributes = [])
183
 * @method static string canvas(?string $content = '', array $attributes = [])
184
 * @method static string caption(?string $content = '', array $attributes = [])
185
 * @method static string cite(?string $content = '', array $attributes = [])
186
 * @method static string code(?string $content = '', array $attributes = [])
187
 * @method static string col(?string $content = '', array $attributes = [])
188
 * @method static string colgroup(?string $content = '', array $attributes = [])
189
 * @method static string data(?string $content = '', array $attributes = [])
190
 * @method static string datalist(?string $content = '', array $attributes = [])
191
 * @method static string dd(?string $content = '', array $attributes = [])
192
 * @method static string del(?string $content = '', array $attributes = [])
193
 * @method static string details(?string $content = '', array $attributes = [])
194
 * @method static string dfn(?string $content = '', array $attributes = [])
195
 * @method static string dialog(?string $content = '', array $attributes = [])
196
 * @method static string div(?string $content = '', array $attributes = [])
197
 * @method static string dl(?string $content = '', array $attributes = [])
198
 * @method static string dt(?string $content = '', array $attributes = [])
199
 * @method static string em(?string $content = '', array $attributes = [])
200
 * @method static string embed(?string $content = '', array $attributes = [])
201
 * @method static string fieldset(?string $content = '', array $attributes = [])
202
 * @method static string figcaption(?string $content = '', array $attributes = [])
203
 * @method static string figure(?string $content = '', array $attributes = [])
204
 * @method static string footer(?string $content = '', array $attributes = [])
205
 * @method static string form(?string $content = '', array $attributes = [])
206
 * @method static string h1(?string $content = '', array $attributes = [])
207
 * @method static string h2(?string $content = '', array $attributes = [])
208
 * @method static string h3(?string $content = '', array $attributes = [])
209
 * @method static string h4(?string $content = '', array $attributes = [])
210
 * @method static string h5(?string $content = '', array $attributes = [])
211
 * @method static string h6(?string $content = '', array $attributes = [])
212
 * @method static string head(?string $content = '', array $attributes = [])
213
 * @method static string header(?string $content = '', array $attributes = [])
214
 * @method static string hr(?string $content = '', array $attributes = [])
215
 * @method static string html(?string $content = '', array $attributes = [])
216
 * @method static string i(?string $content = '', array $attributes = [])
217
 * @method static string iframe(?string $content = '', array $attributes = [])
218
 * @method static string img(?string $content = '', array $attributes = [])
219
 * @method static string input(?string $content = '', array $attributes = [])
220
 * @method static string ins(?string $content = '', array $attributes = [])
221
 * @method static string kbd(?string $content = '', array $attributes = [])
222
 * @method static string label(?string $content = '', array $attributes = [])
223
 * @method static string legend(?string $content = '', array $attributes = [])
224
 * @method static string li(?string $content = '', array $attributes = [])
225
 * @method static string link(?string $content = '', array $attributes = [])
226
 * @method static string main(?string $content = '', array $attributes = [])
227
 * @method static string map(?string $content = '', array $attributes = [])
228
 * @method static string mark(?string $content = '', array $attributes = [])
229
 * @method static string meta(?string $content = '', array $attributes = [])
230
 * @method static string meter(?string $content = '', array $attributes = [])
231
 * @method static string nav(?string $content = '', array $attributes = [])
232
 * @method static string noscript(?string $content = '', array $attributes = [])
233
 * @method static string object(?string $content = '', array $attributes = [])
234
 * @method static string ol(?string $content = '', array $attributes = [])
235
 * @method HTML ol(?string $content = '', array $attributes = [])
236
 * @method static string optgroup(?string $content = '', array $attributes = [])
237
 * @method HTML optgroup(?string $content = '', array $attributes = [])
238
 * @method static string option(?string $content = '', array $attributes = [])
239
 * @method HTML option(?string $content = '', array $attributes = [])
240
 * @method static string output(?string $content = '', array $attributes = [])
241
 * @method HTML output(?string $content = '', array $attributes = [])
242
 * @method static string p(?string $content = '', array $attributes = [])
243
 * @method static string param(?string $content = '', array $attributes = [])
244
 * @method static string picture(?string $content = '', array $attributes = [])
245
 * @method static string pre(?string $content = '', array $attributes = [])
246
 * @method static string progress(?string $content = '', array $attributes = [])
247
 * @method static string q(?string $content = '', array $attributes = [])
248
 * @method static string rp(?string $content = '', array $attributes = [])
249
 * @method static string rt(?string $content = '', array $attributes = [])
250
 * @method static string ruby(?string $content = '', array $attributes = [])
251
 * @method static string s(?string $content = '', array $attributes = [])
252
 * @method static string samp(?string $content = '', array $attributes = [])
253
 * @method static string script(?string $content = '', array $attributes = [])
254
 * @method static string section(?string $content = '', array $attributes = [])
255
 * @method static string select(?string $content = '', array $attributes = [])
256
 * @method static string small(?string $content = '', array $attributes = [])
257
 * @method static string source(?string $content = '', array $attributes = [])
258
 * @method static string span(?string $content = '', array $attributes = [])
259
 * @method static string strong(?string $content = '', array $attributes = [])
260
 * @method static string style(?string $content = '', array $attributes = [])
261
 * @method static string sub(?string $content = '', array $attributes = [])
262
 * @method static string summary(?string $content = '', array $attributes = [])
263
 * @method static string sup(?string $content = '', array $attributes = [])
264
 * @method static string svg(?string $content = '', array $attributes = [])
265
 * @method static string table(?string $content = '', array $attributes = [])
266
 * @method static string tbody(?string $content = '', array $attributes = [])
267
 * @method static string template(?string $content = '', array $attributes = [])
268
 * @method static string textarea(?string $content = '', array $attributes = [])
269
 * @method static string tfoot(?string $content = '', array $attributes = [])
270
 * @method static string th(?string $content = '', array $attributes = [])
271
 * @method static string thead(?string $content = '', array $attributes = [])
272
 * @method static string time(?string $content = '', array $attributes = [])
273
 * @method static string title(?string $content = '', array $attributes = [])
274
 * @method static string tr(?string $content = '', array $attributes = [])
275
 * @method static string track(?string $content = '', array $attributes = [])
276
 * @method static string u(?string $content = '', array $attributes = [])
277
 * @method static string ul(?string $content = '', array $attributes = [])
278
 * @method static string var(?string $content = '', array $attributes = [])
279
 * @method static string video(?string $content = '', array $attributes = [])
280
 * @method static string wbr(?string $content = '', array $attributes = [])
281
 */
282
class HTML
283
{
284
    private bool $validate;
285
286
    private array $conditions;
287
288
    private array $buffer;
289
290
    private array $stack;
291
292
293
    /**
294
     * Class constructor.
295
     *
296
     * @param bool $validate Whether to validate the HTML upon return/echo or not.
297
     */
298 21
    public function __construct(bool $validate = true)
299
    {
300 21
        $this->validate   = $validate;
301 21
        $this->buffer     = [];
302 21
        $this->stack      = [];
303 21
        $this->conditions = [];
304
    }
305
306
307
    /**
308
     * Creates a complete HTML element (opening and closing tags) constructed from the specified parameters and pass it to the buffer.
309
     *
310
     * @param string $name A name of an HTML tag.
311
     * @param string|null $content [optional] The text or html content of the element, passing null will make it a self-closing tag.
312
     * @param string[] $attributes [optional] An associative array of attributes. To indicate a boolean-attribute (no-value-attribute), simply provide a key with value `null` or provide only a value without a key with the name of the attribute. As a shortcut, setting the value as `false` will exclude the attribute.
313
     *
314
     * @return $this
315
     *
316
     * @throws \Exception If the supplied name is invalid.
317
     */
318 16
    public function element(string $name, ?string $content = '', array $attributes = []): HTML
319
    {
320 16
        $this->agreeOrFail(
321 16
            !strlen($name),
322
            'Invalid name supplied to %s::%s() in %s on line %s. Tag name cannot be an empty string'
323
        );
324
325 16
        if ($this->isConditionTruthy()) {
326 16
            $tag = $content !== null
327 15
                ? '<{name}{attributes}>{content}</{name}>'
328 16
                : '<{name}{attributes} />';
329 16
            $name = strtolower(trim($name));
330 16
            $attributes = $this->stringifyAttributes($attributes);
331
332 16
            $this->buffer[] = $this->translateElement($tag, compact('name', 'content', 'attributes'));
333
        }
334
335 16
        return $this;
336
    }
337
338
    /**
339
     * Creates an HTML entity from the specified name and pass it to the buffer.
340
     *
341
     * @param string $name The name of the HTML entity without `&` nor `;`.
342
     *
343
     * @return $this
344
     *
345
     * @throws \Exception If the supplied name is invalid.
346
     */
347 2
    public function entity(string $name): HTML
348
    {
349 2
        $this->agreeOrFail(
350 2
            !strlen($name),
351
            'Invalid name supplied to %s::%s() in %s on line %s. Entity name cannot be an empty string'
352
        );
353
354 2
        if ($this->isConditionTruthy()) {
355 2
            $entity = sprintf('&%s;', trim($name, '& ;'));
356 2
            $this->buffer[] = $entity;
357
        }
358
359
360 2
        return $this;
361
    }
362
363
    /**
364
     * Creates an HTML comment from the specified text and pass it to the buffer.
365
     *
366
     * @param string $comment The text content of the HTML comment without `<!--` and `-->`.
367
     *
368
     * @return $this
369
     *
370
     * @throws \Exception If the supplied text is invalid.
371
     */
372 1
    public function comment(string $text): HTML
373
    {
374 1
        $this->agreeOrFail(
375 1
            !strlen($text),
376
            'Invalid text supplied to %s::%s() in %s on line %s. Comment text cannot be an empty string'
377
        );
378
379 1
        if ($this->isConditionTruthy()) {
380 1
            $comment = sprintf('<!-- %s -->', trim($text));
381 1
            $this->buffer[] = $comment;
382
        }
383
384 1
        return $this;
385
    }
386
387
    /**
388
     * Creates an arbitrary text-node from the specified text and pass it to the buffer (useful to add some special tags, "\<!DOCTYPE html>" for example).
389
     *
390
     * @param string $text The text to pass to the buffer.
391
     *
392
     * @return $this
393
     *
394
     * @throws \Exception If the supplied text is invalid.
395
     */
396 2
    public function node(string $text): HTML
397
    {
398 2
        $this->agreeOrFail(
399 2
            !strlen($text),
400
            'Invalid text supplied to %s::%s() in %s on line %s. Node text cannot be an empty string'
401
        );
402
403 2
        if ($this->isConditionTruthy()) {
404 2
            $text = trim($text);
405 2
            $this->buffer[] = $text;
406
        }
407
408 2
        return $this;
409
    }
410
411
    /**
412
     * Creates an HTML opening tag from the specified parameters and pass it to the buffer. Works in conjunction with `self::close()`.
413
     *
414
     * @param string $name A name of an HTML tag.
415
     * @param string[] $attributes [optional] An associative array of attributes. To indicate a boolean-attribute (no-value-attribute), simply provide a key with value `null`. Setting the value as `false` will exclude the attribute.
416
     *
417
     * @return $this
418
     *
419
     * @throws \Exception If the supplied name is invalid.
420
     */
421 9
    public function open(string $name, array $attributes = []): HTML
422
    {
423 9
        $this->agreeOrFail(
424 9
            !strlen($name),
425
            'Invalid name supplied to %s::%s() in %s on line %s. Tag name cannot be an empty string'
426
        );
427
428 9
        if ($this->isConditionTruthy(1)) {
429 9
            $tag = '<{name}{attributes}>';
430 9
            $name = strtolower(trim($name));
431 9
            $attributes = $this->stringifyAttributes($attributes);
432
433 9
            $this->buffer[] = $this->translateElement($tag, compact('name', 'attributes'));
434
435 9
            array_push($this->stack, $name);
436
        }
437
438 9
        return $this;
439
    }
440
441
    /**
442
     * Creates an HTML closing tag matching the last tag opened by `self::open()`.
443
     *
444
     * @return $this
445
     *
446
     * @throws \Exception If no tag has been opened.
447
     */
448 8
    public function close(): HTML
449
    {
450 8
        $this->agreeOrFail(
451 8
            !count($this->stack),
452
            'Not in a context to close a tag! Call to %s::%s() in %s on line %s is superfluous'
453
        );
454
455 8
        if ($this->isConditionTruthy(-1)) {
456 8
            $tag = '</{name}>';
457
458 8
            $name = array_pop($this->stack);
459
460 8
            $this->buffer[] = $this->translateElement($tag, compact('name'));
461
        }
462
463 8
        return $this;
464
    }
465
466
    /**
467
     * Takes a callback and executes it after binding $this (HTML) to it, useful for example to execute any PHP code while creating the markup.
468
     *
469
     * @param callable $callback The callback to call and bind $this to, this callback will also be passed the instance it was called on as the first parameter.
470
     *
471
     * @return $this
472
     */
473 1
    public function execute(callable $callback): HTML
474
    {
475 1
        if ($this->isConditionTruthy()) {
476 1
            $boundClosure = \Closure::fromCallable($callback)->bindTo($this);
477
478 1
            $boundClosure($this);
479
        }
480
481 1
        return $this;
482
    }
483
484
    /**
485
     * Takes a condition (some boolean value) to determine whether or not to create the very next element and pass it to the buffer.
486
     *
487
     * @param mixed $condition Any value that can be casted into a boolean.
488
     *
489
     * @return $this
490
     */
491 11
    public function condition($condition): HTML
492
    {
493 11
        $this->conditions[] = (bool)($condition);
494
495 11
        return $this;
496
    }
497
498
    /**
499
     * Determines whether or not the last set condition is truthy or falsy.
500
     *
501
     * @param int $parent [optional] A flag to indicate condition depth `[+1 = parentOpened, 0 = normal, -1 = parentClosed]`.
502
     *
503
     * @return bool The result of the current condition.
504
     */
505 16
    private function isConditionTruthy(int $parent = 0): bool
506
    {
507 16
        static $parentConditions = [];
508
509 16
        $result = true;
510
511 16
        if (!empty($this->conditions) || !empty($parentConditions)) {
512 11
            $actualCondition = $this->conditions[count($this->conditions) - 1] ?? $result;
513 11
            $parentCondition = $parentConditions[count($parentConditions) - 1] ?? $result;
514
515 11
            $condition = $parentCondition & $actualCondition;
516 11
            if (!$condition) {
517 2
                $result = false;
518
            }
519
520 11
            switch ($parent) {
521
                case +1:
522 1
                    array_push($parentConditions, $condition);
523 1
                    break;
524
                case -1:
525 1
                    array_pop($parentConditions);
526 1
                    break;
527 11
                case 0:
528
                    // NORMAL!
529 11
                    break;
530
            }
531
532 11
            array_pop($this->conditions);
533
        }
534
535 16
        return $result;
536
    }
537
538
    /**
539
     * Checks if the passed condition and throws exception if it's not truthy.
540
     * The message that is passed to this function should contain four `%s` placeholders for the
541
     * `class`, `function`, `file` and `line` of the previous caller (offset 2 of the backtrace)
542
     *
543
     * @param bool $condition
544
     * @param string $message
545
     *
546
     * @return void
547
     *
548
     * @throws \Exception
549
     */
550 16
    private function agreeOrFail(bool $condition, string $message): void
551
    {
552 16
        if ($condition) {
553 1
            $variables = ['class', 'function', 'file', 'line'];
554 1
            $backtrace = Misc::backtrace($variables, 2);
555 1
            $backtrace = is_array($backtrace) ? $backtrace : array_map('strtoupper', $variables);
556
557 1
            throw new \Exception(vsprintf($message, $backtrace));
558
        }
559
    }
560
561
    /**
562
     * Returns an HTML attributes string from an associative array of attributes.
563
     *
564
     * @param array $attributes
565
     *
566
     * @return string
567
     */
568 16
    private function stringifyAttributes(array $attributes): string
569
    {
570 16
        $attrStr = '';
571
572 16
        foreach ($attributes as $name => $value) {
573 13
            if ($value === false) {
574 1
                continue;
575
            }
576
577 13
            $attrStr .= is_string($name) && !is_null($value)
578 13
                ? sprintf(' %s="%s"', $name, $value)
579 4
                : sprintf(' %s', $value ?: $name);
580
        }
581
582 16
        return $attrStr;
583
    }
584
585
    /**
586
     * Replaces variables in the passed string with values with matching key from the passed array.
587
     *
588
     * @param string $text A string like `{var} world!`.
589
     * @param array $variables An array like `['var' => 'Hello']`.
590
     *
591
     * @return string A string like `Hello world!`
592
     */
593 16
    private function translateElement(string $text, array $variables = []): string
594
    {
595 16
        $replacements = [];
596
597 16
        foreach ($variables as $key => $value) {
598 16
            $replacements[sprintf('{%s}', $key)] = $value;
599
        }
600
601 16
        return strtr($text, $replacements);
602
    }
603
604
    /**
605
     * Asserts that the passed HTML is valid.
606
     *
607
     * @return void
608
     *
609
     * @throws \Exception If the passed html is invalid.
610
     */
611 4
    private function validate(string $html): void
612
    {
613 4
        $html = !empty($html) ? $html : '<br/>';
614
615 4
        $xml = libxml_use_internal_errors(true);
616
617 4
        $dom = new \DOMDocument();
618 4
        $dom->validateOnParse = true;
619 4
        $dom->loadHTML($html);
620
        // $dom->saveHTML();
621
622 4
        $ignoredCodes = [801];
623 4
        $errors = libxml_get_errors();
624 4
        libxml_clear_errors();
625
626 4
        if (!empty($errors) && !in_array($errors[0]->code, $ignoredCodes)) {
627 1
            $file = Misc::backtrace('file', 3);
628 1
            $file = is_string($file) ? $file : 'FILE';
629
630 1
            throw new \Exception(
631 1
                vsprintf(
632
                    'HTML is invalid in %s! Found %s problem(s). Last LibXMLError: [level:%s/code:%s] %s',
633 1
                    [$file, count($errors), $errors[0]->level, $errors[0]->code, $errors[0]->message]
634
                )
635
            );
636
        }
637
638 3
        libxml_use_internal_errors($xml);
639
    }
640
641
    /**
642
     * Returns the created HTML elements found in the buffer and empties it.
643
     *
644
     * @return string
645
     *
646
     * @throws \Exception If not all open elements are closed or the generated html is invalid.
647
     */
648 15
    public function return(): string
649
    {
650 15
        if (count($this->stack)) {
651 1
            $file = Misc::backtrace('file', 2);
652 1
            $file = is_string($file) ? $file : 'FILE';
653
654 1
            throw new \Exception(
655 1
                sprintf(
656
                    "Cannot return HTML in %s. The following tag(s): '%s' has/have not been closed properly",
657
                    $file,
658 1
                    implode(', ', $this->stack)
659
                )
660
            );
661
        }
662
663 14
        $html = implode('', $this->buffer);
664
665 14
        $this->buffer = [];
666
667 14
        if ($this->validate) {
668 4
            $this->validate($html);
669
        }
670
671 13
        return $html;
672
    }
673
674
    /**
675
     * Echos the created HTML elements found in the buffer and empties it.
676
     *
677
     * @return void
678
     *
679
     * @throws \Exception If not all open elements are closed or the generated html is invalid.
680
     */
681 3
    public function echo(): void
682
    {
683 3
        echo $this->return();
684
    }
685
686
    /**
687
     * Minifies HTML buffers by removing all unnecessary whitespaces and comments.
688
     *
689
     * @param string $html
690
     *
691
     * @return string
692
     */
693 7
    public static function minify(string $html): string
694
    {
695
        $patterns = [
696
            '/(\s)+/s'          => '$1', // shorten multiple whitespace sequences
697
            '/>[^\S ]+/s'       => '>',  // remove spaces after tag, except one space
698
            '/[^\S ]+</s'       => '<',  // remove spaces before tag, except one space
699
            '/<(\s|\t|\r?\n)+/' => '<',  // remove spaces, tabs, and new lines after start of the tag
700
            '/(\s|\t|\r?\n)+>/' => '>',  // remove spaces, tabs, and new lines before end of the tag
701
            '/<!--(.|\s)*?-->/' => '',   // remove comments
702
        ];
703
704 7
        $minified = preg_replace(
705 7
            array_keys($patterns),
706 7
            array_values($patterns),
707
            $html
708
        );
709
710 7
        return trim($minified);
711
    }
712
713
714
    /**
715
     * Makes HTML tags available as methods on the class.
716
     */
717 16
    public function __call(string $method, array $arguments)
718
    {
719 16
        $name = $method;
720 16
        $content = $arguments[0] ?? (count($arguments) ? null : '');
721 16
        $attributes = $arguments[1] ?? [];
722
723 16
        return $this->element($name, $content, $attributes);
724
    }
725
726
    /**
727
     * Makes HTML tags available as static methods on the class.
728
     */
729 9
    public static function __callStatic(string $method, array $arguments)
730
    {
731
        static $instance = null;
732
733 9
        if ($instance === null) {
734 1
            $instance = new HTML(false);
735
        }
736
737 9
        return $instance->condition(true)->{$method}(...$arguments)->return();
738
    }
739
}
740