Completed
Push — master ( 61afef...b5e9d9 )
by Lars
03:02
created

HtmlMin::domNodeToString()   D

Complexity

Conditions 23
Paths 49

Size

Total Lines 92
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 23.4255

Importance

Changes 0
Metric Value
dl 0
loc 92
ccs 39
cts 43
cp 0.907
rs 4.6303
c 0
b 0
f 0
cc 23
eloc 52
nc 49
nop 1
crap 23.4255

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * Class HtmlMin
9
 *
10
 * Inspired by:
11
 * - JS: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js
12
 * - PHP: https://github.com/searchturbine/phpwee-php-minifier
13
 * - PHP: https://github.com/WyriHaximus/HtmlCompress
14
 * - PHP: https://github.com/zaininnari/html-minifier
15
 * - PHP: https://github.com/ampaze/PHP-HTML-Minifier
16
 * - Java: https://code.google.com/archive/p/htmlcompressor/
17
 *
18
 * Ideas:
19
 * - http://perfectionkills.com/optimizing-html/
20
 *
21
 * @package voku\helper
22
 */
23
class HtmlMin
24
{
25
  /**
26
   * @var string
27
   */
28
  private static $regExSpace = "/[[:space:]]{2,}|[\r\n]+/u";
29
30
  /**
31
   * @var array
32
   */
33
  private static $optional_end_tags = [
34
      'html',
35
      'head',
36
      'body',
37
  ];
38
39
  /**
40
   * // https://mathiasbynens.be/demo/javascript-mime-type
41
   * // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
42
   *
43
   * @var array
44
   */
45
  private static $executableScriptsMimeTypes = [
46
      'text/javascript'          => '',
47
      'text/ecmascript'          => '',
48
      'text/jscript'             => '',
49
      'application/javascript'   => '',
50
      'application/x-javascript' => '',
51
      'application/ecmascript'   => '',
52
  ];
53
54
  private static $selfClosingTags = [
55
      'area',
56
      'base',
57
      'basefont',
58
      'br',
59
      'col',
60
      'command',
61
      'embed',
62
      'frame',
63
      'hr',
64
      'img',
65
      'input',
66
      'isindex',
67
      'keygen',
68
      'link',
69
      'meta',
70
      'param',
71
      'source',
72
      'track',
73
      'wbr',
74
  ];
75
76
  private static $trimWhitespaceFromTags = [
77
      'article' => '',
78
      'br'      => '',
79
      'div'     => '',
80
      'footer'  => '',
81
      'hr'      => '',
82
      'nav'     => '',
83
      'p'       => '',
84
      'script'  => '',
85
  ];
86
87
  /**
88
   * @var array
89
   */
90
  private static $booleanAttributes = [
91
      'allowfullscreen' => '',
92
      'async'           => '',
93
      'autofocus'       => '',
94
      'autoplay'        => '',
95
      'checked'         => '',
96
      'compact'         => '',
97
      'controls'        => '',
98
      'declare'         => '',
99
      'default'         => '',
100
      'defaultchecked'  => '',
101
      'defaultmuted'    => '',
102
      'defaultselected' => '',
103
      'defer'           => '',
104
      'disabled'        => '',
105
      'enabled'         => '',
106
      'formnovalidate'  => '',
107
      'hidden'          => '',
108
      'indeterminate'   => '',
109
      'inert'           => '',
110
      'ismap'           => '',
111
      'itemscope'       => '',
112
      'loop'            => '',
113
      'multiple'        => '',
114
      'muted'           => '',
115
      'nohref'          => '',
116
      'noresize'        => '',
117
      'noshade'         => '',
118
      'novalidate'      => '',
119
      'nowrap'          => '',
120
      'open'            => '',
121
      'pauseonexit'     => '',
122
      'readonly'        => '',
123
      'required'        => '',
124
      'reversed'        => '',
125
      'scoped'          => '',
126
      'seamless'        => '',
127
      'selected'        => '',
128
      'sortable'        => '',
129
      'truespeed'       => '',
130
      'typemustmatch'   => '',
131
      'visible'         => '',
132
  ];
133
  /**
134
   * @var array
135
   */
136
  private static $skipTagsForRemoveWhitespace = [
137
      'code',
138
      'pre',
139
      'script',
140
      'style',
141
      'textarea',
142
  ];
143
144
  /**
145
   * @var array
146
   */
147
  private $protectedChildNodes = [];
148
149
  /**
150
   * @var string
151
   */
152
  private $protectedChildNodesHelper = 'html-min--voku--saved-content';
153
154
  /**
155
   * @var bool
156
   */
157
  private $doOptimizeViaHtmlDomParser = true;
158
159
  /**
160
   * @var bool
161
   */
162
  private $doOptimizeAttributes = true;
163
164
  /**
165
   * @var bool
166
   */
167
  private $doRemoveComments = true;
168
169
  /**
170
   * @var bool
171
   */
172
  private $doRemoveWhitespaceAroundTags = false;
173
174
  /**
175
   * @var bool
176
   */
177
  private $doRemoveOmittedQuotes = true;
178
179
  /**
180
   * @var bool
181
   */
182
  private $doRemoveOmittedHtmlTags = true;
183
184
  /**
185
   * @var bool
186
   */
187
  private $doRemoveHttpPrefixFromAttributes = false;
188
189
  /**
190
   * @var array
191
   */
192
  private $domainsToRemoveHttpPrefixFromAttributes = [
193
      'google.com',
194
      'google.de',
195
  ];
196
197
  /**
198
   * @var bool
199
   */
200
  private $doSortCssClassNames = true;
201
202
  /**
203
   * @var bool
204
   */
205
  private $doSortHtmlAttributes = true;
206
207
  /**
208
   * @var bool
209
   */
210
  private $doRemoveDeprecatedScriptCharsetAttribute = true;
211
212
  /**
213
   * @var bool
214
   */
215
  private $doRemoveDefaultAttributes = false;
216
217
  /**
218
   * @var bool
219
   */
220
  private $doRemoveDeprecatedAnchorName = true;
221
222
  /**
223
   * @var bool
224
   */
225
  private $doRemoveDeprecatedTypeFromStylesheetLink = true;
226
227
  /**
228
   * @var bool
229
   */
230
  private $doRemoveDeprecatedTypeFromScriptTag = true;
231
232
  /**
233
   * @var bool
234
   */
235
  private $doRemoveValueFromEmptyInput = true;
236
237
  /**
238
   * @var bool
239
   */
240
  private $doRemoveEmptyAttributes = true;
241
242
  /**
243
   * @var bool
244
   */
245
  private $doSumUpWhitespace = true;
246
247
  /**
248
   * @var bool
249
   */
250
  private $doRemoveSpacesBetweenTags = false;
251
252
  /**
253
   * @var
254
   */
255
  private $withDocType;
256
257
  /**
258
   * HtmlMin constructor.
259
   */
260 31
  public function __construct()
261
  {
262 31
  }
263
264
  /**
265
   * @param boolean $doOptimizeAttributes
266
   *
267
   * @return $this
268
   */
269 2
  public function doOptimizeAttributes(bool $doOptimizeAttributes = true)
270
  {
271 2
    $this->doOptimizeAttributes = $doOptimizeAttributes;
272
273 2
    return $this;
274
  }
275
276
  /**
277
   * @param boolean $doOptimizeViaHtmlDomParser
278
   *
279
   * @return $this
280
   */
281 1
  public function doOptimizeViaHtmlDomParser(bool $doOptimizeViaHtmlDomParser = true)
282
  {
283 1
    $this->doOptimizeViaHtmlDomParser = $doOptimizeViaHtmlDomParser;
284
285 1
    return $this;
286
  }
287
288
  /**
289
   * @param boolean $doRemoveComments
290
   *
291
   * @return $this
292
   */
293 2
  public function doRemoveComments(bool $doRemoveComments = true)
294
  {
295 2
    $this->doRemoveComments = $doRemoveComments;
296
297 2
    return $this;
298
  }
299
300
  /**
301
   * @param boolean $doRemoveDefaultAttributes
302
   *
303
   * @return $this
304
   */
305 2
  public function doRemoveDefaultAttributes(bool $doRemoveDefaultAttributes = true)
306
  {
307 2
    $this->doRemoveDefaultAttributes = $doRemoveDefaultAttributes;
308
309 2
    return $this;
310
  }
311
312
  /**
313
   * @param boolean $doRemoveDeprecatedAnchorName
314
   *
315
   * @return $this
316
   */
317 2
  public function doRemoveDeprecatedAnchorName(bool $doRemoveDeprecatedAnchorName = true)
318
  {
319 2
    $this->doRemoveDeprecatedAnchorName = $doRemoveDeprecatedAnchorName;
320
321 2
    return $this;
322
  }
323
324
  /**
325
   * @param boolean $doRemoveDeprecatedScriptCharsetAttribute
326
   *
327
   * @return $this
328
   */
329 2
  public function doRemoveDeprecatedScriptCharsetAttribute(bool $doRemoveDeprecatedScriptCharsetAttribute = true)
330
  {
331 2
    $this->doRemoveDeprecatedScriptCharsetAttribute = $doRemoveDeprecatedScriptCharsetAttribute;
332
333 2
    return $this;
334
  }
335
336
  /**
337
   * @param boolean $doRemoveDeprecatedTypeFromScriptTag
338
   *
339
   * @return $this
340
   */
341 2
  public function doRemoveDeprecatedTypeFromScriptTag(bool $doRemoveDeprecatedTypeFromScriptTag = true)
342
  {
343 2
    $this->doRemoveDeprecatedTypeFromScriptTag = $doRemoveDeprecatedTypeFromScriptTag;
344
345 2
    return $this;
346
  }
347
348
  /**
349
   * @param boolean $doRemoveDeprecatedTypeFromStylesheetLink
350
   *
351
   * @return $this
352
   */
353 2
  public function doRemoveDeprecatedTypeFromStylesheetLink(bool $doRemoveDeprecatedTypeFromStylesheetLink = true)
354
  {
355 2
    $this->doRemoveDeprecatedTypeFromStylesheetLink = $doRemoveDeprecatedTypeFromStylesheetLink;
356
357 2
    return $this;
358
  }
359
360
  /**
361
   * @param boolean $doRemoveEmptyAttributes
362
   *
363
   * @return $this
364
   */
365 2
  public function doRemoveEmptyAttributes(bool $doRemoveEmptyAttributes = true)
366
  {
367 2
    $this->doRemoveEmptyAttributes = $doRemoveEmptyAttributes;
368
369 2
    return $this;
370
  }
371
372
  /**
373
   * @param boolean $doRemoveHttpPrefixFromAttributes
374
   *
375
   * @return $this
376
   */
377 4
  public function doRemoveHttpPrefixFromAttributes(bool $doRemoveHttpPrefixFromAttributes = true)
378
  {
379 4
    $this->doRemoveHttpPrefixFromAttributes = $doRemoveHttpPrefixFromAttributes;
380
381 4
    return $this;
382
  }
383
384
  /**
385
   * @param boolean $doRemoveSpacesBetweenTags
386
   *
387
   * @return $this
388
   */
389
  public function doRemoveSpacesBetweenTags(bool $doRemoveSpacesBetweenTags = true)
390
  {
391
    $this->doRemoveSpacesBetweenTags = $doRemoveSpacesBetweenTags;
392
393
    return $this;
394
  }
395
396
  /**
397
   * @param boolean $doRemoveValueFromEmptyInput
398
   *
399
   * @return $this
400
   */
401 2
  public function doRemoveValueFromEmptyInput(bool $doRemoveValueFromEmptyInput = true)
402
  {
403 2
    $this->doRemoveValueFromEmptyInput = $doRemoveValueFromEmptyInput;
404
405 2
    return $this;
406
  }
407
408
  /**
409
   * @param boolean $doRemoveWhitespaceAroundTags
410
   *
411
   * @return $this
412
   */
413 4
  public function doRemoveWhitespaceAroundTags(bool $doRemoveWhitespaceAroundTags = true)
414
  {
415 4
    $this->doRemoveWhitespaceAroundTags = $doRemoveWhitespaceAroundTags;
416
417 4
    return $this;
418
  }
419
420
  /**
421
   * @param bool $doRemoveOmittedQuotes
422
   *
423
   * @return $this
424
   */
425 1
  public function doRemoveOmittedQuotes(bool $doRemoveOmittedQuotes = true)
426
  {
427 1
    $this->doRemoveOmittedQuotes = $doRemoveOmittedQuotes;
428
429 1
    return $this;
430
  }
431
432
  /**
433
   * @param bool $doRemoveOmittedHtmlTags
434
   *
435
   * @return $this
436
   */
437 1
  public function doRemoveOmittedHtmlTags(bool $doRemoveOmittedHtmlTags = true)
438
  {
439 1
    $this->doRemoveOmittedHtmlTags = $doRemoveOmittedHtmlTags;
440
441 1
    return $this;
442
  }
443
444
  /**
445
   * @param boolean $doSortCssClassNames
446
   *
447
   * @return $this
448
   */
449 2
  public function doSortCssClassNames(bool $doSortCssClassNames = true)
450
  {
451 2
    $this->doSortCssClassNames = $doSortCssClassNames;
452
453 2
    return $this;
454
  }
455
456
  /**
457
   * @param boolean $doSortHtmlAttributes
458
   *
459
   * @return $this
460
   */
461 2
  public function doSortHtmlAttributes(bool $doSortHtmlAttributes = true)
462
  {
463 2
    $this->doSortHtmlAttributes = $doSortHtmlAttributes;
464
465 2
    return $this;
466
  }
467
468
  /**
469
   * @param boolean $doSumUpWhitespace
470
   *
471
   * @return $this
472
   */
473 2
  public function doSumUpWhitespace(bool $doSumUpWhitespace = true)
474
  {
475 2
    $this->doSumUpWhitespace = $doSumUpWhitespace;
476
477 2
    return $this;
478
  }
479
480 27
  private function domNodeAttributesToString(\DOMNode $node): string
481
  {
482
    # Remove quotes around attribute values, when allowed (<p class="foo"> → <p class=foo>)
483 27
    $attrstr = '';
484 27
    if ($node->attributes != null) {
485 27
      foreach ($node->attributes as $attribute) {
486 16
        $attrstr .= $attribute->name;
487
488
        if (
489 16
            $this->doOptimizeAttributes === true
490
            &&
491 16
            isset(self::$booleanAttributes[$attribute->name])
492
        ) {
493 6
          $attrstr .= ' ';
494 6
          continue;
495
        }
496
497 16
        $attrstr .= '=';
498
499
        # http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#attributes-0
500 16
        $omitquotes = $this->doRemoveOmittedQuotes
501
                      &&
502 16
                      $attribute->value != ''
503
                      &&
504 16
                      0 == \preg_match('/["\'=<>` \t\r\n\f]+/', $attribute->value);
505
506 16
        $attr_val = $attribute->value;
507 16
        $attrstr .= ($omitquotes ? '' : '"') . $attr_val . ($omitquotes ? '' : '"');
508 16
        $attrstr .= ' ';
509
      }
510
    }
511
512 27
    return \trim($attrstr);
513
  }
514
515
  /**
516
   * @param \DOMNode $node
517
   *
518
   * @return bool
519
   */
520 26
  private function domNodeClosingTagOptional(\DOMNode $node): bool
521
  {
522 26
    $tag_name = $node->nodeName;
523 26
    $nextSibling = $this->getNextSiblingOfTypeDOMElement($node);
524
525
    // https://html.spec.whatwg.org/multipage/syntax.html#syntax-tag-omission
526
527
    // Implemented:
528
    //
529
    // A <p> element's end tag may be omitted if the p element is immediately followed by an address, article, aside, blockquote, details, div, dl, fieldset, figcaption, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, hr, main, menu, nav, ol, p, pre, section, table, or ul element, or if there is no more content in the parent element and the parent element is an HTML element that is not an a, audio, del, ins, map, noscript, or video element, or an autonomous custom element.
530
    // An <li> element's end tag may be omitted if the li element is immediately followed by another li element or if there is no more content in the parent element.
531
    // A <td> element's end tag may be omitted if the td element is immediately followed by a td or th element, or if there is no more content in the parent element.
532
    // An <option> element's end tag may be omitted if the option element is immediately followed by another option element, or if it is immediately followed by an optgroup element, or if there is no more content in the parent element.
533
    // A <tr> element's end tag may be omitted if the tr element is immediately followed by another tr element, or if there is no more content in the parent element.
534
    // A <th> element's end tag may be omitted if the th element is immediately followed by a td or th element, or if there is no more content in the parent element.
535
    // A <dt> element's end tag may be omitted if the dt element is immediately followed by another dt element or a dd element.
536
    // A <dd> element's end tag may be omitted if the dd element is immediately followed by another dd element or a dt element, or if there is no more content in the parent element.
537
    // An <rp> element's end tag may be omitted if the rp element is immediately followed by an rt or rp element, or if there is no more content in the parent element.
538
539
    // TODO:
540
    //
541
    // <html> may be omitted if first thing inside is not comment
542
    // <head> may be omitted if first thing inside is an element
543
    // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
544
    // <colgroup> may be omitted if first thing inside is <col>
545
    // <tbody> may be omitted if first thing inside is <tr>
546
    // An <optgroup> element's end tag may be omitted if the optgroup element is immediately followed by another optgroup element, or if there is no more content in the parent element.
547
    // A <colgroup> element's start tag may be omitted if the first thing inside the colgroup element is a col element, and if the element is not immediately preceded by another colgroup element whose end tag has been omitted. (It can't be omitted if the element is empty.)
548
    // A <colgroup> element's end tag may be omitted if the colgroup element is not immediately followed by ASCII whitespace or a comment.
549
    // A <caption> element's end tag may be omitted if the caption element is not immediately followed by ASCII whitespace or a comment.
550
    // A <thead> element's end tag may be omitted if the thead element is immediately followed by a tbody or tfoot element.
551
    // A <tbody> element's start tag may be omitted if the first thing inside the tbody element is a tr element, and if the element is not immediately preceded by a tbody, thead, or tfoot element whose end tag has been omitted. (It can't be omitted if the element is empty.)
552
    // A <tbody> element's end tag may be omitted if the tbody element is immediately followed by a tbody or tfoot element, or if there is no more content in the parent element.
553
    // A <tfoot> element's end tag may be omitted if there is no more content in the parent element.
554
    //
555
    // <-- However, a start tag must never be omitted if it has any attributes.
556
557 26
    return \in_array($tag_name, self::$optional_end_tags, true)
558
           ||
559
           (
560 23
               $tag_name == 'li'
561
               &&
562
               (
563 4
                   $nextSibling === null
564
                   ||
565
                   (
566 2
                       $nextSibling instanceof \DOMElement
567
                       &&
568 23
                       $nextSibling->tagName == 'li'
569
                   )
570
               )
571
           )
572
           ||
573
           (
574
               (
575 23
                   $tag_name == 'rp'
576
               )
577
               &&
578
               (
579
                   $nextSibling === null
580
                   ||
581
                   (
582
                       $nextSibling instanceof \DOMElement
583
                       &&
584
                       (
585
                           $nextSibling->tagName == 'rp'
586
                           ||
587 23
                           $nextSibling->tagName == 'rt'
588
                       )
589
                   )
590
               )
591
           )
592
           ||
593
           (
594 23
               $tag_name == 'tr'
595
               &&
596
               (
597 1
                   $nextSibling === null
598
                   ||
599
                   (
600 1
                       $nextSibling instanceof \DOMElement
601
                       &&
602 23
                       $nextSibling->tagName == 'tr'
603
                   )
604
               )
605
           )
606
           ||
607
           (
608
               (
609 23
                   $tag_name == 'td'
610
                   ||
611 23
                   $tag_name == 'th'
612
               )
613
               &&
614
               (
615 1
                   $nextSibling === null
616
                   ||
617
                   (
618 1
                       $nextSibling instanceof \DOMElement
619
                       &&
620
                       (
621 1
                           $nextSibling->tagName == 'td'
622
                           ||
623 23
                           $nextSibling->tagName == 'th'
624
                       )
625
                   )
626
               )
627
           )
628
           ||
629
           (
630
               (
631 23
                   $tag_name == 'dd'
632
                   ||
633 23
                   $tag_name == 'dt'
634
               )
635
               &&
636
               (
637
                   (
638 3
                       $nextSibling === null
639
                       &&
640 3
                       $tag_name == 'dd'
641
                   )
642
                   ||
643
                   (
644 3
                       $nextSibling instanceof \DOMElement
645
                       &&
646
                       (
647 3
                           $nextSibling->tagName == 'dd'
648
                           ||
649 23
                           $nextSibling->tagName == 'dt'
650
                       )
651
                   )
652
               )
653
           )
654
           ||
655
           (
656 23
               $tag_name == 'option'
657
               &&
658
               (
659
                   $nextSibling === null
660
                   ||
661
                   (
662
                       $nextSibling instanceof \DOMElement
663
                       &&
664
                       (
665
                           $nextSibling->tagName == 'option'
666
                           ||
667 23
                           $nextSibling->tagName == 'optgroup'
668
                       )
669
                   )
670
               )
671
           )
672
           ||
673
           (
674 23
               $tag_name == 'p'
675
               &&
676
               (
677
                   (
678 9
                       $nextSibling === null
679
                       &&
680
                       (
681 9
                           $node->parentNode !== null
682
                           &&
683 9
                           !\in_array(
684 9
                               $node->parentNode->nodeName,
685
                               [
686 9
                                   'a',
687
                                   'audio',
688
                                   'del',
689
                                   'ins',
690
                                   'map',
691
                                   'noscript',
692
                                   'video',
693
                               ],
694 9
                               true
695
                           )
696
                       )
697
                   )
698
                   ||
699
                   (
700 5
                       $nextSibling instanceof \DOMElement
701
                       &&
702 5
                       \in_array(
703 5
                           $nextSibling->tagName,
704
                           [
705 5
                               'address',
706
                               'article',
707
                               'aside',
708
                               'blockquote',
709
                               'dir',
710
                               'div',
711
                               'dl',
712
                               'fieldset',
713
                               'footer',
714
                               'form',
715
                               'h1',
716
                               'h2',
717
                               'h3',
718
                               'h4',
719
                               'h5',
720
                               'h6',
721
                               'header',
722
                               'hgroup',
723
                               'hr',
724
                               'menu',
725
                               'nav',
726
                               'ol',
727
                               'p',
728
                               'pre',
729
                               'section',
730
                               'table',
731
                               'ul',
732
                           ],
733 26
                           true
734
                       )
735
                   )
736
               )
737
           );
738
  }
739
740 27
  protected function domNodeToString(\DOMNode $node): string
741
  {
742
    // init
743 27
    $html = '';
744 27
    $emptyStringTmp = '';
745
746 27
    foreach ($node->childNodes as $child) {
747
748 27
      if ($emptyStringTmp === 'is_empty') {
749 18
        $emptyStringTmp = 'last_was_empty';
750
      } else {
751 27
        $emptyStringTmp = '';
752
      }
753
754 27
      if ($child instanceof \DOMDocumentType) {
755
756
        // add the doc-type only if it wasn't generated by DomDocument
757 8
        if ($this->withDocType !== true) {
758
          continue;
759
        }
760
761 8
        if ($child->name) {
762
763 8
          if (!$child->publicId && $child->systemId) {
764
            $tmpTypeSystem = 'SYSTEM';
765
            $tmpTypePublic = '';
766
          } else {
767 8
            $tmpTypeSystem = '';
768 8
            $tmpTypePublic = 'PUBLIC';
769
          }
770
771 8
          $html .= '<!DOCTYPE ' . $child->name . ''
772 8
                   . ($child->publicId ? ' ' . $tmpTypePublic . ' "' . $child->publicId . '"' : '')
773 8
                   . ($child->systemId ? ' ' . $tmpTypeSystem . ' "' . $child->systemId . '"' : '')
774 8
                   . '>';
775
        }
776
777 27
      } elseif ($child instanceof \DOMElement) {
778
779 27
        $html .= \rtrim('<' . $child->tagName . ' ' . $this->domNodeAttributesToString($child));
780 27
        $html .= '>' . $this->domNodeToString($child);
781
782
        if (
783 27
            $this->doRemoveOmittedHtmlTags === false
784
            ||
785 27
            !$this->domNodeClosingTagOptional($child)
786
        ) {
787 21
          $html .= '</' . $child->tagName . '>';
788
        }
789
790 27
        if ($this->doRemoveWhitespaceAroundTags === false) {
791
          if (
792 26
              $child->nextSibling instanceof \DOMText
793
              &&
794 26
              $child->nextSibling->wholeText === ' '
795
          ) {
796 17
            if ($emptyStringTmp !== 'last_was_empty') {
797 17
              $html .= ' ';
798
            }
799 27
            $emptyStringTmp = 'is_empty';
800
          }
801
        }
802
803 23
      } elseif ($child instanceof \DOMText) {
804
805 23
        if ($child->isElementContentWhitespace()) {
806
          if (
807 19
              $child->previousSibling !== null
808
              &&
809 19
              $child->nextSibling !== null
810
          ) {
811 11
            if ($emptyStringTmp !== 'last_was_empty') {
812 3
              $html .= ' ';
813
            }
814 19
            $emptyStringTmp = 'is_empty';
815
          }
816
817
        } else {
818
819 23
          $html .= $child->wholeText;
820
821
        }
822
823
      } elseif ($child instanceof \DOMComment) {
824
825 27
        $html .= $child->wholeText;
826
827
      }
828
    }
829
830 27
    return $html;
831
  }
832
833
  /**
834
   * @param \DOMNode $node
835
   *
836
   * @return \DOMNode|null
837
   */
838 26
  protected function getNextSiblingOfTypeDOMElement(\DOMNode $node)
839
  {
840
    do {
841 26
      $node = $node->nextSibling;
842 26
    } while (!($node === null || $node instanceof \DOMElement));
843
844 26
    return $node;
845
  }
846
847
  /**
848
   * Check if the current string is an conditional comment.
849
   *
850
   * INFO: since IE >= 10 conditional comment are not working anymore
851
   *
852
   * <!--[if expression]> HTML <![endif]-->
853
   * <![if expression]> HTML <![endif]>
854
   *
855
   * @param string $comment
856
   *
857
   * @return bool
858
   */
859 3
  private function isConditionalComment($comment): bool
860
  {
861 3
    if (preg_match('/^\[if [^\]]+\]/', $comment)) {
862 2
      return true;
863
    }
864
865 3
    if (preg_match('/\[endif\]$/', $comment)) {
866 1
      return true;
867
    }
868
869 3
    return false;
870
  }
871
872
  /**
873
   * @param string $html
874
   * @param bool   $decodeUtf8Specials <p>Use this only in special cases, e.g. for PHP 5.3</p>
875
   *
876
   * @return string
877
   */
878 31
  public function minify($html, $decodeUtf8Specials = false): string
879
  {
880 31
    $html = (string)$html;
881 31
    if (!isset($html[0])) {
882 1
      return '';
883
    }
884
885 31
    $html = \trim($html);
886 31
    if (!$html) {
887 3
      return '';
888
    }
889
890
    // init
891 28
    static $CACHE_SELF_CLOSING_TAGS = null;
892 28
    if ($CACHE_SELF_CLOSING_TAGS === null) {
893 1
      $CACHE_SELF_CLOSING_TAGS = \implode('|', self::$selfClosingTags);
894
    }
895
896
    // reset
897 28
    $this->protectedChildNodes = [];
898
899
    // save old content
900 28
    $origHtml = $html;
901 28
    $origHtmlLength = UTF8::strlen($html);
902
903
    // -------------------------------------------------------------------------
904
    // Minify the HTML via "HtmlDomParser"
905
    // -------------------------------------------------------------------------
906
907 28
    if ($this->doOptimizeViaHtmlDomParser === true) {
908 27
      $html = $this->minifyHtmlDom($html, $decodeUtf8Specials);
909
    }
910
911
    // -------------------------------------------------------------------------
912
    // Trim whitespace from html-string. [protected html is still protected]
913
    // -------------------------------------------------------------------------
914
915
    // Remove extra white-space(s) between HTML attribute(s)
916 28
    $html = (string)\preg_replace_callback(
917 28
        '#<([^\/\s<>!]+)(?:\s+([^<>]*?)\s*|\s*)(\/?)>#',
918 28
        function ($matches) {
919 28
          return '<' . $matches[1] . (string)\preg_replace('#([^\s=]+)(\=([\'"]?)(.*?)\3)?(\s+|$)#s', ' $1$2', $matches[2]) . $matches[3] . '>';
920 28
        },
921 28
        $html
922
    );
923
924 28
    if ($this->doRemoveSpacesBetweenTags === true) {
925
      // Remove spaces that are between > and <
926
      $html = (string)\preg_replace('/(>) (<)/', '>$2', $html);
927
    }
928
929
    // -------------------------------------------------------------------------
930
    // Restore protected HTML-code.
931
    // -------------------------------------------------------------------------
932
933 28
    $html = (string)\preg_replace_callback(
934 28
        '/<(?<element>' . $this->protectedChildNodesHelper . ')(?<attributes> [^>]*)?>(?<value>.*?)<\/' . $this->protectedChildNodesHelper . '>/',
935 28
        [$this, 'restoreProtectedHtml'],
936 28
        $html
937
    );
938
939
    // -------------------------------------------------------------------------
940
    // Restore protected HTML-entities.
941
    // -------------------------------------------------------------------------
942
943 28
    if ($this->doOptimizeViaHtmlDomParser === true) {
944 27
      $html = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($html);
945
    }
946
947
    // ------------------------------------
948
    // Final clean-up
949
    // ------------------------------------
950
951 28
    $html = UTF8::cleanup($html);
952
953 28
    $html = \str_replace(
954
        [
955 28
            'html>' . "\n",
956
            "\n" . '<html',
957
            'html/>' . "\n",
958
            "\n" . '</html',
959
            'head>' . "\n",
960
            "\n" . '<head',
961
            'head/>' . "\n",
962
            "\n" . '</head',
963
        ],
964
        [
965 28
            'html>',
966
            '<html',
967
            'html/>',
968
            '</html',
969
            'head>',
970
            '<head',
971
            'head/>',
972
            '</head',
973
        ],
974 28
        $html
975
    );
976
977
    // self closing tags, don't need a trailing slash ...
978 28
    $replace = [];
979 28
    $replacement = [];
980 28
    foreach (self::$selfClosingTags as $selfClosingTag) {
981 28
      $replace[] = '<' . $selfClosingTag . '/>';
982 28
      $replacement[] = '<' . $selfClosingTag . '>';
983 28
      $replace[] = '<' . $selfClosingTag . ' />';
984 28
      $replacement[] = '<' . $selfClosingTag . '>';
985
    }
986 28
    $html = \str_replace(
987 28
        $replace,
988 28
        $replacement,
989 28
        $html
990
    );
991
992 28
    $html = (string)\preg_replace('#<\b(' . $CACHE_SELF_CLOSING_TAGS . ')([^>]+)><\/\b\1>#', '<\\1\\2>', $html);
993
994
    // ------------------------------------
995
    // check if compression worked
996
    // ------------------------------------
997
998 28
    if ($origHtmlLength < UTF8::strlen($html)) {
999 2
      $html = $origHtml;
1000
    }
1001
1002 28
    return $html;
1003
  }
1004
1005
  /**
1006
   * @param $html
1007
   * @param $decodeUtf8Specials
1008
   *
1009
   * @return string
1010
   */
1011 27
  private function minifyHtmlDom($html, $decodeUtf8Specials): string
1012
  {
1013
    // init dom
1014 27
    $dom = new HtmlDomParser();
1015 27
    $dom->getDocument()->preserveWhiteSpace = false; // remove redundant white space
1016 27
    $dom->getDocument()->formatOutput = false; // do not formats output with indentation
1017
1018
    // load dom
1019 27
    $dom->loadHtml($html);
1020
1021 27
    $this->withDocType = (\stripos(\ltrim($html), '<!DOCTYPE') === 0);
1022
1023
    // -------------------------------------------------------------------------
1024
    // Protect HTML tags and conditional comments.
1025
    // -------------------------------------------------------------------------
1026
1027 27
    $dom = $this->protectTags($dom);
1028
1029
    // -------------------------------------------------------------------------
1030
    // Remove default HTML comments. [protected html is still protected]
1031
    // -------------------------------------------------------------------------
1032
1033 27
    if ($this->doRemoveComments === true) {
1034 26
      $dom = $this->removeComments($dom);
1035
    }
1036
1037
    // -------------------------------------------------------------------------
1038
    // Sum-Up extra whitespace from the Dom. [protected html is still protected]
1039
    // -------------------------------------------------------------------------
1040
1041 27
    if ($this->doSumUpWhitespace === true) {
1042 26
      $dom = $this->sumUpWhitespace($dom);
1043
    }
1044
1045 27
    foreach ($dom->find('*') as $element) {
0 ignored issues
show
Bug introduced by
The expression $dom->find('*') of type array<integer,object<vok...leHtmlDomNodeInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1046
1047
      // -------------------------------------------------------------------------
1048
      // Optimize html attributes. [protected html is still protected]
1049
      // -------------------------------------------------------------------------
1050
1051 27
      if ($this->doOptimizeAttributes === true) {
1052 26
        $this->optimizeAttributes($element);
1053
      }
1054
1055
      // -------------------------------------------------------------------------
1056
      // Remove whitespace around tags. [protected html is still protected]
1057
      // -------------------------------------------------------------------------
1058
1059 27
      if ($this->doRemoveWhitespaceAroundTags === true) {
1060 27
        $this->removeWhitespaceAroundTags($element);
1061
      }
1062
    }
1063
1064
    // -------------------------------------------------------------------------
1065
    // Convert the Dom into a string.
1066
    // -------------------------------------------------------------------------
1067
1068 27
    $html = $dom->fixHtmlOutput(
1069 27
        $this->domNodeToString($dom->getDocument()),
1070 27
        $decodeUtf8Specials
1071
    );
1072
1073 27
    return $html;
1074
  }
1075
1076
  /**
1077
   * Sort HTML-Attributes, so that gzip can do better work and remove some default attributes...
1078
   *
1079
   * @param SimpleHtmlDom $element
1080
   *
1081
   * @return bool
1082
   */
1083 26
  private function optimizeAttributes(SimpleHtmlDom $element): bool
1084
  {
1085 26
    $attributes = $element->getAllAttributes();
1086 26
    if ($attributes === null) {
1087 26
      return false;
1088
    }
1089
1090 15
    $attrs = [];
1091 15
    foreach ((array)$attributes as $attrName => $attrValue) {
1092
1093
      // -------------------------------------------------------------------------
1094
      // Remove optional "http:"-prefix from attributes.
1095
      // -------------------------------------------------------------------------
1096
1097 15
      if ($this->doRemoveHttpPrefixFromAttributes === true) {
1098
        if (
1099 3
            ($attrName === 'href' || $attrName === 'src' || $attrName === 'action')
1100
            &&
1101 3
            !(isset($attributes['rel']) && $attributes['rel'] === 'external')
1102
            &&
1103 3
            !(isset($attributes['target']) && $attributes['target'] === '_blank')
1104
        ) {
1105 2
          $attrValue = \str_replace('http://', '//', $attrValue);
1106
        }
1107
      }
1108
1109 15
      if ($this->removeAttributeHelper($element->tag, $attrName, $attrValue, $attributes)) {
1110 3
        $element->{$attrName} = null;
1111 3
        continue;
1112
      }
1113
1114
      // -------------------------------------------------------------------------
1115
      // Sort css-class-names, for better gzip results.
1116
      // -------------------------------------------------------------------------
1117
1118 15
      if ($this->doSortCssClassNames === true) {
1119 15
        $attrValue = $this->sortCssClassNames($attrName, $attrValue);
1120
      }
1121
1122 15
      if ($this->doSortHtmlAttributes === true) {
1123 15
        $attrs[$attrName] = $attrValue;
1124 15
        $element->{$attrName} = null;
1125
      }
1126
    }
1127
1128
    // -------------------------------------------------------------------------
1129
    // Sort html-attributes, for better gzip results.
1130
    // -------------------------------------------------------------------------
1131
1132 15
    if ($this->doSortHtmlAttributes === true) {
1133 15
      \ksort($attrs);
1134 15
      foreach ($attrs as $attrName => $attrValue) {
1135 15
        $attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue);
1136 15
        $element->setAttribute($attrName, $attrValue, true);
1137
      }
1138
    }
1139
1140 15
    return true;
1141
  }
1142
1143
  /**
1144
   * Prevent changes of inline "styles" and "scripts".
1145
   *
1146
   * @param HtmlDomParser $dom
1147
   *
1148
   * @return HtmlDomParser
1149
   */
1150 27
  private function protectTags(HtmlDomParser $dom): HtmlDomParser
1151
  {
1152
    // init
1153 27
    $counter = 0;
1154
1155 27
    foreach ($dom->find('script, style') as $element) {
0 ignored issues
show
Bug introduced by
The expression $dom->find('script, style') of type array<integer,object<vok...leHtmlDomNodeInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1156
1157
      // skip external links
1158 3
      if ($element->tag === 'script' || $element->tag === 'style') {
1159 3
        $attributes = $element->getAllAttributes();
1160 3
        if (isset($attributes['src'])) {
1161 2
          continue;
1162
        }
1163
      }
1164
1165 2
      $this->protectedChildNodes[$counter] = $element->text();
1166 2
      $element->getNode()->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $counter . '"></' . $this->protectedChildNodesHelper . '>';
1167
1168 2
      ++$counter;
1169
    }
1170
1171 27
    $dom->getDocument()->normalizeDocument();
1172
1173 27
    foreach ($dom->find('//comment()') as $element) {
0 ignored issues
show
Bug introduced by
The expression $dom->find('//comment()') of type array<integer,object<vok...leHtmlDomNodeInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1174 3
      $text = $element->text();
1175
1176
      // skip normal comments
1177 3
      if ($this->isConditionalComment($text) === false) {
1178 3
        continue;
1179
      }
1180
1181 2
      $this->protectedChildNodes[$counter] = '<!--' . $text . '-->';
1182
1183
      /* @var $node \DOMComment */
1184 2
      $node = $element->getNode();
1185 2
      $child = new \DOMText('<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $counter . '"></' . $this->protectedChildNodesHelper . '>');
1186 2
      $element->getNode()->parentNode->replaceChild($child, $node);
1187
1188 2
      ++$counter;
1189
    }
1190
1191 27
    $dom->getDocument()->normalizeDocument();
1192
1193 27
    return $dom;
1194
  }
1195
1196
  /**
1197
   * Check if the attribute can be removed.
1198
   *
1199
   * @param string $tag
1200
   * @param string $attrName
1201
   * @param string $attrValue
1202
   * @param array  $allAttr
1203
   *
1204
   * @return bool
1205
   */
1206 15
  private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr): bool
1207
  {
1208
    // remove defaults
1209 15
    if ($this->doRemoveDefaultAttributes === true) {
1210
1211 1
      if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') {
1212
        return true;
1213
      }
1214
1215 1
      if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') {
1216
        return true;
1217
      }
1218
1219 1
      if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') {
1220
        return true;
1221
      }
1222
1223 1
      if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') {
1224
        return true;
1225
      }
1226
    }
1227
1228
    // remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway)
1229 15 View Code Duplication
    if ($this->doRemoveDeprecatedScriptCharsetAttribute === true) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1230 15
      if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) {
1231
        return true;
1232
      }
1233
    }
1234
1235
    // remove deprecated anchor-jump
1236 15 View Code Duplication
    if ($this->doRemoveDeprecatedAnchorName === true) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1237 15
      if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) {
1238
        return true;
1239
      }
1240
    }
1241
1242
    // remove "type=text/css" for css links
1243 15 View Code Duplication
    if ($this->doRemoveDeprecatedTypeFromStylesheetLink === true) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1244 15
      if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet') {
1245 1
        return true;
1246
      }
1247
    }
1248
1249
    // remove deprecated script-mime-types
1250 15 View Code Duplication
    if ($this->doRemoveDeprecatedTypeFromScriptTag === true) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1251 15
      if ($tag === 'script' && $attrName === 'type' && isset($allAttr['src'], self::$executableScriptsMimeTypes[$attrValue])) {
1252 1
        return true;
1253
      }
1254
    }
1255
1256
    // remove 'value=""' from <input type="text">
1257 15
    if ($this->doRemoveValueFromEmptyInput === true) {
1258 15
      if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') {
1259 1
        return true;
1260
      }
1261
    }
1262
1263
    // remove some empty attributes
1264 15
    if ($this->doRemoveEmptyAttributes === true) {
1265 15
      if (\trim($attrValue) === '' && \preg_match('/^(?:class|id|style|title|lang|dir|on(?:focus|blur|change|click|dblclick|mouse(?:down|up|over|move|out)|key(?:press|down|up)))$/', $attrName)) {
1266 2
        return true;
1267
      }
1268
    }
1269
1270 15
    return false;
1271
  }
1272
1273
  /**
1274
   * Remove comments in the dom.
1275
   *
1276
   * @param HtmlDomParser $dom
1277
   *
1278
   * @return HtmlDomParser
1279
   */
1280 26
  private function removeComments(HtmlDomParser $dom): HtmlDomParser
1281
  {
1282 26
    foreach ($dom->find('//comment()') as $commentWrapper) {
0 ignored issues
show
Bug introduced by
The expression $dom->find('//comment()') of type array<integer,object<vok...leHtmlDomNodeInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1283 3
      $comment = $commentWrapper->getNode();
1284 3
      $val = $comment->nodeValue;
1285 3
      if (\strpos($val, '[') === false) {
1286 3
        $comment->parentNode->removeChild($comment);
1287
      }
1288
    }
1289
1290 26
    $dom->getDocument()->normalizeDocument();
1291
1292 26
    return $dom;
1293
  }
1294
1295
  /**
1296
   * Trim tags in the dom.
1297
   *
1298
   * @param SimpleHtmlDom $element
1299
   *
1300
   * @return void
1301
   */
1302 3
  private function removeWhitespaceAroundTags(SimpleHtmlDom $element)
1303
  {
1304 3
    if (isset(self::$trimWhitespaceFromTags[$element->tag])) {
1305 1
      $node = $element->getNode();
1306
1307 1
      $candidates = [];
1308 1
      if ($node->childNodes->length > 0) {
1309 1
        $candidates[] = $node->firstChild;
1310 1
        $candidates[] = $node->lastChild;
1311 1
        $candidates[] = $node->previousSibling;
1312 1
        $candidates[] = $node->nextSibling;
1313
      }
1314
1315 1
      foreach ($candidates as &$candidate) {
1316 1
        if ($candidate === null) {
1317
          continue;
1318
        }
1319
1320 1
        if ($candidate->nodeType === 3) {
1321 1
          $candidate->nodeValue = \preg_replace(self::$regExSpace, ' ', $candidate->nodeValue);
1322
        }
1323
      }
1324
    }
1325 3
  }
1326
1327
  /**
1328
   * Callback function for preg_replace_callback use.
1329
   *
1330
   * @param array $matches PREG matches
1331
   *
1332
   * @return string
1333
   */
1334 2
  private function restoreProtectedHtml($matches): string
1335
  {
1336 2
    \preg_match('/.*"(?<id>\d*)"/', $matches['attributes'], $matchesInner);
1337
1338 2
    $html = '';
1339 2
    if (isset($this->protectedChildNodes[$matchesInner['id']])) {
1340 2
      $html .= $this->protectedChildNodes[$matchesInner['id']];
1341
    }
1342
1343 2
    return $html;
1344
  }
1345
1346
  /**
1347
   * @param array $domainsToRemoveHttpPrefixFromAttributes
1348
   *
1349
   * @return $this
1350
   */
1351 2
  public function setDomainsToRemoveHttpPrefixFromAttributes($domainsToRemoveHttpPrefixFromAttributes)
1352
  {
1353 2
    $this->domainsToRemoveHttpPrefixFromAttributes = $domainsToRemoveHttpPrefixFromAttributes;
1354
1355 2
    return $this;
1356
  }
1357
1358
  /**
1359
   * @param $attrName
1360
   * @param $attrValue
1361
   *
1362
   * @return string
1363
   */
1364 15
  private function sortCssClassNames($attrName, $attrValue): string
1365
  {
1366 15
    if ($attrName !== 'class' || !$attrValue) {
1367 13
      return $attrValue;
1368
    }
1369
1370 9
    $classes = \array_unique(
1371 9
        \explode(' ', $attrValue)
1372
    );
1373 9
    \sort($classes);
1374
1375 9
    $attrValue = '';
1376 9
    foreach ($classes as $class) {
1377
1378 9
      if (!$class) {
1379 2
        continue;
1380
      }
1381
1382 9
      $attrValue .= \trim($class) . ' ';
1383
    }
1384 9
    $attrValue = \trim($attrValue);
1385
1386 9
    return $attrValue;
1387
  }
1388
1389
  /**
1390
   * Sum-up extra whitespace from dom-nodes.
1391
   *
1392
   * @param HtmlDomParser $dom
1393
   *
1394
   * @return HtmlDomParser
1395
   */
1396 26
  private function sumUpWhitespace(HtmlDomParser $dom): HtmlDomParser
1397
  {
1398 26
    $textnodes = $dom->find('//text()');
1399 26
    foreach ($textnodes as $textnodeWrapper) {
0 ignored issues
show
Bug introduced by
The expression $textnodes of type array<integer,object<vok...leHtmlDomNodeInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1400
      /* @var $textnode \DOMNode */
1401 22
      $textnode = $textnodeWrapper->getNode();
1402 22
      $xp = $textnode->getNodePath();
1403
1404 22
      $doSkip = false;
1405 22
      foreach (self::$skipTagsForRemoveWhitespace as $pattern) {
1406 22
        if (\strpos($xp, "/$pattern") !== false) {
1407 3
          $doSkip = true;
1408 22
          break;
1409
        }
1410
      }
1411 22
      if ($doSkip) {
1412 3
        continue;
1413
      }
1414
1415 22
      $textnode->nodeValue = \preg_replace(self::$regExSpace, ' ', $textnode->nodeValue);
1416
    }
1417
1418 26
    $dom->getDocument()->normalizeDocument();
1419
1420 26
    return $dom;
1421
  }
1422
}
1423