Passed
Push — master ( 667180...1d7760 )
by P.R.
02:00
created

Html::htmlTagHelper()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 3
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 2
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace Plaisio\Helper;
5
6
use SetBased\Exception\FallenException;
7
8
/**
9
 * A utility class for generating HTML elements, tags, and attributes.
10
 */
11
final class Html
12
{
13
  //--------------------------------------------------------------------------------------------------------------------
14
  /**
15
   * The encoding of the generated HTML code.
16
   *
17
   * @var string
18
   *
19
   * @since 1.0.0
20
   * @api
21
   */
22
  public static string $encoding = 'UTF-8';
23
24
  /**
25
   * Counter for generating unique element IDs.
26
   *
27
   * @var int
28
   */
29
  private static int $autoId = 0;
30
31
  /**
32
   * Map from (some) unicode characters to ASCII characters.
33
   *
34
   * @var array
35
   */
36
  private static array $trans = ['ß' => 'sz',
37
                                 'à' => 'a',
38
                                 'á' => 'a',
39
                                 'â' => 'a',
40
                                 'ã' => 'a',
41
                                 'ä' => 'a',
42
                                 'å' => 'a',
43
                                 'æ' => 'ae',
44
                                 'ç' => 'c',
45
                                 'è' => 'e',
46
                                 'é' => 'e',
47
                                 'ê' => 'e',
48
                                 'ë' => 'e',
49
                                 'ì' => 'i',
50
                                 'í' => 'i',
51
                                 'î' => 'i',
52
                                 'ï' => 'i',
53
                                 'ð' => 'e',
54
                                 'ñ' => 'n',
55
                                 'ò' => 'o',
56
                                 'ó' => 'o',
57
                                 'ô' => 'o',
58
                                 'õ' => 'o',
59
                                 'ö' => 'o',
60
                                 '÷' => 'x',
61
                                 'ø' => 'o',
62
                                 'ù' => 'u',
63
                                 'ú' => 'u',
64
                                 'û' => 'u',
65
                                 'ü' => 'u',
66
                                 'ý' => 'y',
67
                                 'þ' => 'b',
68
                                 'ÿ' => 'y',
69
                                 'č' => 'c',
70
                                 'ł' => 'l',
71
                                 'š' => 's',
72
                                 'ů' => 'u',
73
                                 'ž' => 'z',
74
                                 'а' => 'a',
75
                                 'б' => 'b',
76
                                 'в' => 'v',
77
                                 'г' => 'g',
78
                                 'д' => 'd',
79
                                 'е' => 'e',
80
                                 'ж' => 'zh',
81
                                 'з' => 'z',
82
                                 'и' => 'i',
83
                                 'й' => 'i',
84
                                 'к' => 'k',
85
                                 'л' => 'l',
86
                                 'м' => 'm',
87
                                 'н' => 'n',
88
                                 'о' => 'o',
89
                                 'п' => 'p',
90
                                 'р' => 'r',
91
                                 'с' => 's',
92
                                 'т' => 't',
93
                                 'у' => 'u',
94
                                 'ф' => 'f',
95
                                 'х' => 'kh',
96
                                 'ц' => 'ts',
97
                                 'ч' => 'ch',
98
                                 'ш' => 'sh',
99
                                 'щ' => 'shch',
100
                                 'ъ' => '',
101
                                 'ы' => 'y',
102
                                 'ь' => '',
103
                                 'э' => 'e',
104
                                 'ю' => 'iu',
105
                                 'я' => 'ia',
106
                                 'ё' => 'e'];
107
108
  //--------------------------------------------------------------------------------------------------------------------
109
  /**
110
   * Returns a string that can be safely used as an ID for an element. The format of the id is 'abc_<n>' where n is
111
   * incremented with each call of this method.
112
   *
113
   * @return string
114
   *
115
   * @since 1.0.0
116
   * @api
117
   */
118 2
  public static function getAutoId(): string
119
  {
120 2
    self::$autoId++;
121
122 2
    return 'plaisio-id-'.self::$autoId;
123
  }
124
125
  //--------------------------------------------------------------------------------------------------------------------
126
  /**
127
   * Returns the HTML code of nested elements.
128
   *
129
   * Example:
130
   *
131
   * $html = Html::generateNested([['tag'   => 'table',
132
   *                                'attr'  => ['class' => 'test'],
133
   *                                'inner' => [['tag'   => 'tr',
134
   *                                             'attr'  => ['id' => 'first-row'],
135
   *                                             'inner' => [['tag'  => 'td',
136
   *                                                          'text' => 'hello'],
137
   *                                                         ['tag'  => 'td',
138
   *                                                          'attr' => ['class' => 'bold'],
139
   *                                                          'html' => '<b>world</b>']]],
140
   *                                            ['tag'   => 'tr',
141
   *                                             'inner' => [['tag'  => 'td',
142
   *                                                          'text' => 'foo'],
143
   *                                                         ['tag'  => 'td',
144
   *                                                          'text' => 'bar']]],
145
   *                                            ['tag'   => 'tr',
146
   *                                             'attr'  => ['id' => 'last-row'],
147
   *                                             'inner' => [['tag'  => 'td',
148
   *                                                          'text' => 'foo'],
149
   *                                                         ['tag'  => 'td',
150
   *                                                          'text' => 'bar']]]]],
151
   *                               ['text' => 'The End'],
152
   *                               ['html' => '!']]);
153
   *
154
   * @param array|null $struct The structure of the nested elements.
155
   *
156
   * @return string
157
   *
158
   * @since 3.2.0
159
   * @api
160
   */
161 48
  public static function htmlNested(?array $struct): string
162
  {
163 48
    $html = '';
164 48
    self::htmlNestedHelper($struct, $html);
165
166 47
    return $html;
167
  }
168
169
  //--------------------------------------------------------------------------------------------------------------------
170
  /**
171
   * Returns a string with special characters converted to HTML entities.
172
   * This method is a wrapper around [htmlspecialchars](http://php.net/manual/en/function.htmlspecialchars.php).
173
   *
174
   * @param bool|int|float|string|null $value The string with optionally special characters.
175
   *
176
   * @return string
177
   *
178
   * @since 1.0.0
179
   * @api
180
   */
181 34
  public static function txt2Html($value): string
182
  {
183
    switch (true)
184
    {
185 34
      case is_string($value):
186 27
        return htmlspecialchars($value, ENT_QUOTES, self::$encoding);
187
188 12
      case is_int($value):
189 10
      case is_float($value):
190 9
      case $value===null:
191 7
        return (string)$value;
192
193 7
      case $value===true:
194 1
        return '1';
195
196 6
      case $value===false:
197 4
        return '0';
198
199
      default:
200 2
        throw new FallenException('type', is_object($value) ? get_class($value) : gettype($value));
201
    }
202
  }
203
204
  //--------------------------------------------------------------------------------------------------------------------
205
  /**
206
   * Returns the slug of a string that can be safely used in a URL.
207
   *
208
   * @param string|null $string The string.
209
   *
210
   * @return string
211
   *
212
   * @since 1.1.0
213
   * @api
214
   */
215 1
  public static function txt2Slug(?string $string): string
216
  {
217 1
    if ($string===null) return '';
218
219 1
    return trim(preg_replace('/[^0-9a-z]+/', '-', strtr(mb_strtolower($string), self::$trans)), '-');
220
  }
221
222
  //--------------------------------------------------------------------------------------------------------------------
223
  /**
224
   * Removes empty and duplicate classes from an array with classes.
225
   *
226
   * @param array $classes The classes.
227
   *
228
   * @return array
229
   */
230 13
  private static function cleanClasses(array $classes): array
231
  {
232 13
    $clean = [];
233
234 13
    foreach ($classes as $class)
235
    {
236 12
      $tmp = Html::txt2Html($class);
237 12
      if ($tmp!=='')
238
      {
239 12
        $clean[] = $tmp;
240
      }
241
    }
242
243 13
    $clean = array_unique($clean, SORT_STRING);
244 13
    sort($clean);
245
246 13
    return $clean;
247
  }
248
249
  //--------------------------------------------------------------------------------------------------------------------
250
  /**
251
   * Returns a string with proper conversion of special characters to HTML entities of an attribute of an HTML tag.
252
   *
253
   * Boolean attributes (e.g. checked, disabled and draggable, autocomplete also) are set when the value is none empty.
254
   *
255
   * @param string $name  The name of the attribute.
256
   * @param mixed  $value The value of the attribute.
257
   * @param string $html  The generated HTML code.
258
   */
259 43
  private static function htmlAttributeHelper(string $name, $value, string &$html): void
260
  {
261
    switch ($name)
262
    {
263
      // Boolean attributes.
264 43
      case 'autofocus':
265 43
      case 'checked':
266 43
      case 'disabled':
267 43
      case 'hidden':
268 42
      case 'ismap':
269 42
      case 'multiple':
270 42
      case 'novalidate':
271 42
      case 'readonly':
272 42
      case 'required':
273 42
      case 'selected':
274 42
      case 'spellcheck':
275 4
        if (!empty($value))
276
        {
277 3
          $html .= ' ';
278 3
          $html .= $name;
279 3
          $html .= '="';
280 3
          $html .= $name;
281 3
          $html .= '"';
282
        }
283 4
        break;
284
285
      // Annoying boolean attribute exceptions.
286 39
      case 'draggable':
287 3
        if ($value!==null)
288
        {
289 3
          if ($value==='auto')
290
          {
291 1
            $html .= ' draggable="auto"';
292
          }
293 3
          elseif (empty($value) || $value==='false')
294
          {
295 2
            $html .= ' draggable="false"';
296
          }
297
          else
298
          {
299 2
            $html .= ' draggable="true"';
300
          }
301
        }
302 3
        break;
303
304 38
      case 'contenteditable':
305 3
        if ($value!==null)
306
        {
307 3
          $html .= ' ';
308 3
          $html .= $name;
309 3
          $html .= (!empty($value)) ? '="true"' : '="false"';
310
        }
311 3
        break;
312
313 35
      case 'autocomplete':
314 2
        if ($value!==null)
315
        {
316 2
          $html .= ' ';
317 2
          $html .= $name;
318 2
          $html .= (!empty($value)) ? '="on"' : '="off"';
319
        }
320 2
        break;
321
322 33
      case 'translate':
323 3
        if ($value!==null)
324
        {
325 3
          $html .= ' ';
326 3
          $html .= $name;
327 3
          $html .= (!empty($value)) ? '="yes"' : '="no"';
328
        }
329 3
        break;
330
331 30
      case 'class' and is_array($value):
332 13
        $classes = implode(' ', self::cleanClasses($value));
333 13
        if ($classes!=='')
334
        {
335 12
          $html .= ' class="';
336 12
          $html .= htmlspecialchars($classes, ENT_QUOTES, self::$encoding);
337 12
          $html .= '"';
338
        }
339 13
        break;
340
341
      default:
342 17
        if ($value!==null && $value!=='')
343
        {
344 14
          $html .= ' ';
345 14
          $html .= htmlspecialchars($name, ENT_QUOTES, self::$encoding);
346 14
          $html .= '="';
347 14
          $html .= self::txt2Html($value);
348 14
          $html .= '"';
349
        }
350
    }
351 43
  }
352
353
  //--------------------------------------------------------------------------------------------------------------------
354
  /**
355
   * Helper method for method generateNested().
356
   *
357
   * @param array|null $struct The (nested) structure of the HTML code.
358
   * @param string     $html   The generated HTML code.
359
   */
360 48
  private static function htmlNestedHelper(?array $struct, string &$html): void
361
  {
362 48
    if ($struct!==null)
1 ignored issue
show
introduced by
The condition $struct !== null is always true.
Loading history...
363
    {
364 47
      $key = array_key_first($struct);
365 47
      if (is_int($key))
366
      {
367
        // Structure is a list of elements.
368 3
        foreach ($struct as $element)
369
        {
370 3
          self::htmlNestedHelper($element, $html);
371
        }
372
      }
373 46
      elseif ($key!==null)
374
      {
375
        // Structure is an associative array.
376 46
        if (isset($struct['tag']))
377
        {
378
          // Element with content.
379 45
          if (array_key_exists('inner', $struct))
380
          {
381 4
            self::htmlTagHelper($struct['tag'], $struct['attr'] ?? [], $html);
382 4
            self::htmlNestedHelper($struct['inner'], $html);
383 4
            $html .= '</';
384 4
            $html .= $struct['tag'];
385 4
            $html .= '>';
386
          }
387 44
          elseif (array_key_exists('text', $struct))
388
          {
389 6
            self::htmlTagHelper($struct['tag'], $struct['attr'] ?? [], $html);
390 6
            $html .= self::txt2Html($struct['text']);
391 6
            $html .= '</';
392 6
            $html .= $struct['tag'];
393 6
            $html .= '>';
394
          }
395 40
          elseif (array_key_exists('html', $struct))
396
          {
397 38
            self::htmlTagHelper($struct['tag'], $struct['attr'] ?? [], $html);
398 38
            $html .= $struct['html'];
399 38
            $html .= '</';
400 38
            $html .= $struct['tag'];
401 38
            $html .= '>';
402
          }
403
          else
404
          {
405 45
            self::htmlVoidElementHelper($struct['tag'], $struct['attr'] ?? [], $html);
406
          }
407
        }
408 2
        elseif (array_key_exists('text', $struct))
409
        {
410 1
          $html .= self::txt2Html($struct['text']);
411
        }
412 2
        elseif (array_key_exists('html', $struct))
413
        {
414 1
          $html .= $struct['html'];
415
        }
416
        else
417
        {
418 1
          throw new \LogicException("Expected key 'tag', 'text', or 'html'");
419
        }
420
      }
421
    }
422 47
  }
423
424
  //--------------------------------------------------------------------------------------------------------------------
425
  /**
426
   * Generates the HTML code for a start tag of an element.
427
   *
428
   * @param string $tagName    The name of the tag, e.g. a, form.
429
   * @param array  $attributes The attributes of the tag. Special characters in the attributes will be replaced with
430
   *                           HTML entities.
431
   * @param string $html       The generated HTML code.
432
   */
433 44
  private static function htmlTagHelper(string $tagName, array $attributes, string &$html): void
434
  {
435 44
    $html .= '<';
436 44
    $html .= $tagName;
437 44
    foreach ($attributes as $name => $value)
438
    {
439 42
      self::htmlAttributeHelper($name, $value, $html);
440
    }
441 44
    $html .= '>';
442 44
  }
443
444
  //--------------------------------------------------------------------------------------------------------------------
445
  /**
446
   * Generates the HTML code for a void element.
447
   *
448
   * Void elements are: area, base, br, col, embed, hr, img, input, keygen, link, menuitem, meta, param, source, track,
449
   * wbr. See <http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements>
450
   *
451
   * @param string $tagName    The name of the tag, e.g. img, link.
452
   * @param array  $attributes The attributes of the tag. Special characters in the attributes will be replaced with
453
   *                           HTML entities.
454
   * @param string $html       The generated HTML code.
455
   */
456 2
  private static function htmlVoidElementHelper(string $tagName, array $attributes, string &$html): void
457
  {
458 2
    $html .= '<';
459 2
    $html .= $tagName;
460 2
    foreach ($attributes as $name => $value)
461
    {
462 1
      self::htmlAttributeHelper($name, $value, $html);
463
    }
464 2
    $html .= '/>';
465 2
  }
466
467
  //--------------------------------------------------------------------------------------------------------------------
468
}
469
470
//----------------------------------------------------------------------------------------------------------------------
471