Passed
Push — master ( 33af8f...59c3c7 )
by Marwan
01:15
created

HTML::stringifyAttributes()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 8
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 15
rs 9.2222
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
        $this->agreeOrFail(
320
            !strlen($name),
321
            'Invalid name supplied to %s::%s() in %s on line %s. Tag name cannot be an empty string'
322
        );
323
324
        if ($this->isConditionTruthy()) {
325
            $tag = $content !== null
326
                ? '<{name}{attributes}>{content}</{name}>'
327
                : '<{name}{attributes} />';
328
            $name = strtolower(trim($name));
329
            $attributes = $this->stringifyAttributes($attributes);
330
331
            $this->buffer[] = $this->translateElement($tag, compact('name', 'content', 'attributes'));
332
        }
333
334
        return $this;
335
    }
336
337
    /**
338
     * Creates an HTML entity from the specified name and pass it to the buffer.
339
     *
340
     * @param string $name The name of the HTML entity without `&` nor `;`.
341
     *
342
     * @return $this
343
     *
344
     * @throws \Exception If the supplied name is invalid.
345
     */
346
    public function entity(string $name): HTML
347
    {
348
        $this->agreeOrFail(
349
            !strlen($name),
350
            'Invalid name supplied to %s::%s() in %s on line %s. Entity name cannot be an empty string'
351
        );
352
353
        if ($this->isConditionTruthy()) {
354
            $entity = sprintf('&%s;', trim($name, '& ;'));
355
            $this->buffer[] = $entity;
356
        }
357
358
359
        return $this;
360
    }
361
362
    /**
363
     * Creates an HTML comment from the specified text and pass it to the buffer.
364
     *
365
     * @param string $comment The text content of the HTML comment without `<!--` and `-->`.
366
     *
367
     * @return $this
368
     *
369
     * @throws \Exception If the supplied text is invalid.
370
     */
371
    public function comment(string $text): HTML
372
    {
373
        $this->agreeOrFail(
374
            !strlen($text),
375
            'Invalid text supplied to %s::%s() in %s on line %s. Comment text cannot be an empty string'
376
        );
377
378
        if ($this->isConditionTruthy()) {
379
            $comment = sprintf('<!-- %s -->', trim($text));
380
            $this->buffer[] = $comment;
381
        }
382
383
        return $this;
384
    }
385
386
    /**
387
     * 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).
388
     *
389
     * @param string $text The text to pass to the buffer.
390
     *
391
     * @return $this
392
     *
393
     * @throws \Exception If the supplied text is invalid.
394
     */
395
    public function node(string $text): HTML
396
    {
397
        $this->agreeOrFail(
398
            !strlen($text),
399
            'Invalid text supplied to %s::%s() in %s on line %s. Node text cannot be an empty string'
400
        );
401
402
        if ($this->isConditionTruthy()) {
403
            $text = trim($text);
404
            $this->buffer[] = $text;
405
        }
406
407
        return $this;
408
    }
409
410
    /**
411
     * Creates an HTML opening tag from the specified parameters and pass it to the buffer. Works in conjunction with `self::close()`.
412
     *
413
     * @param string $name A name of an HTML tag.
414
     * @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.
415
     *
416
     * @return $this
417
     *
418
     * @throws \Exception If the supplied name is invalid.
419
     */
420
    public function open(string $name, array $attributes = []): HTML
421
    {
422
        $this->agreeOrFail(
423
            !strlen($name),
424
            'Invalid name supplied to %s::%s() in %s on line %s. Tag name cannot be an empty string'
425
        );
426
427
        if ($this->isConditionTruthy(1)) {
428
            $tag = '<{name}{attributes}>';
429
            $name = strtolower(trim($name));
430
            $attributes = $this->stringifyAttributes($attributes);
431
432
            $this->buffer[] = $this->translateElement($tag, compact('name', 'attributes'));
433
434
            array_push($this->stack, $name);
435
        }
436
437
        return $this;
438
    }
439
440
    /**
441
     * Creates an HTML closing tag matching the last tag opened by `self::open()`.
442
     *
443
     * @return $this
444
     *
445
     * @throws \Exception If no tag has been opened.
446
     */
447
    public function close(): HTML
448
    {
449
        $this->agreeOrFail(
450
            !count($this->stack),
451
            'Not in a context to close a tag! Call to %s::%s() in %s on line %s is superfluous'
452
        );
453
454
        if ($this->isConditionTruthy(-1)) {
455
            $tag = '</{name}>';
456
457
            $name = array_pop($this->stack);
458
459
            $this->buffer[] = $this->translateElement($tag, compact('name'));
460
        }
461
462
        return $this;
463
    }
464
465
    /**
466
     * Takes a callback and executes it after binding $this (HTML) to it, useful for example to execute any PHP code while creating the markup.
467
     *
468
     * @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.
469
     *
470
     * @return $this
471
     */
472
    public function execute(callable $callback): HTML
473
    {
474
        if ($this->isConditionTruthy()) {
475
            $boundClosure = \Closure::fromCallable($callback)->bindTo($this);
476
477
            if ($boundClosure !== false) {
478
                $boundClosure($this);
479
            } else {
480
                $callback($this);
481
            }
482
        }
483
484
        return $this;
485
    }
486
487
    /**
488
     * Takes a condition (some boolean value) to determine whether or not to create the very next element and pass it to the buffer.
489
     *
490
     * @param mixed $condition Any value that can be casted into a boolean.
491
     *
492
     * @return $this
493
     */
494
    public function condition($condition): HTML
495
    {
496
        $this->conditions[] = (bool)($condition);
497
498
        return $this;
499
    }
500
501
    /**
502
     * Determines whether or not the last set condition is truthy or falsy.
503
     *
504
     * @param int $parent [optional] A flag to indicate condition depth `[+1 = parentOpened, 0 = normal, -1 = parentClosed]`.
505
     *
506
     * @return bool The result of the current condition.
507
     */
508
    private function isConditionTruthy(int $parent = 0): bool
509
    {
510
        static $parentConditions = [];
511
512
        $result = true;
513
514
        if (!empty($this->conditions) || !empty($parentConditions)) {
515
            $actualCondition = $this->conditions[count($this->conditions) - 1] ?? $result;
516
            $parentCondition = $parentConditions[count($parentConditions) - 1] ?? $result;
517
518
            $condition = $parentCondition & $actualCondition;
519
            if (!$condition) {
520
                $result = false;
521
            }
522
523
            switch ($parent) {
524
                case +1:
525
                    array_push($parentConditions, $condition);
526
                    break;
527
                case -1:
528
                    array_pop($parentConditions);
529
                    break;
530
                case 0:
531
                    // NORMAL!
532
                    break;
533
            }
534
535
            array_pop($this->conditions);
536
        }
537
538
        return $result;
539
    }
540
541
    /**
542
     * Checks if the passed condition and throws exception if it's not truthy.
543
     * The message that is passed to this function should contain four `%s` placeholders for the
544
     * `class`, `function`, `file` and `line` of the previous caller (offset 2 of the backtrace)
545
     *
546
     * @param bool $condition
547
     * @param string $message
548
     *
549
     * @return void
550
     *
551
     * @throws \Exception
552
     */
553
    private function agreeOrFail(bool $condition, string $message): void
554
    {
555
        if ($condition) {
556
            $variables = ['class', 'function', 'file', 'line'];
557
            $backtrace = Misc::backtrace($variables, 2);
558
            $backtrace = is_array($backtrace) ? $backtrace : array_map('strtoupper', $variables);
559
560
            throw new \Exception(vsprintf($message, $backtrace));
561
        }
562
    }
563
564
    /**
565
     * Returns an HTML attributes string from an associative array of attributes.
566
     *
567
     * @param array $attributes
568
     *
569
     * @return string
570
     */
571
    private function stringifyAttributes(array $attributes): string
572
    {
573
        $attrStr = '';
574
575
        foreach ($attributes as $name => $value) {
576
            if ($value === false) {
577
                continue;
578
            }
579
580
            $attrStr .= is_string($name) && !is_null($value)
581
                ? sprintf(' %s="%s"', $name, $value)
582
                : sprintf(' %s', $value ?: $name);
583
        }
584
585
        return $attrStr;
586
    }
587
588
    /**
589
     * Replaces variables in the passed string with values with matching key from the passed array.
590
     *
591
     * @param string $text A string like `{var} world!`.
592
     * @param array $variables An array like `['var' => 'Hello']`.
593
     *
594
     * @return string A string like `Hello world!`
595
     */
596
    private function translateElement(string $text, array $variables = []): string
597
    {
598
        $replacements = [];
599
600
        foreach ($variables as $key => $value) {
601
            $replacements[sprintf('{%s}', $key)] = $value;
602
        }
603
604
        return strtr($text, $replacements);
605
    }
606
607
    /**
608
     * Asserts that the passed HTML is valid.
609
     *
610
     * @return void
611
     *
612
     * @throws \Exception If the passed html is invalid.
613
     */
614
    private function validate(string $html): void
615
    {
616
        $html = !empty($html) ? $html : '<br/>';
617
618
        $xml = libxml_use_internal_errors(true);
619
620
        $dom = new \DOMDocument();
621
        $dom->validateOnParse = true;
622
        $dom->loadHTML($html);
623
        // $dom->saveHTML();
624
625
        $ignoredCodes = [801];
626
        $errors = libxml_get_errors();
627
        libxml_clear_errors();
628
629
        if (!empty($errors) && !in_array($errors[0]->code, $ignoredCodes)) {
630
            $file = Misc::backtrace('file', 3);
631
            $file = is_string($file) ? $file : 'FILE';
632
633
            throw new \Exception(
634
                vsprintf(
635
                    'HTML is invalid in %s! Found %s problem(s). Last LibXMLError: [level:%s/code:%s] %s',
636
                    [$file, count($errors), $errors[0]->level, $errors[0]->code, $errors[0]->message]
637
                )
638
            );
639
        }
640
641
        libxml_use_internal_errors($xml);
642
    }
643
644
    /**
645
     * Returns the created HTML elements found in the buffer and empties it.
646
     *
647
     * @return string
648
     *
649
     * @throws \Exception If not all open elements are closed or the generated html is invalid.
650
     */
651
    public function return(): string
652
    {
653
        if (count($this->stack)) {
654
            $file = Misc::backtrace('file', 2);
655
            $file = is_string($file) ? $file : 'FILE';
656
657
            throw new \Exception(
658
                sprintf(
659
                    "Cannot return HTML in %s. The following tag(s): '%s' has/have not been closed properly",
660
                    $file,
661
                    implode(', ', $this->stack)
662
                )
663
            );
664
        }
665
666
        $html = implode('', $this->buffer);
667
668
        $this->buffer = [];
669
670
        if ($this->validate) {
671
            $this->validate($html);
672
        }
673
674
        return $html;
675
    }
676
677
    /**
678
     * Echos the created HTML elements found in the buffer and empties it.
679
     *
680
     * @return void
681
     *
682
     * @throws \Exception If not all open elements are closed or the generated html is invalid.
683
     */
684
    public function echo(): void
685
    {
686
        echo $this->return();
687
    }
688
689
    /**
690
     * Minifies HTML buffers by removing all unnecessary whitespaces and comments.
691
     *
692
     * @param string $html
693
     *
694
     * @return string
695
     */
696
    public static function minify(string $html): string
697
    {
698
        $patterns = [
699
            '/(\s)+/s'          => '$1', // shorten multiple whitespace sequences
700
            '/>[^\S ]+/s'       => '>',  // remove spaces after tag, except one space
701
            '/[^\S ]+</s'       => '<',  // remove spaces before tag, except one space
702
            '/>[\s]+</'         => '><', // remove spaces between tags, except one space
703
            '/<(\s|\t|\r?\n)+/' => '<',  // remove spaces, tabs, and new lines after start of the tag
704
            '/(\s|\t|\r?\n)+>/' => '>',  // remove spaces, tabs, and new lines before end of the tag
705
            '/<!--(.|\s)*?-->/' => '',   // remove comments
706
        ];
707
708
        return preg_replace(
709
            array_keys($patterns),
710
            array_values($patterns),
711
            $html
712
        );
713
    }
714
715
716
    /**
717
     * Makes HTML tags available as methods on the class.
718
     */
719
    public function __call(string $method, array $arguments)
720
    {
721
        $name = $method;
722
        $content = $arguments[0] ?? (count($arguments) ? null : '');
723
        $attributes = $arguments[1] ?? [];
724
725
        return $this->element($name, $content, $attributes);
726
    }
727
728
    /**
729
     * Makes HTML tags available as static methods on the class.
730
     */
731
    public static function __callStatic(string $method, array $arguments)
732
    {
733
        static $instance = null;
734
735
        if ($instance === null) {
736
            $instance = new HTML(false);
737
        }
738
739
        return $instance->condition(true)->{$method}(...$arguments)->return();
740
    }
741
}
742