Completed
Push — master ( bc80ed...247e13 )
by Lars
06:22
created

HtmlMin::doRemoveDefaultAttributes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
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 17
        $attrstr .= $attribute->name;
487
488
        if (
489 17
            $this->doOptimizeAttributes === true
490
            &&
491 17
            isset(self::$booleanAttributes[$attribute->name])
492
        ) {
493 6
          $attrstr .= ' ';
494 6
          continue;
495
        }
496
497 17
        $attrstr .= '=';
498
499
        # http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#attributes-0
500 17
        $omitquotes = $this->doRemoveOmittedQuotes
501
                      &&
502 17
                      $attribute->value != ''
503
                      &&
504 17
                      0 == \preg_match('/["\'=<>` \t\r\n\f]+/', $attribute->value);
505
506 17
        $attr_val = $attribute->value;
507 17
        $attrstr .= ($omitquotes ? '' : '"') . $attr_val . ($omitquotes ? '' : '"');
508 17
        $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 24
               $tag_name == 'li'
561
               &&
562
               (
563 5
                   $nextSibling === null
564
                   ||
565
                   (
566 3
                       $nextSibling instanceof \DOMElement
567
                       &&
568 24
                       $nextSibling->tagName == 'li'
569
                   )
570
               )
571
           )
572
           ||
573
           (
574
               (
575 24
                   $tag_name == 'rp'
576
               )
577
               &&
578
               (
579
                   $nextSibling === null
580
                   ||
581
                   (
582
                       $nextSibling instanceof \DOMElement
583
                       &&
584
                       (
585
                           $nextSibling->tagName == 'rp'
586
                           ||
587 24
                           $nextSibling->tagName == 'rt'
588
                       )
589
                   )
590
               )
591
           )
592
           ||
593
           (
594 24
               $tag_name == 'tr'
595
               &&
596
               (
597 1
                   $nextSibling === null
598
                   ||
599
                   (
600 1
                       $nextSibling instanceof \DOMElement
601
                       &&
602 24
                       $nextSibling->tagName == 'tr'
603
                   )
604
               )
605
           )
606
           ||
607
           (
608
               (
609 24
                   $tag_name == 'td'
610
                   ||
611 24
                   $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 24
                           $nextSibling->tagName == 'th'
624
                       )
625
                   )
626
               )
627
           )
628
           ||
629
           (
630
               (
631 24
                   $tag_name == 'dd'
632
                   ||
633 24
                   $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 24
                           $nextSibling->tagName == 'dt'
650
                       )
651
                   )
652
               )
653
           )
654
           ||
655
           (
656 24
               $tag_name == 'option'
657
               &&
658
               (
659
                   $nextSibling === null
660
                   ||
661
                   (
662
                       $nextSibling instanceof \DOMElement
663
                       &&
664
                       (
665
                           $nextSibling->tagName == 'option'
666
                           ||
667 24
                           $nextSibling->tagName == 'optgroup'
668
                       )
669
                   )
670
               )
671
           )
672
           ||
673
           (
674 24
               $tag_name == 'p'
675
               &&
676
               (
677
                   (
678 9
                       $nextSibling === null
679
                       &&
680
                       (
681 9
                           $node->parentNode !== null
682
                           &&
683
                           !\in_array(
684 9
                               $node->parentNode->nodeName,
685
                               [
686
                                   'a',
687
                                   'audio',
688
                                   'del',
689
                                   'ins',
690
                                   'map',
691
                                   'noscript',
692
                                   'video',
693
                               ],
694
                               true
695
                           )
696
                       )
697
                   )
698
                   ||
699
                   (
700 5
                       $nextSibling instanceof \DOMElement
701
                       &&
702
                       \in_array(
703 26
                           $nextSibling->tagName,
704
                           [
705
                               '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
                           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 22
          $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
            if (
797 17
                $emptyStringTmp !== 'last_was_empty'
798
                &&
799 17
                substr($html, -1) !== ' '
800
            ) {
801 17
              $html .= ' ';
802
            }
803 27
            $emptyStringTmp = 'is_empty';
804
          }
805
        }
806
807 23
      } elseif ($child instanceof \DOMText) {
808
809 23
        if ($child->isElementContentWhitespace()) {
810
          if (
811 19
              $child->previousSibling !== null
812
              &&
813 19
              $child->nextSibling !== null
814
          ) {
815
            if (
816 12
                $emptyStringTmp !== 'last_was_empty'
817
                &&
818 12
                substr($html, -1) !== ' '
819
            ) {
820 3
              $html .= ' ';
821
            }
822 19
            $emptyStringTmp = 'is_empty';
823
          }
824
825
        } else {
826
827 23
          $html .= $child->wholeText;
828
829
        }
830
831
      } elseif ($child instanceof \DOMComment) {
832
833 27
        $html .= $child->wholeText;
834
835
      }
836
    }
837
838 27
    return $html;
839
  }
840
841
  /**
842
   * @param \DOMNode $node
843
   *
844
   * @return \DOMNode|null
845
   */
846 26
  protected function getNextSiblingOfTypeDOMElement(\DOMNode $node)
847
  {
848
    do {
849 26
      $node = $node->nextSibling;
850 26
    } while (!($node === null || $node instanceof \DOMElement));
851
852 26
    return $node;
853
  }
854
855
  /**
856
   * Check if the current string is an conditional comment.
857
   *
858
   * INFO: since IE >= 10 conditional comment are not working anymore
859
   *
860
   * <!--[if expression]> HTML <![endif]-->
861
   * <![if expression]> HTML <![endif]>
862
   *
863
   * @param string $comment
864
   *
865
   * @return bool
866
   */
867 3
  private function isConditionalComment($comment): bool
868
  {
869 3
    if (preg_match('/^\[if [^\]]+\]/', $comment)) {
870 2
      return true;
871
    }
872
873 3
    if (preg_match('/\[endif\]$/', $comment)) {
874 1
      return true;
875
    }
876
877 3
    return false;
878
  }
879
880
  /**
881
   * @param string $html
882
   * @param bool   $decodeUtf8Specials <p>Use this only in special cases, e.g. for PHP 5.3</p>
883
   *
884
   * @return string
885
   */
886 31
  public function minify($html, $decodeUtf8Specials = false): string
887
  {
888 31
    $html = (string)$html;
889 31
    if (!isset($html[0])) {
890 1
      return '';
891
    }
892
893 31
    $html = \trim($html);
894 31
    if (!$html) {
895 3
      return '';
896
    }
897
898
    // init
899 28
    static $CACHE_SELF_CLOSING_TAGS = null;
900 28
    if ($CACHE_SELF_CLOSING_TAGS === null) {
901 1
      $CACHE_SELF_CLOSING_TAGS = \implode('|', self::$selfClosingTags);
902
    }
903
904
    // reset
905 28
    $this->protectedChildNodes = [];
906
907
    // save old content
908 28
    $origHtml = $html;
909 28
    $origHtmlLength = \strlen($html);
910
911
    // -------------------------------------------------------------------------
912
    // Minify the HTML via "HtmlDomParser"
913
    // -------------------------------------------------------------------------
914
915 28
    if ($this->doOptimizeViaHtmlDomParser === true) {
916 27
      $html = $this->minifyHtmlDom($html, $decodeUtf8Specials);
917
    }
918
919
    // -------------------------------------------------------------------------
920
    // Trim whitespace from html-string. [protected html is still protected]
921
    // -------------------------------------------------------------------------
922
923
    // Remove extra white-space(s) between HTML attribute(s)
924 28
    $html = (string)\preg_replace_callback(
925 28
        '#<([^\/\s<>!]+)(?:\s+([^<>]*?)\s*|\s*)(\/?)>#',
926 28
        function ($matches) {
927 28
          return '<' . $matches[1] . (string)\preg_replace('#([^\s=]+)(\=([\'"]?)(.*?)\3)?(\s+|$)#s', ' $1$2', $matches[2]) . $matches[3] . '>';
928 28
        },
929 28
        $html
930
    );
931
932 28
    if ($this->doRemoveSpacesBetweenTags === true) {
933
      // Remove spaces that are between > and <
934
      $html = (string)\preg_replace('/(>) (<)/', '>$2', $html);
935
    }
936
937
    // -------------------------------------------------------------------------
938
    // Restore protected HTML-code.
939
    // -------------------------------------------------------------------------
940
941 28
    $html = (string)\preg_replace_callback(
942 28
        '/<(?<element>' . $this->protectedChildNodesHelper . ')(?<attributes> [^>]*)?>(?<value>.*?)<\/' . $this->protectedChildNodesHelper . '>/',
943 28
        [$this, 'restoreProtectedHtml'],
944 28
        $html
945
    );
946
947
    // -------------------------------------------------------------------------
948
    // Restore protected HTML-entities.
949
    // -------------------------------------------------------------------------
950
951 28
    if ($this->doOptimizeViaHtmlDomParser === true) {
952 27
      $html = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($html);
953
    }
954
955
    // ------------------------------------
956
    // Final clean-up
957
    // ------------------------------------
958
959 28
    $html = \str_replace(
960
        [
961 28
            'html>' . "\n",
962
            "\n" . '<html',
963
            'html/>' . "\n",
964
            "\n" . '</html',
965
            'head>' . "\n",
966
            "\n" . '<head',
967
            'head/>' . "\n",
968
            "\n" . '</head',
969
        ],
970
        [
971 28
            'html>',
972
            '<html',
973
            'html/>',
974
            '</html',
975
            'head>',
976
            '<head',
977
            'head/>',
978
            '</head',
979
        ],
980 28
        $html
981
    );
982
983
    // self closing tags, don't need a trailing slash ...
984 28
    $replace = [];
985 28
    $replacement = [];
986 28
    foreach (self::$selfClosingTags as $selfClosingTag) {
987 28
      $replace[] = '<' . $selfClosingTag . '/>';
988 28
      $replacement[] = '<' . $selfClosingTag . '>';
989 28
      $replace[] = '<' . $selfClosingTag . ' />';
990 28
      $replacement[] = '<' . $selfClosingTag . '>';
991
    }
992 28
    $html = \str_replace(
993 28
        $replace,
994 28
        $replacement,
995 28
        $html
996
    );
997
998 28
    $html = (string)\preg_replace('#<\b(' . $CACHE_SELF_CLOSING_TAGS . ')([^>]+)><\/\b\1>#', '<\\1\\2>', $html);
999
1000
    // ------------------------------------
1001
    // check if compression worked
1002
    // ------------------------------------
1003
1004 28
    if ($origHtmlLength < \strlen($html)) {
1005 2
      $html = $origHtml;
1006
    }
1007
1008 28
    return $html;
1009
  }
1010
1011
  /**
1012
   * @param $html
1013
   * @param $decodeUtf8Specials
1014
   *
1015
   * @return string
1016
   */
1017 27
  private function minifyHtmlDom($html, $decodeUtf8Specials): string
1018
  {
1019
    // init dom
1020 27
    $dom = new HtmlDomParser();
1021 27
    $dom->getDocument()->preserveWhiteSpace = false; // remove redundant white space
1022 27
    $dom->getDocument()->formatOutput = false; // do not formats output with indentation
1023
1024
    // load dom
1025 27
    $dom->loadHtml($html);
1026
1027 27
    $this->withDocType = (\stripos(\ltrim($html), '<!DOCTYPE') === 0);
1028
1029
    // -------------------------------------------------------------------------
1030
    // Protect HTML tags and conditional comments.
1031
    // -------------------------------------------------------------------------
1032
1033 27
    $dom = $this->protectTags($dom);
1034
1035
    // -------------------------------------------------------------------------
1036
    // Remove default HTML comments. [protected html is still protected]
1037
    // -------------------------------------------------------------------------
1038
1039 27
    if ($this->doRemoveComments === true) {
1040 26
      $dom = $this->removeComments($dom);
1041
    }
1042
1043
    // -------------------------------------------------------------------------
1044
    // Sum-Up extra whitespace from the Dom. [protected html is still protected]
1045
    // -------------------------------------------------------------------------
1046
1047 27
    if ($this->doSumUpWhitespace === true) {
1048 26
      $dom = $this->sumUpWhitespace($dom);
1049
    }
1050
1051 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...
1052
1053
      // -------------------------------------------------------------------------
1054
      // Optimize html attributes. [protected html is still protected]
1055
      // -------------------------------------------------------------------------
1056
1057 27
      if ($this->doOptimizeAttributes === true) {
1058 26
        $this->optimizeAttributes($element);
1059
      }
1060
1061
      // -------------------------------------------------------------------------
1062
      // Remove whitespace around tags. [protected html is still protected]
1063
      // -------------------------------------------------------------------------
1064
1065 27
      if ($this->doRemoveWhitespaceAroundTags === true) {
1066 27
        $this->removeWhitespaceAroundTags($element);
1067
      }
1068
    }
1069
1070
    // -------------------------------------------------------------------------
1071
    // Convert the Dom into a string.
1072
    // -------------------------------------------------------------------------
1073
1074 27
    $html = $dom->fixHtmlOutput(
1075 27
        $this->domNodeToString($dom->getDocument()),
1076 27
        $decodeUtf8Specials
1077
    );
1078
1079 27
    return $html;
1080
  }
1081
1082
  /**
1083
   * Sort HTML-Attributes, so that gzip can do better work and remove some default attributes...
1084
   *
1085
   * @param SimpleHtmlDom $element
1086
   *
1087
   * @return bool
1088
   */
1089 26
  private function optimizeAttributes(SimpleHtmlDom $element): bool
1090
  {
1091 26
    $attributes = $element->getAllAttributes();
1092 26
    if ($attributes === null) {
1093 26
      return false;
1094
    }
1095
1096 16
    $attrs = [];
1097 16
    foreach ((array)$attributes as $attrName => $attrValue) {
1098
1099
      // -------------------------------------------------------------------------
1100
      // Remove optional "http:"-prefix from attributes.
1101
      // -------------------------------------------------------------------------
1102
1103 16
      if ($this->doRemoveHttpPrefixFromAttributes === true) {
1104
        if (
1105 3
            ($attrName === 'href' || $attrName === 'src' || $attrName === 'action')
1106
            &&
1107 3
            !(isset($attributes['rel']) && $attributes['rel'] === 'external')
1108
            &&
1109 3
            !(isset($attributes['target']) && $attributes['target'] === '_blank')
1110
        ) {
1111 2
          $attrValue = \str_replace('http://', '//', $attrValue);
1112
        }
1113
      }
1114
1115 16
      if ($this->removeAttributeHelper($element->tag, $attrName, $attrValue, $attributes)) {
1116 4
        $element->{$attrName} = null;
1117 4
        continue;
1118
      }
1119
1120
      // -------------------------------------------------------------------------
1121
      // Sort css-class-names, for better gzip results.
1122
      // -------------------------------------------------------------------------
1123
1124 16
      if ($this->doSortCssClassNames === true) {
1125 16
        $attrValue = $this->sortCssClassNames($attrName, $attrValue);
1126
      }
1127
1128 16
      if ($this->doSortHtmlAttributes === true) {
1129 16
        $attrs[$attrName] = $attrValue;
1130 16
        $element->{$attrName} = null;
1131
      }
1132
    }
1133
1134
    // -------------------------------------------------------------------------
1135
    // Sort html-attributes, for better gzip results.
1136
    // -------------------------------------------------------------------------
1137
1138 16
    if ($this->doSortHtmlAttributes === true) {
1139 16
      \ksort($attrs);
1140 16
      foreach ($attrs as $attrName => $attrValue) {
1141 16
        $attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue);
1142 16
        $element->setAttribute($attrName, $attrValue, true);
1143
      }
1144
    }
1145
1146 16
    return true;
1147
  }
1148
1149
  /**
1150
   * Prevent changes of inline "styles" and "scripts".
1151
   *
1152
   * @param HtmlDomParser $dom
1153
   *
1154
   * @return HtmlDomParser
1155
   */
1156 27
  private function protectTags(HtmlDomParser $dom): HtmlDomParser
1157
  {
1158
    // init
1159 27
    $counter = 0;
1160
1161 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...
1162
1163
      // skip external links
1164 3
      if ($element->tag === 'script' || $element->tag === 'style') {
1165 3
        $attributes = $element->getAllAttributes();
1166 3
        if (isset($attributes['src'])) {
1167 2
          continue;
1168
        }
1169
      }
1170
1171 2
      $this->protectedChildNodes[$counter] = $element->text();
1172 2
      $element->getNode()->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $counter . '"></' . $this->protectedChildNodesHelper . '>';
1173
1174 2
      ++$counter;
1175
    }
1176
1177 27
    $dom->getDocument()->normalizeDocument();
1178
1179 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...
1180 3
      $text = $element->text();
1181
1182
      // skip normal comments
1183 3
      if ($this->isConditionalComment($text) === false) {
1184 3
        continue;
1185
      }
1186
1187 2
      $this->protectedChildNodes[$counter] = '<!--' . $text . '-->';
1188
1189
      /* @var $node \DOMComment */
1190 2
      $node = $element->getNode();
1191 2
      $child = new \DOMText('<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $counter . '"></' . $this->protectedChildNodesHelper . '>');
1192 2
      $element->getNode()->parentNode->replaceChild($child, $node);
1193
1194 2
      ++$counter;
1195
    }
1196
1197 27
    $dom->getDocument()->normalizeDocument();
1198
1199 27
    return $dom;
1200
  }
1201
1202
  /**
1203
   * Check if the attribute can be removed.
1204
   *
1205
   * @param string $tag
1206
   * @param string $attrName
1207
   * @param string $attrValue
1208
   * @param array  $allAttr
1209
   *
1210
   * @return bool
1211
   */
1212 16
  private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr): bool
1213
  {
1214
    // remove defaults
1215 16
    if ($this->doRemoveDefaultAttributes === true) {
1216
1217 1
      if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') {
1218
        return true;
1219
      }
1220
1221 1
      if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') {
1222
        return true;
1223
      }
1224
1225 1
      if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') {
1226
        return true;
1227
      }
1228
1229 1
      if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') {
1230
        return true;
1231
      }
1232
    }
1233
1234
    // remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway)
1235 16 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...
1236 16
      if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) {
1237
        return true;
1238
      }
1239
    }
1240
1241
    // remove deprecated anchor-jump
1242 16 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...
1243 16
      if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) {
1244
        return true;
1245
      }
1246
    }
1247
1248
    // remove "type=text/css" for css links
1249 16 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...
1250 16
      if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet') {
1251 1
        return true;
1252
      }
1253
    }
1254
1255
    // remove deprecated script-mime-types
1256 16 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...
1257 16
      if ($tag === 'script' && $attrName === 'type' && isset($allAttr['src'], self::$executableScriptsMimeTypes[$attrValue])) {
1258 1
        return true;
1259
      }
1260
    }
1261
1262
    // remove 'value=""' from <input type="text">
1263 16
    if ($this->doRemoveValueFromEmptyInput === true) {
1264 16
      if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') {
1265 1
        return true;
1266
      }
1267
    }
1268
1269
    // remove some empty attributes
1270 16
    if ($this->doRemoveEmptyAttributes === true) {
1271 16
      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)) {
1272 3
        return true;
1273
      }
1274
    }
1275
1276 16
    return false;
1277
  }
1278
1279
  /**
1280
   * Remove comments in the dom.
1281
   *
1282
   * @param HtmlDomParser $dom
1283
   *
1284
   * @return HtmlDomParser
1285
   */
1286 26
  private function removeComments(HtmlDomParser $dom): HtmlDomParser
1287
  {
1288 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...
1289 3
      $comment = $commentWrapper->getNode();
1290 3
      $val = $comment->nodeValue;
1291 3
      if (\strpos($val, '[') === false) {
1292 3
        $comment->parentNode->removeChild($comment);
1293
      }
1294
    }
1295
1296 26
    $dom->getDocument()->normalizeDocument();
1297
1298 26
    return $dom;
1299
  }
1300
1301
  /**
1302
   * Trim tags in the dom.
1303
   *
1304
   * @param SimpleHtmlDom $element
1305
   *
1306
   * @return void
1307
   */
1308 3
  private function removeWhitespaceAroundTags(SimpleHtmlDom $element)
1309
  {
1310 3
    if (isset(self::$trimWhitespaceFromTags[$element->tag])) {
1311 1
      $node = $element->getNode();
1312
1313 1
      $candidates = [];
1314 1
      if ($node->childNodes->length > 0) {
1315 1
        $candidates[] = $node->firstChild;
1316 1
        $candidates[] = $node->lastChild;
1317 1
        $candidates[] = $node->previousSibling;
1318 1
        $candidates[] = $node->nextSibling;
1319
      }
1320
1321 1
      foreach ($candidates as &$candidate) {
1322 1
        if ($candidate === null) {
1323
          continue;
1324
        }
1325
1326 1
        if ($candidate->nodeType === 3) {
1327 1
          $candidate->nodeValue = \preg_replace(self::$regExSpace, ' ', $candidate->nodeValue);
1328
        }
1329
      }
1330
    }
1331 3
  }
1332
1333
  /**
1334
   * Callback function for preg_replace_callback use.
1335
   *
1336
   * @param array $matches PREG matches
1337
   *
1338
   * @return string
1339
   */
1340 2
  private function restoreProtectedHtml($matches): string
1341
  {
1342 2
    \preg_match('/.*"(?<id>\d*)"/', $matches['attributes'], $matchesInner);
1343
1344 2
    $html = '';
1345 2
    if (isset($this->protectedChildNodes[$matchesInner['id']])) {
1346 2
      $html .= $this->protectedChildNodes[$matchesInner['id']];
1347
    }
1348
1349 2
    return $html;
1350
  }
1351
1352
  /**
1353
   * @param array $domainsToRemoveHttpPrefixFromAttributes
1354
   *
1355
   * @return $this
1356
   */
1357 2
  public function setDomainsToRemoveHttpPrefixFromAttributes($domainsToRemoveHttpPrefixFromAttributes)
1358
  {
1359 2
    $this->domainsToRemoveHttpPrefixFromAttributes = $domainsToRemoveHttpPrefixFromAttributes;
1360
1361 2
    return $this;
1362
  }
1363
1364
  /**
1365
   * @param $attrName
1366
   * @param $attrValue
1367
   *
1368
   * @return string
1369
   */
1370 16
  private function sortCssClassNames($attrName, $attrValue): string
1371
  {
1372 16
    if ($attrName !== 'class' || !$attrValue) {
1373 14
      return $attrValue;
1374
    }
1375
1376 10
    $classes = \array_unique(
1377 10
        \explode(' ', $attrValue)
1378
    );
1379 10
    \sort($classes);
1380
1381 10
    $attrValue = '';
1382 10
    foreach ($classes as $class) {
1383
1384 10
      if (!$class) {
1385 2
        continue;
1386
      }
1387
1388 10
      $attrValue .= \trim($class) . ' ';
1389
    }
1390 10
    $attrValue = \trim($attrValue);
1391
1392 10
    return $attrValue;
1393
  }
1394
1395
  /**
1396
   * Sum-up extra whitespace from dom-nodes.
1397
   *
1398
   * @param HtmlDomParser $dom
1399
   *
1400
   * @return HtmlDomParser
1401
   */
1402 26
  private function sumUpWhitespace(HtmlDomParser $dom): HtmlDomParser
1403
  {
1404 26
    $textnodes = $dom->find('//text()');
1405 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...
1406
      /* @var $textnode \DOMNode */
1407 22
      $textnode = $textnodeWrapper->getNode();
1408 22
      $xp = $textnode->getNodePath();
1409
1410 22
      $doSkip = false;
1411 22
      foreach (self::$skipTagsForRemoveWhitespace as $pattern) {
1412 22
        if (\strpos($xp, "/$pattern") !== false) {
1413 3
          $doSkip = true;
1414 22
          break;
1415
        }
1416
      }
1417 22
      if ($doSkip) {
1418 3
        continue;
1419
      }
1420
1421 22
      $textnode->nodeValue = \preg_replace(self::$regExSpace, ' ', $textnode->nodeValue);
1422
    }
1423
1424 26
    $dom->getDocument()->normalizeDocument();
1425
1426 26
    return $dom;
1427
  }
1428
}
1429