Completed
Push — master ( f18ab2...132208 )
by P.R.
06:31
created

Html   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 471
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 196
dl 0
loc 471
ccs 126
cts 126
cp 1
rs 3.52
c 0
b 0
f 0
wmc 61

10 Methods

Rating   Name   Duplication   Size   Complexity  
D generateAttribute() 0 80 28
A generateElement() 0 12 2
B generateNestedHelper() 0 50 10
A getAutoId() 0 5 1
B txt2Html() 0 20 8
A cleanClasses() 0 11 3
A generateTag() 0 12 3
A generateNested() 0 6 1
A txt2Slug() 0 5 2
A generateVoidElement() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like Html often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Html, and based on these observations, apply Extract Interface, too.

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