Completed
Push — master ( b2e476...c61f67 )
by P.R.
02:52
created

Html::generateNested()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 $encoding = 'UTF-8';
24
25
  /**
26
   * Counter for generating unique element IDs.
27
   *
28
   * @var int
29
   */
30
  private static $autoId = 0;
31
32
  /**
33
   * Map from (some) unicode characters to ASCII characters.
34
   *
35
   * @var array
36
   */
37
  private static $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 12
  public static function generateElement(string $tagName,
224
                                         array $attributes = [],
225
                                         $innerText = '',
226
                                         bool $isHtml = false): string
227
  {
228 12
    $html = self::generateTag($tagName, $attributes);
229 12
    $html .= ($isHtml) ? $innerText : self::txt2Html($innerText);
230 12
    $html .= '</';
231 12
    $html .= $tagName;
232 12
    $html .= '>';
233
234 12
    return $html;
235
  }
236
237
  //--------------------------------------------------------------------------------------------------------------------
238
  /**
239
   * Returns the HTML code of nested elements.
240
   *
241
   * @param array $structure The structure of the nested elements.
242
   *
243
   * @return string
244
   */
245 7
  public static function generateNested(array $structure): string
246
  {
247 7
    $html = '';
248 7
    self::generateNestedHelper($structure, $html);
249
250 7
    return $html;
251
  }
252
253
  //--------------------------------------------------------------------------------------------------------------------
254
  /**
255
   * Generates HTML code for a start tag of an element.
256
   *
257
   * @param string $tagName    The name of the tag, e.g. a, form.
258
   * @param array  $attributes The attributes of the tag. Special characters in the attributes will be replaced with
259
   *                           HTML entities.
260
   *
261
   * @return string
262
   *
263
   * @since 1.0.0
264
   * @api
265
   */
266 18
  public static function generateTag(string $tagName, array $attributes = []): string
267
  {
268 18
    $html = '<';
269 18
    $html .= $tagName;
270 18
    foreach ($attributes as $name => $value)
271
    {
272
      // Ignore attributes with leading underscore.
273 16
      if (strpos($name, '_')!==0) $html .= self::generateAttribute($name, $value);
274
    }
275 18
    $html .= '>';
276
277 18
    return $html;
278
  }
279
280
  //--------------------------------------------------------------------------------------------------------------------
281
  /**
282
   * Generates HTML code for a void element.
283
   *
284
   * Void elements are: area, base, br, col, embed, hr, img, input, keygen, link, menuitem, meta, param, source, track,
285
   * wbr. See <http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements>
286
   *
287
   * @param string $tagName    The name of the tag, e.g. img, link.
288
   * @param array  $attributes The attributes of the tag. Special characters in the attributes will be replaced with
289
   *                           HTML entities.
290
   *
291
   * @return string
292
   *
293
   * @since 1.0.0
294
   * @api
295
   */
296 4
  public static function generateVoidElement(string $tagName, array $attributes = []): string
297
  {
298 4
    $html = '<';
299 4
    $html .= $tagName;
300 4
    foreach ($attributes as $name => $value)
301
    {
302
      // Ignore attributes with leading underscore.
303 2
      if (strpos($name, '_')!==0) $html .= self::generateAttribute($name, $value);
304
    }
305 4
    $html .= '/>';
306
307 4
    return $html;
308
  }
309
310
  //--------------------------------------------------------------------------------------------------------------------
311
  /**
312
   * 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
313
   * incremented with each call of this method.
314
   *
315
   * @return string
316
   *
317
   * @since 1.0.0
318
   * @api
319
   */
320 1
  public static function getAutoId(): string
321
  {
322 1
    self::$autoId++;
323
324 1
    return 'plaisio-id-'.self::$autoId;
325
  }
326
327
  //--------------------------------------------------------------------------------------------------------------------
328
  /**
329
   * Returns a string with special characters converted to HTML entities.
330
   * This method is a wrapper around [htmlspecialchars](http://php.net/manual/en/function.htmlspecialchars.php).
331
   *
332
   * @param bool|int|float|string|null $value The string with optionally special characters.
333
   *
334
   * @return string
335
   *
336
   * @since 1.0.0
337
   * @api
338
   */
339 30
  public static function txt2Html($value): string
340
  {
341
    switch (true)
342
    {
343 30
      case is_string($value):
344 23
        return htmlspecialchars($value, ENT_QUOTES, self::$encoding);
345
346 10
      case is_int($value):
347 9
      case is_float($value):
348 8
      case $value===null:
349 5
        return (string)$value;
350
351 6
      case $value===true:
352 1
        return '1';
353
354 5
      case $value===false:
355 3
        return '0';
356
357
      default:
358 2
        throw new FallenException('type', is_object($value) ? get_class($value) : gettype($value));
359
    }
360
  }
361
362
  //--------------------------------------------------------------------------------------------------------------------
363
  /**
364
   * Returns the slug of a string that can be safely used in an URL.
365
   *
366
   * @param string|null $string The string.
367
   *
368
   * @return string
369
   *
370
   * @since 1.1.0
371
   * @api
372
   */
373 1
  public static function txt2Slug(?string $string): string
374
  {
375 1
    if ($string===null) return '';
376
377 1
    return trim(preg_replace('/[^0-9a-z]+/', '-', strtr(mb_strtolower($string), self::$trans)), '-');
378
  }
379
380
  //--------------------------------------------------------------------------------------------------------------------
381
  /**
382
   * Removes empty and duplicate classes from an array with classes.
383
   *
384
   * @param array $classes The classes.
385
   *
386
   * @return array
387
   */
388 3
  private static function cleanClasses(array $classes): array
389
  {
390 3
    $ret = [];
391
392 3
    foreach ($classes as $class)
393
    {
394 2
      $tmp = Cast::toManString($class, '');
395 2
      if ($tmp!=='') $ret[] = $tmp;
396
    }
397
398 3
    return array_unique($ret);
399
  }
400
401
  //--------------------------------------------------------------------------------------------------------------------
402
  /**
403
   * Helper method for method generateNested().
404
   *
405
   * @param array  $structure The (nested) structure of the HTML code.
406
   * @param string $html      The generated HTML code.
407
   */
408 7
  private static function generateNestedHelper(array $structure, string &$html): void
409
  {
410 7
    $key = array_key_first($structure);
411
412 7
    if (is_int($key))
413
    {
414
      // Structure is a list of elements.
415 2
      foreach ($structure as $element)
416
      {
417 2
        self::generateNestedHelper($element, $html);
418
      }
419
    }
420 7
    elseif ($key!==null)
421
    {
422
      // Structure is an associative array.
423 7
      if (array_key_exists('inner', $structure))
424
      {
425
        // Element with content.
426 6
        $html .= self::generateTag($structure['tag'], $structure['attr'] ?? []);
427 6
        if (is_array($structure['inner']))
428
        {
429 3
          self::generateNestedHelper($structure['inner'], $html);
430
        }
431
        else
432
        {
433 6
          if ($structure['html'] ?? false)
434
          {
435 2
            $html .= $structure['inner'];
436
          }
437
          else
438
          {
439 5
            $html .= self::txt2Html(Cast::toOptString($structure['inner']));
440
          }
441
        }
442 6
        $html .= '</';
443 6
        $html .= $structure['tag'];
444 6
        $html .= '>';
445
      }
446
      else
447
      {
448
        // Void element.
449 2
        $html .= self::generateVoidElement($structure['tag'], $structure['attr'] ?? []);
450
      }
451
    }
452 7
  }
453
454
  //--------------------------------------------------------------------------------------------------------------------
455
}
456
457
//----------------------------------------------------------------------------------------------------------------------
458