Completed
Push — master ( c7ad42...bd1bbe )
by Lars
02:11
created

HtmlMin::doRemoveDeprecatedAnchorName()   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 32
  public function __construct()
261
  {
262 32
  }
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 28
  private function domNodeAttributesToString(\DOMNode $node): string
481
  {
482
    # Remove quotes around attribute values, when allowed (<p class="foo"> → <p class=foo>)
483 28
    $attrstr = '';
484 28
    if ($node->attributes != null) {
485 28
      foreach ($node->attributes as $attribute) {
486 18
        $attrstr .= $attribute->name;
487
488
        if (
489 18
            $this->doOptimizeAttributes === true
490
            &&
491 18
            isset(self::$booleanAttributes[$attribute->name])
492
        ) {
493 6
          $attrstr .= ' ';
494 6
          continue;
495
        }
496
497 18
        $attrstr .= '=';
498
499
        # http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#attributes-0
500 18
        $omitquotes = $this->doRemoveOmittedQuotes
501
                      &&
502 18
                      $attribute->value != ''
503
                      &&
504 18
                      0 == \preg_match('/["\'=<>` \t\r\n\f]+/', $attribute->value);
505
506 18
        $attr_val = $attribute->value;
507 18
        $attrstr .= ($omitquotes ? '' : '"') . $attr_val . ($omitquotes ? '' : '"');
508 18
        $attrstr .= ' ';
509
      }
510
    }
511
512 28
    return \trim($attrstr);
513
  }
514
515
  /**
516
   * @param \DOMNode $node
517
   *
518
   * @return bool
519
   */
520 27
  private function domNodeClosingTagOptional(\DOMNode $node): bool
521
  {
522 27
    $tag_name = $node->nodeName;
523 27
    $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 27
    return \in_array($tag_name, self::$optional_end_tags, true)
558
           ||
559
           (
560 25
               $tag_name == 'li'
561
               &&
562
               (
563 5
                   $nextSibling === null
564
                   ||
565
                   (
566 3
                       $nextSibling instanceof \DOMElement
567
                       &&
568 25
                       $nextSibling->tagName == 'li'
569
                   )
570
               )
571
           )
572
           ||
573
           (
574
               (
575 25
                   $tag_name == 'rp'
576
               )
577
               &&
578
               (
579
                   $nextSibling === null
580
                   ||
581
                   (
582
                       $nextSibling instanceof \DOMElement
583
                       &&
584
                       (
585
                           $nextSibling->tagName == 'rp'
586
                           ||
587 25
                           $nextSibling->tagName == 'rt'
588
                       )
589
                   )
590
               )
591
           )
592
           ||
593
           (
594 25
               $tag_name == 'tr'
595
               &&
596
               (
597 1
                   $nextSibling === null
598
                   ||
599
                   (
600 1
                       $nextSibling instanceof \DOMElement
601
                       &&
602 25
                       $nextSibling->tagName == 'tr'
603
                   )
604
               )
605
           )
606
           ||
607
           (
608
               (
609 25
                   $tag_name == 'td'
610
                   ||
611 25
                   $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 25
                           $nextSibling->tagName == 'th'
624
                       )
625
                   )
626
               )
627
           )
628
           ||
629
           (
630
               (
631 25
                   $tag_name == 'dd'
632
                   ||
633 25
                   $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 25
                           $nextSibling->tagName == 'dt'
650
                       )
651
                   )
652
               )
653
           )
654
           ||
655
           (
656 25
               $tag_name == 'option'
657
               &&
658
               (
659
                   $nextSibling === null
660
                   ||
661
                   (
662
                       $nextSibling instanceof \DOMElement
663
                       &&
664
                       (
665
                           $nextSibling->tagName == 'option'
666
                           ||
667 25
                           $nextSibling->tagName == 'optgroup'
668
                       )
669
                   )
670
               )
671
           )
672
           ||
673
           (
674 25
               $tag_name == 'p'
675
               &&
676
               (
677
                   (
678 10
                       $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 6
                       $nextSibling instanceof \DOMElement
701
                       &&
702
                       \in_array(
703 27
                           $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 28
  protected function domNodeToString(\DOMNode $node): string
741
  {
742
    // init
743 28
    $html = '';
744 28
    $emptyStringTmp = '';
745
746 28
    foreach ($node->childNodes as $child) {
747
748 28
      if ($emptyStringTmp === 'is_empty') {
749 18
        $emptyStringTmp = 'last_was_empty';
750
      } else {
751 28
        $emptyStringTmp = '';
752
      }
753
754 28
      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 28
      } elseif ($child instanceof \DOMElement) {
778
779 28
        $html .= \rtrim('<' . $child->tagName . ' ' . $this->domNodeAttributesToString($child));
780 28
        $html .= '>' . $this->domNodeToString($child);
781
782
        if (
783 28
            $this->doRemoveOmittedHtmlTags === false
784
            ||
785 28
            !$this->domNodeClosingTagOptional($child)
786
        ) {
787 23
          $html .= '</' . $child->tagName . '>';
788
        }
789
790 28
        if ($this->doRemoveWhitespaceAroundTags === false) {
791
          if (
792 27
              $child->nextSibling instanceof \DOMText
793
              &&
794 27
              $child->nextSibling->wholeText === ' '
795
          ) {
796
            if (
797 17
                $emptyStringTmp !== 'last_was_empty'
798
                &&
799 17
                substr($html, -1) !== ' '
800
            ) {
801 17
              $html .= ' ';
802
            }
803 28
            $emptyStringTmp = 'is_empty';
804
          }
805
        }
806
807 24
      } elseif ($child instanceof \DOMText) {
808
809 24
        if ($child->isElementContentWhitespace()) {
810
          if (
811 19
              $child->previousSibling !== null
812
              &&
813 19
              $child->nextSibling !== null
814
          ) {
815
            if (
816 13
                $emptyStringTmp !== 'last_was_empty'
817
                &&
818 13
                substr($html, -1) !== ' '
819
            ) {
820 4
              $html .= ' ';
821
            }
822 19
            $emptyStringTmp = 'is_empty';
823
          }
824
825
        } else {
826
827 24
          $html .= $child->wholeText;
828
829
        }
830
831
      } elseif ($child instanceof \DOMComment) {
832
833 28
        $html .= $child->wholeText;
834
835
      }
836
    }
837
838 28
    return $html;
839
  }
840
841
  /**
842
   * @param \DOMNode $node
843
   *
844
   * @return \DOMNode|null
845
   */
846 27
  protected function getNextSiblingOfTypeDOMElement(\DOMNode $node)
847
  {
848
    do {
849 27
      $node = $node->nextSibling;
850 27
    } while (!($node === null || $node instanceof \DOMElement));
851
852 27
    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 32
  public function minify($html, $decodeUtf8Specials = false): string
887
  {
888 32
    $html = (string)$html;
889 32
    if (!isset($html[0])) {
890 1
      return '';
891
    }
892
893 32
    $html = \trim($html);
894 32
    if (!$html) {
895 3
      return '';
896
    }
897
898
    // init
899 29
    static $CACHE_SELF_CLOSING_TAGS = null;
900 29
    if ($CACHE_SELF_CLOSING_TAGS === null) {
901 1
      $CACHE_SELF_CLOSING_TAGS = \implode('|', self::$selfClosingTags);
902
    }
903
904
    // reset
905 29
    $this->protectedChildNodes = [];
906
907
    // save old content
908 29
    $origHtml = $html;
909 29
    $origHtmlLength = \strlen($html);
910
911
    // -------------------------------------------------------------------------
912
    // Minify the HTML via "HtmlDomParser"
913
    // -------------------------------------------------------------------------
914
915 29
    if ($this->doOptimizeViaHtmlDomParser === true) {
916 28
      $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 29
    $html = (string)\preg_replace_callback(
925 29
        '#<([^\/\s<>!]+)(?:\s+([^<>]*?)\s*|\s*)(\/?)>#',
926 29
        function ($matches) {
927 29
          return '<' . $matches[1] . (string)\preg_replace('#([^\s=]+)(\=([\'"]?)(.*?)\3)?(\s+|$)#s', ' $1$2', $matches[2]) . $matches[3] . '>';
928 29
        },
929 29
        $html
930
    );
931
932 29
    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 29
    $html = (string)\preg_replace_callback(
942 29
        '/<(?<element>' . $this->protectedChildNodesHelper . ')(?<attributes> [^>]*)?>(?<value>.*?)<\/' . $this->protectedChildNodesHelper . '>/',
943 29
        [$this, 'restoreProtectedHtml'],
944 29
        $html
945
    );
946
947
    // -------------------------------------------------------------------------
948
    // Restore protected HTML-entities.
949
    // -------------------------------------------------------------------------
950
951 29
    if ($this->doOptimizeViaHtmlDomParser === true) {
952 28
      $html = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($html);
953
    }
954
955
    // ------------------------------------
956
    // Final clean-up
957
    // ------------------------------------
958
959 29
    $html = \str_replace(
960
        [
961 29
            '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 29
            'html>',
972
            '<html',
973
            'html/>',
974
            '</html',
975
            'head>',
976
            '<head',
977
            'head/>',
978
            '</head',
979
        ],
980 29
        $html
981
    );
982
983
    // self closing tags, don't need a trailing slash ...
984 29
    $replace = [];
985 29
    $replacement = [];
986 29
    foreach (self::$selfClosingTags as $selfClosingTag) {
987 29
      $replace[] = '<' . $selfClosingTag . '/>';
988 29
      $replacement[] = '<' . $selfClosingTag . '>';
989 29
      $replace[] = '<' . $selfClosingTag . ' />';
990 29
      $replacement[] = '<' . $selfClosingTag . '>';
991
    }
992 29
    $html = \str_replace(
993 29
        $replace,
994 29
        $replacement,
995 29
        $html
996
    );
997
998 29
    $html = (string)\preg_replace('#<\b(' . $CACHE_SELF_CLOSING_TAGS . ')([^>]*+)><\/\b\1>#', '<\\1\\2>', $html);
999
1000
    // ------------------------------------
1001
    // check if compression worked
1002
    // ------------------------------------
1003
1004 29
    if ($origHtmlLength < \strlen($html)) {
1005 2
      $html = $origHtml;
1006
    }
1007
1008 29
    return $html;
1009
  }
1010
1011
  /**
1012
   * @param $html
1013
   * @param $decodeUtf8Specials
1014
   *
1015
   * @return string
1016
   */
1017 28
  private function minifyHtmlDom($html, $decodeUtf8Specials): string
1018
  {
1019
    // init dom
1020 28
    $dom = new HtmlDomParser();
1021 28
    $dom->getDocument()->preserveWhiteSpace = false; // remove redundant white space
1022 28
    $dom->getDocument()->formatOutput = false; // do not formats output with indentation
1023
1024
    // load dom
1025 28
    $dom->loadHtml($html);
1026
1027 28
    $this->withDocType = (\stripos(\ltrim($html), '<!DOCTYPE') === 0);
1028
1029
    // -------------------------------------------------------------------------
1030
    // Protect HTML tags and conditional comments.
1031
    // -------------------------------------------------------------------------
1032
1033 28
    $dom = $this->protectTags($dom);
1034
1035
    // -------------------------------------------------------------------------
1036
    // Remove default HTML comments. [protected html is still protected]
1037
    // -------------------------------------------------------------------------
1038
1039 28
    if ($this->doRemoveComments === true) {
1040 27
      $dom = $this->removeComments($dom);
1041
    }
1042
1043
    // -------------------------------------------------------------------------
1044
    // Sum-Up extra whitespace from the Dom. [protected html is still protected]
1045
    // -------------------------------------------------------------------------
1046
1047 28
    if ($this->doSumUpWhitespace === true) {
1048 27
      $dom = $this->sumUpWhitespace($dom);
1049
    }
1050
1051 28
    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 28
      if ($this->doOptimizeAttributes === true) {
1058 27
        $this->optimizeAttributes($element);
1059
      }
1060
1061
      // -------------------------------------------------------------------------
1062
      // Remove whitespace around tags. [protected html is still protected]
1063
      // -------------------------------------------------------------------------
1064
1065 28
      if ($this->doRemoveWhitespaceAroundTags === true) {
1066 28
        $this->removeWhitespaceAroundTags($element);
1067
      }
1068
    }
1069
1070
    // -------------------------------------------------------------------------
1071
    // Convert the Dom into a string.
1072
    // -------------------------------------------------------------------------
1073
1074 28
    $html = $dom->fixHtmlOutput(
1075 28
        $this->domNodeToString($dom->getDocument()),
1076 28
        $decodeUtf8Specials
1077
    );
1078
1079 28
    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 27
  private function optimizeAttributes(SimpleHtmlDom $element): bool
1090
  {
1091 27
    $attributes = $element->getAllAttributes();
1092 27
    if ($attributes === null) {
1093 27
      return false;
1094
    }
1095
1096 17
    $attrs = [];
1097 17
    foreach ((array)$attributes as $attrName => $attrValue) {
1098
1099
      // -------------------------------------------------------------------------
1100
      // Remove optional "http:"-prefix from attributes.
1101
      // -------------------------------------------------------------------------
1102
1103 17
      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 17
      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 17
      if ($this->doSortCssClassNames === true) {
1125 17
        $attrValue = $this->sortCssClassNames($attrName, $attrValue);
1126
      }
1127
1128 17
      if ($this->doSortHtmlAttributes === true) {
1129 17
        $attrs[$attrName] = $attrValue;
1130 17
        $element->{$attrName} = null;
1131
      }
1132
    }
1133
1134
    // -------------------------------------------------------------------------
1135
    // Sort html-attributes, for better gzip results.
1136
    // -------------------------------------------------------------------------
1137
1138 17
    if ($this->doSortHtmlAttributes === true) {
1139 17
      \ksort($attrs);
1140 17
      foreach ($attrs as $attrName => $attrValue) {
1141 17
        $attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue);
1142 17
        $element->setAttribute($attrName, $attrValue, true);
1143
      }
1144
    }
1145
1146 17
    return true;
1147
  }
1148
1149
  /**
1150
   * Prevent changes of inline "styles" and "scripts".
1151
   *
1152
   * @param HtmlDomParser $dom
1153
   *
1154
   * @return HtmlDomParser
1155
   */
1156 28
  private function protectTags(HtmlDomParser $dom): HtmlDomParser
1157
  {
1158
    // init
1159 28
    $counter = 0;
1160
1161 28
    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 28
    $dom->getDocument()->normalizeDocument();
1178
1179 28
    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 28
    $dom->getDocument()->normalizeDocument();
1198
1199 28
    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 17
  private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr): bool
1213
  {
1214
    // remove defaults
1215 17
    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 17 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 17
      if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) {
1237
        return true;
1238
      }
1239
    }
1240
1241
    // remove deprecated anchor-jump
1242 17 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 17
      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 17 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 17
      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 17 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 17
      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 17
    if ($this->doRemoveValueFromEmptyInput === true) {
1264 17
      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 17
    if ($this->doRemoveEmptyAttributes === true) {
1271 17
      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 17
    return false;
1277
  }
1278
1279
  /**
1280
   * Remove comments in the dom.
1281
   *
1282
   * @param HtmlDomParser $dom
1283
   *
1284
   * @return HtmlDomParser
1285
   */
1286 27
  private function removeComments(HtmlDomParser $dom): HtmlDomParser
1287
  {
1288 27
    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 27
    $dom->getDocument()->normalizeDocument();
1297
1298 27
    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 17
  private function sortCssClassNames($attrName, $attrValue): string
1371
  {
1372 17
    if ($attrName !== 'class' || !$attrValue) {
1373 14
      return $attrValue;
1374
    }
1375
1376 11
    $classes = \array_unique(
1377 11
        \explode(' ', $attrValue)
1378
    );
1379 11
    \sort($classes);
1380
1381 11
    $attrValue = '';
1382 11
    foreach ($classes as $class) {
1383
1384 11
      if (!$class) {
1385 3
        continue;
1386
      }
1387
1388 11
      $attrValue .= \trim($class) . ' ';
1389
    }
1390 11
    $attrValue = \trim($attrValue);
1391
1392 11
    return $attrValue;
1393
  }
1394
1395
  /**
1396
   * Sum-up extra whitespace from dom-nodes.
1397
   *
1398
   * @param HtmlDomParser $dom
1399
   *
1400
   * @return HtmlDomParser
1401
   */
1402 27
  private function sumUpWhitespace(HtmlDomParser $dom): HtmlDomParser
1403
  {
1404 27
    $textnodes = $dom->find('//text()');
1405 27
    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 23
      $textnode = $textnodeWrapper->getNode();
1408 23
      $xp = $textnode->getNodePath();
1409
1410 23
      $doSkip = false;
1411 23
      foreach (self::$skipTagsForRemoveWhitespace as $pattern) {
1412 23
        if (\strpos($xp, "/$pattern") !== false) {
1413 3
          $doSkip = true;
1414 23
          break;
1415
        }
1416
      }
1417 23
      if ($doSkip) {
1418 3
        continue;
1419
      }
1420
1421 23
      $textnode->nodeValue = \preg_replace(self::$regExSpace, ' ', $textnode->nodeValue);
1422
    }
1423
1424 27
    $dom->getDocument()->normalizeDocument();
1425
1426 27
    return $dom;
1427
  }
1428
}
1429