Passed
Push — master ( 228c08...e198b9 )
by Marwan
01:18
created

HTML::stringifyAttributes()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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