Completed
Push — master ( 60e98c...d80912 )
by Lars
01:25
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
 * - Java: https://code.google.com/archive/p/htmlcompressor/
16
 *
17
 * Ideas:
18
 * - http://perfectionkills.com/optimizing-html/
19
 *
20
 * @package voku\helper
21
 */
22
class HtmlMin
23
{
24
  /**
25
   * @var string
26
   */
27
  private static $regExSpace = "/[[:space:]]{2,}|[\r\n]+/u";
28
29
  /**
30
   * @var array
31
   */
32
  private static $optional_end_tags = [
33
      'html',
34
      'head',
35
      'body',
36
  ];
37
38
  /**
39
   * // https://mathiasbynens.be/demo/javascript-mime-type
40
   * // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
41
   *
42
   * @var array
43
   */
44
  private static $executableScriptsMimeTypes = [
45
      'text/javascript'          => '',
46
      'text/ecmascript'          => '',
47
      'text/jscript'             => '',
48
      'application/javascript'   => '',
49
      'application/x-javascript' => '',
50
      'application/ecmascript'   => '',
51
  ];
52
53
  private static $selfClosingTags = [
54
      'area',
55
      'base',
56
      'basefont',
57
      'br',
58
      'col',
59
      'command',
60
      'embed',
61
      'frame',
62
      'hr',
63
      'img',
64
      'input',
65
      'isindex',
66
      'keygen',
67
      'link',
68
      'meta',
69
      'param',
70
      'source',
71
      'track',
72
      'wbr',
73
  ];
74
75
  private static $trimWhitespaceFromTags = [
76
      'article' => '',
77
      'br'      => '',
78
      'div'     => '',
79
      'footer'  => '',
80
      'hr'      => '',
81
      'nav'     => '',
82
      'p'       => '',
83
      'script'  => '',
84
  ];
85
86
  /**
87
   * @var array
88
   */
89
  private static $booleanAttributes = [
90
      'allowfullscreen' => '',
91
      'async'           => '',
92
      'autofocus'       => '',
93
      'autoplay'        => '',
94
      'checked'         => '',
95
      'compact'         => '',
96
      'controls'        => '',
97
      'declare'         => '',
98
      'default'         => '',
99
      'defaultchecked'  => '',
100
      'defaultmuted'    => '',
101
      'defaultselected' => '',
102
      'defer'           => '',
103
      'disabled'        => '',
104
      'enabled'         => '',
105
      'formnovalidate'  => '',
106
      'hidden'          => '',
107
      'indeterminate'   => '',
108
      'inert'           => '',
109
      'ismap'           => '',
110
      'itemscope'       => '',
111
      'loop'            => '',
112
      'multiple'        => '',
113
      'muted'           => '',
114
      'nohref'          => '',
115
      'noresize'        => '',
116
      'noshade'         => '',
117
      'novalidate'      => '',
118
      'nowrap'          => '',
119
      'open'            => '',
120
      'pauseonexit'     => '',
121
      'readonly'        => '',
122
      'required'        => '',
123
      'reversed'        => '',
124
      'scoped'          => '',
125
      'seamless'        => '',
126
      'selected'        => '',
127
      'sortable'        => '',
128
      'truespeed'       => '',
129
      'typemustmatch'   => '',
130
      'visible'         => '',
131
  ];
132
  /**
133
   * @var array
134
   */
135
  private static $skipTagsForRemoveWhitespace = [
136
      'code',
137
      'pre',
138
      'script',
139
      'style',
140
      'textarea',
141
  ];
142
143
  /**
144
   * @var array
145
   */
146
  private $protectedChildNodes = [];
147
148
  /**
149
   * @var string
150
   */
151
  private $protectedChildNodesHelper = 'html-min--voku--saved-content';
152
153
  /**
154
   * @var bool
155
   */
156
  private $doOptimizeViaHtmlDomParser = true;
157
158
  /**
159
   * @var bool
160
   */
161
  private $doOptimizeAttributes = true;
162
163
  /**
164
   * @var bool
165
   */
166
  private $doRemoveComments = true;
167
168
  /**
169
   * @var bool
170
   */
171
  private $doRemoveWhitespaceAroundTags = true;
172
173
  /**
174
   * @var bool
175
   */
176
  private $doRemoveOmittedQuotes = true;
177
178
  /**
179
   * @var bool
180
   */
181
  private $doRemoveOmittedHtmlTags = true;
182
183
  /**
184
   * @var bool
185
   */
186
  private $doRemoveHttpPrefixFromAttributes = false;
187
188
  /**
189
   * @var array
190
   */
191
  private $domainsToRemoveHttpPrefixFromAttributes = [
192
      'google.com',
193
      'google.de',
194
  ];
195
196
  /**
197
   * @var bool
198
   */
199
  private $doSortCssClassNames = true;
200
201
  /**
202
   * @var bool
203
   */
204
  private $doSortHtmlAttributes = true;
205
206
  /**
207
   * @var bool
208
   */
209
  private $doRemoveDeprecatedScriptCharsetAttribute = true;
210
211
  /**
212
   * @var bool
213
   */
214
  private $doRemoveDefaultAttributes = false;
215
216
  /**
217
   * @var bool
218
   */
219
  private $doRemoveDeprecatedAnchorName = true;
220
221
  /**
222
   * @var bool
223
   */
224
  private $doRemoveDeprecatedTypeFromStylesheetLink = true;
225
226
  /**
227
   * @var bool
228
   */
229
  private $doRemoveDeprecatedTypeFromScriptTag = true;
230
231
  /**
232
   * @var bool
233
   */
234
  private $doRemoveValueFromEmptyInput = true;
235
236
  /**
237
   * @var bool
238
   */
239
  private $doRemoveEmptyAttributes = true;
240
241
  /**
242
   * @var bool
243
   */
244
  private $doSumUpWhitespace = true;
245
246
  /**
247
   * @var bool
248
   */
249
  private $doRemoveSpacesBetweenTags = false;
250
251
  /**
252
   * @var
253
   */
254
  private $withDocType;
255
256
  /**
257
   * HtmlMin constructor.
258
   */
259 27
  public function __construct()
260
  {
261 27
  }
262
263
  /**
264
   * @param boolean $doOptimizeAttributes
265
   *
266
   * @return $this
267
   */
268 2
  public function doOptimizeAttributes(bool $doOptimizeAttributes = true)
269
  {
270 2
    $this->doOptimizeAttributes = $doOptimizeAttributes;
271
272 2
    return $this;
273
  }
274
275
  /**
276
   * @param boolean $doOptimizeViaHtmlDomParser
277
   *
278
   * @return $this
279
   */
280 1
  public function doOptimizeViaHtmlDomParser(bool $doOptimizeViaHtmlDomParser = true)
281
  {
282 1
    $this->doOptimizeViaHtmlDomParser = $doOptimizeViaHtmlDomParser;
283
284 1
    return $this;
285
  }
286
287
  /**
288
   * @param boolean $doRemoveComments
289
   *
290
   * @return $this
291
   */
292 2
  public function doRemoveComments(bool $doRemoveComments = true)
293
  {
294 2
    $this->doRemoveComments = $doRemoveComments;
295
296 2
    return $this;
297
  }
298
299
  /**
300
   * @param boolean $doRemoveDefaultAttributes
301
   *
302
   * @return $this
303
   */
304 2
  public function doRemoveDefaultAttributes(bool $doRemoveDefaultAttributes = true)
305
  {
306 2
    $this->doRemoveDefaultAttributes = $doRemoveDefaultAttributes;
307
308 2
    return $this;
309
  }
310
311
  /**
312
   * @param boolean $doRemoveDeprecatedAnchorName
313
   *
314
   * @return $this
315
   */
316 2
  public function doRemoveDeprecatedAnchorName(bool $doRemoveDeprecatedAnchorName = true)
317
  {
318 2
    $this->doRemoveDeprecatedAnchorName = $doRemoveDeprecatedAnchorName;
319
320 2
    return $this;
321
  }
322
323
  /**
324
   * @param boolean $doRemoveDeprecatedScriptCharsetAttribute
325
   *
326
   * @return $this
327
   */
328 2
  public function doRemoveDeprecatedScriptCharsetAttribute(bool $doRemoveDeprecatedScriptCharsetAttribute = true)
329
  {
330 2
    $this->doRemoveDeprecatedScriptCharsetAttribute = $doRemoveDeprecatedScriptCharsetAttribute;
331
332 2
    return $this;
333
  }
334
335
  /**
336
   * @param boolean $doRemoveDeprecatedTypeFromScriptTag
337
   *
338
   * @return $this
339
   */
340 2
  public function doRemoveDeprecatedTypeFromScriptTag(bool $doRemoveDeprecatedTypeFromScriptTag = true)
341
  {
342 2
    $this->doRemoveDeprecatedTypeFromScriptTag = $doRemoveDeprecatedTypeFromScriptTag;
343
344 2
    return $this;
345
  }
346
347
  /**
348
   * @param boolean $doRemoveDeprecatedTypeFromStylesheetLink
349
   *
350
   * @return $this
351
   */
352 2
  public function doRemoveDeprecatedTypeFromStylesheetLink(bool $doRemoveDeprecatedTypeFromStylesheetLink = true)
353
  {
354 2
    $this->doRemoveDeprecatedTypeFromStylesheetLink = $doRemoveDeprecatedTypeFromStylesheetLink;
355
356 2
    return $this;
357
  }
358
359
  /**
360
   * @param boolean $doRemoveEmptyAttributes
361
   *
362
   * @return $this
363
   */
364 2
  public function doRemoveEmptyAttributes(bool $doRemoveEmptyAttributes = true)
365
  {
366 2
    $this->doRemoveEmptyAttributes = $doRemoveEmptyAttributes;
367
368 2
    return $this;
369
  }
370
371
  /**
372
   * @param boolean $doRemoveHttpPrefixFromAttributes
373
   *
374
   * @return $this
375
   */
376 4
  public function doRemoveHttpPrefixFromAttributes(bool $doRemoveHttpPrefixFromAttributes = true)
377
  {
378 4
    $this->doRemoveHttpPrefixFromAttributes = $doRemoveHttpPrefixFromAttributes;
379
380 4
    return $this;
381
  }
382
383
  /**
384
   * @param boolean $doRemoveSpacesBetweenTags
385
   *
386
   * @return $this
387
   */
388
  public function doRemoveSpacesBetweenTags(bool $doRemoveSpacesBetweenTags = true)
389
  {
390
    $this->doRemoveSpacesBetweenTags = $doRemoveSpacesBetweenTags;
391
392
    return $this;
393
  }
394
395
  /**
396
   * @param boolean $doRemoveValueFromEmptyInput
397
   *
398
   * @return $this
399
   */
400 2
  public function doRemoveValueFromEmptyInput(bool $doRemoveValueFromEmptyInput = true)
401
  {
402 2
    $this->doRemoveValueFromEmptyInput = $doRemoveValueFromEmptyInput;
403
404 2
    return $this;
405
  }
406
407
  /**
408
   * @param boolean $doRemoveWhitespaceAroundTags
409
   *
410
   * @return $this
411
   */
412 2
  public function doRemoveWhitespaceAroundTags(bool $doRemoveWhitespaceAroundTags = true)
413
  {
414 2
    $this->doRemoveWhitespaceAroundTags = $doRemoveWhitespaceAroundTags;
415
416 2
    return $this;
417
  }
418
419
  /**
420
   * @param bool $doRemoveOmittedQuotes
421
   *
422
   * @return $this
423
   */
424 1
  public function doRemoveOmittedQuotes(bool $doRemoveOmittedQuotes = true)
425
  {
426 1
    $this->doRemoveOmittedQuotes = $doRemoveOmittedQuotes;
427
428 1
    return $this;
429
  }
430
431
  /**
432
   * @param bool $doRemoveOmittedHtmlTags
433
   *
434
   * @return $this
435
   */
436 1
  public function doRemoveOmittedHtmlTags(bool $doRemoveOmittedHtmlTags = true)
437
  {
438 1
    $this->doRemoveOmittedHtmlTags = $doRemoveOmittedHtmlTags;
439
440 1
    return $this;
441
  }
442
443
  /**
444
   * @param boolean $doSortCssClassNames
445
   *
446
   * @return $this
447
   */
448 2
  public function doSortCssClassNames(bool $doSortCssClassNames = true)
449
  {
450 2
    $this->doSortCssClassNames = $doSortCssClassNames;
451
452 2
    return $this;
453
  }
454
455
  /**
456
   * @param boolean $doSortHtmlAttributes
457
   *
458
   * @return $this
459
   */
460 2
  public function doSortHtmlAttributes(bool $doSortHtmlAttributes = true)
461
  {
462 2
    $this->doSortHtmlAttributes = $doSortHtmlAttributes;
463
464 2
    return $this;
465
  }
466
467
  /**
468
   * @param boolean $doSumUpWhitespace
469
   *
470
   * @return $this
471
   */
472 2
  public function doSumUpWhitespace(bool $doSumUpWhitespace = true)
473
  {
474 2
    $this->doSumUpWhitespace = $doSumUpWhitespace;
475
476 2
    return $this;
477
  }
478
479 23
  private function domNodeAttributesToString(\DOMNode $node): string
480
  {
481
    # Remove quotes around attribute values, when allowed (<p class="foo"> → <p class=foo>)
482 23
    $attrstr = '';
483 23
    if ($node->attributes != null) {
484 23
      foreach ($node->attributes as $attribute) {
485 12
        $attrstr .= $attribute->name;
486
487
        if (
488 12
            $this->doOptimizeAttributes === true
489
            &&
490 12
            isset(self::$booleanAttributes[$attribute->name])
491
        ) {
492 6
          $attrstr .= ' ';
493 6
          continue;
494
        }
495
496 12
        $attrstr .= '=';
497
498
        # http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#attributes-0
499 12
        $omitquotes = $this->doRemoveOmittedQuotes
500
                      &&
501 12
                      $attribute->value != ''
502
                      &&
503 12
                      0 == \preg_match('/["\'=<>` \t\r\n\f]+/', $attribute->value);
504
505 12
        $attr_val = $attribute->value;
506 12
        $attrstr .= ($omitquotes ? '' : '"') . $attr_val . ($omitquotes ? '' : '"');
507 12
        $attrstr .= ' ';
508
      }
509
    }
510
511 23
    return \trim($attrstr);
512
  }
513
514 22
  private function domNodeClosingTagOptional(\DOMNode $node): bool
515
  {
516 22
    $tag_name = $node->tagName;
0 ignored issues
show
Bug introduced by
The property tagName does not seem to exist in DOMNode.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
517 22
    $nextSibling = $this->getNextSiblingOfTypeDOMElement($node);
518
519
    // https://html.spec.whatwg.org/multipage/syntax.html#syntax-tag-omission
520
521
    // Implemented:
522
    //
523
    // 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.
524
    // 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.
525
    // 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.
526
    // 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.
527
    // 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.
528
    // 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.
529
    // A <dt> element's end tag may be omitted if the dt element is immediately followed by another dt element or a dd element.
530
    // 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.
531
    // 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.
532
533
    // TODO:
534
    //
535
    // <html> may be omitted if first thing inside is not comment
536
    // <head> may be omitted if first thing inside is an element
537
    // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
538
    // <colgroup> may be omitted if first thing inside is <col>
539
    // <tbody> may be omitted if first thing inside is <tr>
540
    // 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.
541
    // 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.)
542
    // A <colgroup> element's end tag may be omitted if the colgroup element is not immediately followed by ASCII whitespace or a comment.
543
    // A <caption> element's end tag may be omitted if the caption element is not immediately followed by ASCII whitespace or a comment.
544
    // A <thead> element's end tag may be omitted if the thead element is immediately followed by a tbody or tfoot element.
545
    // 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.)
546
    // 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.
547
    // A <tfoot> element's end tag may be omitted if there is no more content in the parent element.
548
    //
549
    // <-- However, a start tag must never be omitted if it has any attributes.
550
551 22
    return \in_array($tag_name, self::$optional_end_tags, true)
552
           ||
553
           (
554 19
               $tag_name == 'li'
555
               &&
556
               (
557 4
                   $nextSibling === null
558
                   ||
559
                   (
560 2
                       $nextSibling instanceof \DOMElement
561
                       &&
562 19
                       $nextSibling->tagName == 'li'
563
                   )
564
               )
565
           )
566
           ||
567
           (
568
               (
569 19
                   $tag_name == 'rp'
570
               )
571
               &&
572
               (
573
                   $nextSibling === null
574
                   ||
575
                   (
576
                       $nextSibling instanceof \DOMElement
577
                       &&
578
                       (
579
                           $nextSibling->tagName == 'rp'
580
                           ||
581 19
                           $nextSibling->tagName == 'rt'
582
                       )
583
                   )
584
               )
585
           )
586
           ||
587
           (
588 19
               $tag_name == 'tr'
589
               &&
590
               (
591 1
                   $nextSibling === null
592
                   ||
593
                   (
594 1
                       $nextSibling instanceof \DOMElement
595
                       &&
596 19
                       $nextSibling->tagName == 'tr'
597
                   )
598
               )
599
           )
600
           ||
601
           (
602
               (
603 19
                   $tag_name == 'td'
604
                   ||
605 19
                   $tag_name == 'th'
606
               )
607
               &&
608
               (
609 1
                   $nextSibling === null
610
                   ||
611
                   (
612 1
                       $nextSibling instanceof \DOMElement
613
                       &&
614
                       (
615 1
                           $nextSibling->tagName == 'td'
616
                           ||
617 19
                           $nextSibling->tagName == 'th'
618
                       )
619
                   )
620
               )
621
           )
622
           ||
623
           (
624
               (
625 19
                   $tag_name == 'dd'
626
                   ||
627 19
                   $tag_name == 'dt'
628
               )
629
               &&
630
               (
631
                   (
632 1
                       $nextSibling === null
633
                       &&
634 1
                       $tag_name == 'dd'
635
                   )
636
                   ||
637
                   (
638 1
                       $nextSibling instanceof \DOMElement
639
                       &&
640
                       (
641 1
                           $nextSibling->tagName == 'dd'
642
                           ||
643 19
                           $nextSibling->tagName == 'dt'
644
                       )
645
                   )
646
               )
647
           )
648
           ||
649
           (
650 19
               $tag_name == 'option'
651
               &&
652
               (
653
                   $nextSibling === null
654
                   ||
655
                   (
656
                       $nextSibling instanceof \DOMElement
657
                       &&
658
                       (
659
                           $nextSibling->tagName == 'option'
660
                           ||
661 19
                           $nextSibling->tagName == 'optgroup'
662
                       )
663
                   )
664
               )
665
           )
666
           ||
667
           (
668 19
               $tag_name == 'p'
669
               &&
670
               (
671
                   (
672 9
                       $nextSibling === null
673
                       &&
674
                       (
675 9
                           $node->parentNode !== null
676
                           &&
677 9
                           !\in_array(
678 9
                               $node->parentNode->tagName,
679
                               [
680 9
                                   'a',
681
                                   'audio',
682
                                   'del',
683
                                   'ins',
684
                                   'map',
685
                                   'noscript',
686
                                   'video',
687
                               ],
688 9
                               true
689
                           )
690
                       )
691
                   )
692
                   ||
693
                   (
694 5
                       $nextSibling instanceof \DOMElement
695
                       &&
696 5
                       \in_array(
697 5
                           $nextSibling->tagName,
698
                           [
699 5
                               'address',
700
                               'article',
701
                               'aside',
702
                               'blockquote',
703
                               'dir',
704
                               'div',
705
                               'dl',
706
                               'fieldset',
707
                               'footer',
708
                               'form',
709
                               'h1',
710
                               'h2',
711
                               'h3',
712
                               'h4',
713
                               'h5',
714
                               'h6',
715
                               'header',
716
                               'hgroup',
717
                               'hr',
718
                               'menu',
719
                               'nav',
720
                               'ol',
721
                               'p',
722
                               'pre',
723
                               'section',
724
                               'table',
725
                               'ul',
726
                           ],
727 22
                           true
728
                       )
729
                   )
730
               )
731
           );
732
  }
733
734 23
  protected function domNodeToString(\DOMNode $node): string
735
  {
736
    // init
737 23
    $html = '';
738
739 23
    foreach ($node->childNodes as $child) {
740
741 23
      if ($child instanceof \DOMDocumentType) {
742
743
        // add the doc-type only if it wasn't generated by DomDocument
744 10
        if ($this->withDocType !== true) {
745 2
          continue;
746
        }
747
748 8
        if ($child->name) {
749
750 8
          if (!$child->publicId && $child->systemId) {
751
            $tmpTypeSystem = 'SYSTEM';
752
            $tmpTypePublic = '';
753
          } else {
754 8
            $tmpTypeSystem = '';
755 8
            $tmpTypePublic = 'PUBLIC';
756
          }
757
758 8
          $html .= '<!DOCTYPE ' . $child->name . ''
759 8
                   . ($child->publicId ? ' ' . $tmpTypePublic . ' "' . $child->publicId . '"' : '')
760 8
                   . ($child->systemId ? ' ' . $tmpTypeSystem . ' "' . $child->systemId . '"' : '')
761 8
                   . '>';
762
        }
763
764 23
      } else if ($child instanceof \DOMElement) {
765
766 23
        $html .= trim('<' . $child->tagName . ' ' . $this->domNodeAttributesToString($child));
767 23
        $html .= '>' . $this->domNodeToString($child);
768
769
        if (
770 23
            $this->doRemoveOmittedHtmlTags === false
771
            ||
772 23
            !$this->domNodeClosingTagOptional($child)) {
773 23
          $html .= '</' . $child->tagName . '>';
774
        }
775
776 19
      } elseif ($child instanceof \DOMText) {
777
778 19
        if ($child->isWhitespaceInElementContent()) {
779
          if (
780 15
              $child->previousSibling !== null
781
              &&
782 15
              $child->nextSibling !== null
783
          ) {
784 15
            $html .= ' ';
785
          }
786
        } else {
787 19
          $html .= $child->wholeText;
788
        }
789
790
      } elseif ($child instanceof \DOMComment) {
791
792 23
        $html .= $child->wholeText;
793
794
      }
795
    }
796
797 23
    return $html;
798
  }
799
800
  /**
801
   * @param \DOMNode $node
802
   *
803
   * @return \DOMNode|null
804
   */
805 22
  protected function getNextSiblingOfTypeDOMElement(\DOMNode $node)
806
  {
807
    do {
808 22
      $node = $node->nextSibling;
809 22
    } while (!($node === null || $node instanceof \DOMElement));
810
811 22
    return $node;
812
  }
813
814
  /**
815
   * Check if the current string is an conditional comment.
816
   *
817
   * INFO: since IE >= 10 conditional comment are not working anymore
818
   *
819
   * <!--[if expression]> HTML <![endif]-->
820
   * <![if expression]> HTML <![endif]>
821
   *
822
   * @param string $comment
823
   *
824
   * @return bool
825
   */
826 3
  private function isConditionalComment($comment): bool
827
  {
828 3
    if (preg_match('/^\[if [^\]]+\]/', $comment)) {
829 2
      return true;
830
    }
831
832 3
    if (preg_match('/\[endif\]$/', $comment)) {
833 1
      return true;
834
    }
835
836 3
    return false;
837
  }
838
839
  /**
840
   * @param string $html
841
   * @param bool   $decodeUtf8Specials <p>Use this only in special cases, e.g. for PHP 5.3</p>
842
   *
843
   * @return string
844
   */
845 27
  public function minify($html, $decodeUtf8Specials = false): string
846
  {
847 27
    $html = (string)$html;
848 27
    if (!isset($html[0])) {
849 1
      return '';
850
    }
851
852 27
    $html = trim($html);
853 27
    if (!$html) {
854 3
      return '';
855
    }
856
857
    // init
858 24
    static $CACHE_SELF_CLOSING_TAGS = null;
859 24
    if ($CACHE_SELF_CLOSING_TAGS === null) {
860 1
      $CACHE_SELF_CLOSING_TAGS = implode('|', self::$selfClosingTags);
861
    }
862
863
    // reset
864 24
    $this->protectedChildNodes = [];
865
866
    // save old content
867 24
    $origHtml = $html;
868 24
    $origHtmlLength = UTF8::strlen($html);
869
870
    // -------------------------------------------------------------------------
871
    // Minify the HTML via "HtmlDomParser"
872
    // -------------------------------------------------------------------------
873
874 24
    if ($this->doOptimizeViaHtmlDomParser === true) {
875 23
      $html = $this->minifyHtmlDom($html, $decodeUtf8Specials);
876
    }
877
878
    // -------------------------------------------------------------------------
879
    // Trim whitespace from html-string. [protected html is still protected]
880
    // -------------------------------------------------------------------------
881
882
    // Remove extra white-space(s) between HTML attribute(s)
883 24
    $html = (string)\preg_replace_callback(
884 24
        '#<([^\/\s<>!]+)(?:\s+([^<>]*?)\s*|\s*)(\/?)>#',
885 24
        function ($matches) {
886 24
          return '<' . $matches[1] . (string)\preg_replace('#([^\s=]+)(\=([\'"]?)(.*?)\3)?(\s+|$)#s', ' $1$2', $matches[2]) . $matches[3] . '>';
887 24
        },
888 24
        $html
889
    );
890
891
892 24
    if ($this->doRemoveSpacesBetweenTags === true) {
893
      // Remove spaces that are between > and <
894
      $html = (string)\preg_replace('/(>) (<)/', '>$2', $html);
895
    }
896
897
    // -------------------------------------------------------------------------
898
    // Restore protected HTML-code.
899
    // -------------------------------------------------------------------------
900
901 24
    $html = (string)\preg_replace_callback(
902 24
        '/<(?<element>' . $this->protectedChildNodesHelper . ')(?<attributes> [^>]*)?>(?<value>.*?)<\/' . $this->protectedChildNodesHelper . '>/',
903 24
        [$this, 'restoreProtectedHtml'],
904 24
        $html
905
    );
906
907
    // -------------------------------------------------------------------------
908
    // Restore protected HTML-entities.
909
    // -------------------------------------------------------------------------
910
911 24
    if ($this->doOptimizeViaHtmlDomParser === true) {
912 23
      $html = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($html);
913
    }
914
915
    // ------------------------------------
916
    // Final clean-up
917
    // ------------------------------------
918
919 24
    $html = UTF8::cleanup($html);
920
921 24
    $html = \str_replace(
922
        [
923 24
            'html>' . "\n",
924
            "\n" . '<html',
925
            'html/>' . "\n",
926
            "\n" . '</html',
927
            'head>' . "\n",
928
            "\n" . '<head',
929
            'head/>' . "\n",
930
            "\n" . '</head',
931
        ],
932
        [
933 24
            'html>',
934
            '<html',
935
            'html/>',
936
            '</html',
937
            'head>',
938
            '<head',
939
            'head/>',
940
            '</head',
941
        ],
942 24
        $html
943
    );
944
945
    // self closing tags, don't need a trailing slash ...
946 24
    $replace = [];
947 24
    $replacement = [];
948 24
    foreach (self::$selfClosingTags as $selfClosingTag) {
949 24
      $replace[] = '<' . $selfClosingTag . '/>';
950 24
      $replacement[] = '<' . $selfClosingTag . '>';
951 24
      $replace[] = '<' . $selfClosingTag . ' />';
952 24
      $replacement[] = '<' . $selfClosingTag . '>';
953
    }
954 24
    $html = \str_replace(
955 24
        $replace,
956 24
        $replacement,
957 24
        $html
958
    );
959
960 24
    $html = (string)\preg_replace('#<\b(' . $CACHE_SELF_CLOSING_TAGS . ')([^>]+)><\/\b\1>#', '<\\1\\2>', $html);
961
962
    // ------------------------------------
963
    // check if compression worked
964
    // ------------------------------------
965
966 24
    if ($origHtmlLength < UTF8::strlen($html)) {
967 2
      $html = $origHtml;
968
    }
969
970 24
    return $html;
971
  }
972
973
  /**
974
   * @param $html
975
   * @param $decodeUtf8Specials
976
   *
977
   * @return string
978
   */
979 23
  private function minifyHtmlDom($html, $decodeUtf8Specials): string
980
  {
981
    // init dom
982 23
    $dom = new HtmlDomParser();
983 23
    $dom->getDocument()->preserveWhiteSpace = false; // remove redundant white space
984 23
    $dom->getDocument()->formatOutput = false; // do not formats output with indentation
985
986
    // load dom
987 23
    $dom->loadHtml($html);
988
989 23
    $this->withDocType = (stripos(trim($html), '<!DOCTYPE') === 0);
990
991
    // -------------------------------------------------------------------------
992
    // Protect HTML tags and conditional comments.
993
    // -------------------------------------------------------------------------
994
995 23
    $dom = $this->protectTags($dom);
996
997
    // -------------------------------------------------------------------------
998
    // Remove default HTML comments. [protected html is still protected]
999
    // -------------------------------------------------------------------------
1000
1001 23
    if ($this->doRemoveComments === true) {
1002 22
      $dom = $this->removeComments($dom);
1003
    }
1004
1005
    // -------------------------------------------------------------------------
1006
    // Sum-Up extra whitespace from the Dom. [protected html is still protected]
1007
    // -------------------------------------------------------------------------
1008
1009 23
    if ($this->doSumUpWhitespace === true) {
1010 22
      $dom = $this->sumUpWhitespace($dom);
1011
    }
1012
1013 23
    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...
1014
1015
      // -------------------------------------------------------------------------
1016
      // Optimize html attributes. [protected html is still protected]
1017
      // -------------------------------------------------------------------------
1018
1019 23
      if ($this->doOptimizeAttributes === true) {
1020 22
        $this->optimizeAttributes($element);
1021
      }
1022
1023
      // -------------------------------------------------------------------------
1024
      // Remove whitespace around tags. [protected html is still protected]
1025
      // -------------------------------------------------------------------------
1026
1027 23
      if ($this->doRemoveWhitespaceAroundTags === true) {
1028 23
        $this->removeWhitespaceAroundTags($element);
1029
      }
1030
    }
1031
1032
    // -------------------------------------------------------------------------
1033
    // Convert the Dom into a string.
1034
    // -------------------------------------------------------------------------
1035
1036 23
    $html = $dom->fixHtmlOutput(
1037 23
        $this->domNodeToString($dom->getDocument()),
1038 23
        $decodeUtf8Specials
1039
    );
1040
1041 23
    return $html;
1042
  }
1043
1044
  /**
1045
   * Sort HTML-Attributes, so that gzip can do better work and remove some default attributes...
1046
   *
1047
   * @param SimpleHtmlDom $element
1048
   *
1049
   * @return bool
1050
   */
1051 22
  private function optimizeAttributes(SimpleHtmlDom $element): bool
1052
  {
1053 22
    $attributes = $element->getAllAttributes();
1054 22
    if ($attributes === null) {
1055 22
      return false;
1056
    }
1057
1058 11
    $attrs = [];
1059 11
    foreach ((array)$attributes as $attrName => $attrValue) {
1060
1061
      // -------------------------------------------------------------------------
1062
      // Remove optional "http:"-prefix from attributes.
1063
      // -------------------------------------------------------------------------
1064
1065 11
      if ($this->doRemoveHttpPrefixFromAttributes === true) {
1066
        if (
1067 3
            ($attrName === 'href' || $attrName === 'src' || $attrName === 'action')
1068
            &&
1069 3
            !(isset($attributes['rel']) && $attributes['rel'] === 'external')
1070
            &&
1071 3
            !(isset($attributes['target']) && $attributes['target'] === '_blank')
1072
        ) {
1073 2
          $attrValue = \str_replace('http://', '//', $attrValue);
1074
        }
1075
      }
1076
1077 11
      if ($this->removeAttributeHelper($element->tag, $attrName, $attrValue, $attributes)) {
1078 3
        $element->{$attrName} = null;
1079 3
        continue;
1080
      }
1081
1082
      // -------------------------------------------------------------------------
1083
      // Sort css-class-names, for better gzip results.
1084
      // -------------------------------------------------------------------------
1085
1086 11
      if ($this->doSortCssClassNames === true) {
1087 11
        $attrValue = $this->sortCssClassNames($attrName, $attrValue);
1088
      }
1089
1090 11
      if ($this->doSortHtmlAttributes === true) {
1091 11
        $attrs[$attrName] = $attrValue;
1092 11
        $element->{$attrName} = null;
1093
      }
1094
    }
1095
1096
    // -------------------------------------------------------------------------
1097
    // Sort html-attributes, for better gzip results.
1098
    // -------------------------------------------------------------------------
1099
1100 11
    if ($this->doSortHtmlAttributes === true) {
1101 11
      \ksort($attrs);
1102 11
      foreach ($attrs as $attrName => $attrValue) {
1103 11
        $attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue);
1104 11
        $element->setAttribute($attrName, $attrValue, true);
1105
      }
1106
    }
1107
1108 11
    return true;
1109
  }
1110
1111
  /**
1112
   * Prevent changes of inline "styles" and "scripts".
1113
   *
1114
   * @param HtmlDomParser $dom
1115
   *
1116
   * @return HtmlDomParser
1117
   */
1118 23
  private function protectTags(HtmlDomParser $dom): HtmlDomParser
1119
  {
1120
    // init
1121 23
    $counter = 0;
1122
1123 23
    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...
1124
1125
      // skip external links
1126 3
      if ($element->tag === 'script' || $element->tag === 'style') {
1127 3
        $attributes = $element->getAllAttributes();
1128 3
        if (isset($attributes['src'])) {
1129 2
          continue;
1130
        }
1131
      }
1132
1133 2
      $this->protectedChildNodes[$counter] = $element->text();
1134 2
      $element->getNode()->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $counter . '"></' . $this->protectedChildNodesHelper . '>';
1135
1136 2
      ++$counter;
1137
    }
1138
1139 23
    $dom->getDocument()->normalizeDocument();
1140
1141 23
    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...
1142 3
      $text = $element->text();
1143
1144
      // skip normal comments
1145 3
      if ($this->isConditionalComment($text) === false) {
1146 3
        continue;
1147
      }
1148
1149 2
      $this->protectedChildNodes[$counter] = '<!--' . $text . '-->';
1150
1151
      /* @var $node \DOMComment */
1152 2
      $node = $element->getNode();
1153 2
      $child = new \DOMText('<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $counter . '"></' . $this->protectedChildNodesHelper . '>');
1154 2
      $element->getNode()->parentNode->replaceChild($child, $node);
1155
1156 2
      ++$counter;
1157
    }
1158
1159 23
    $dom->getDocument()->normalizeDocument();
1160
1161 23
    return $dom;
1162
  }
1163
1164
  /**
1165
   * Check if the attribute can be removed.
1166
   *
1167
   * @param string $tag
1168
   * @param string $attrName
1169
   * @param string $attrValue
1170
   * @param array  $allAttr
1171
   *
1172
   * @return bool
1173
   */
1174 11
  private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr): bool
1175
  {
1176
    // remove defaults
1177 11
    if ($this->doRemoveDefaultAttributes === true) {
1178
1179 1
      if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') {
1180
        return true;
1181
      }
1182
1183 1
      if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') {
1184
        return true;
1185
      }
1186
1187 1
      if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') {
1188
        return true;
1189
      }
1190
1191 1
      if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') {
1192
        return true;
1193
      }
1194
    }
1195
1196
    // remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway)
1197 11 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...
1198 11
      if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) {
1199
        return true;
1200
      }
1201
    }
1202
1203
    // remove deprecated anchor-jump
1204 11 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...
1205 11
      if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) {
1206
        return true;
1207
      }
1208
    }
1209
1210
    // remove "type=text/css" for css links
1211 11 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...
1212 11
      if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet') {
1213 1
        return true;
1214
      }
1215
    }
1216
1217
    // remove deprecated script-mime-types
1218 11 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...
1219 11
      if ($tag === 'script' && $attrName === 'type' && isset($allAttr['src'], self::$executableScriptsMimeTypes[$attrValue])) {
1220 1
        return true;
1221
      }
1222
    }
1223
1224
    // remove 'value=""' from <input type="text">
1225 11
    if ($this->doRemoveValueFromEmptyInput === true) {
1226 11
      if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') {
1227 1
        return true;
1228
      }
1229
    }
1230
1231
    // remove some empty attributes
1232 11
    if ($this->doRemoveEmptyAttributes === true) {
1233 11
      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)) {
1234 2
        return true;
1235
      }
1236
    }
1237
1238 11
    return false;
1239
  }
1240
1241
  /**
1242
   * Remove comments in the dom.
1243
   *
1244
   * @param HtmlDomParser $dom
1245
   *
1246
   * @return HtmlDomParser
1247
   */
1248 22
  private function removeComments(HtmlDomParser $dom): HtmlDomParser
1249
  {
1250 22
    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...
1251 3
      $comment = $commentWrapper->getNode();
1252 3
      $val = $comment->nodeValue;
1253 3
      if (\strpos($val, '[') === false) {
1254 3
        $comment->parentNode->removeChild($comment);
1255
      }
1256
    }
1257
1258 22
    $dom->getDocument()->normalizeDocument();
1259
1260 22
    return $dom;
1261
  }
1262
1263
  /**
1264
   * Trim tags in the dom.
1265
   *
1266
   * @param SimpleHtmlDom $element
1267
   *
1268
   * @return void
1269
   */
1270 22
  private function removeWhitespaceAroundTags(SimpleHtmlDom $element)
1271
  {
1272 22
    if (isset(self::$trimWhitespaceFromTags[$element->tag])) {
1273 10
      $node = $element->getNode();
1274
1275 10
      $candidates = [];
1276 10
      if ($node->childNodes->length > 0) {
1277 9
        $candidates[] = $node->firstChild;
1278 9
        $candidates[] = $node->lastChild;
1279 9
        $candidates[] = $node->previousSibling;
1280 9
        $candidates[] = $node->nextSibling;
1281
      }
1282
1283 10
      foreach ($candidates as &$candidate) {
1284 9
        if ($candidate === null) {
1285 5
          continue;
1286
        }
1287
1288 9
        if ($candidate->nodeType === 3) {
1289 9
          $candidate->nodeValue = \preg_replace(self::$regExSpace, ' ', $candidate->nodeValue);
1290
        }
1291
      }
1292
    }
1293 22
  }
1294
1295
  /**
1296
   * Callback function for preg_replace_callback use.
1297
   *
1298
   * @param array $matches PREG matches
1299
   *
1300
   * @return string
1301
   */
1302 2
  private function restoreProtectedHtml($matches): string
1303
  {
1304 2
    \preg_match('/.*"(?<id>\d*)"/', $matches['attributes'], $matchesInner);
1305
1306 2
    $html = '';
1307 2
    if (isset($this->protectedChildNodes[$matchesInner['id']])) {
1308 2
      $html .= $this->protectedChildNodes[$matchesInner['id']];
1309
    }
1310
1311 2
    return $html;
1312
  }
1313
1314
  /**
1315
   * @param array $domainsToRemoveHttpPrefixFromAttributes
1316
   *
1317
   * @return $this
1318
   */
1319 2
  public function setDomainsToRemoveHttpPrefixFromAttributes($domainsToRemoveHttpPrefixFromAttributes)
1320
  {
1321 2
    $this->domainsToRemoveHttpPrefixFromAttributes = $domainsToRemoveHttpPrefixFromAttributes;
1322
1323 2
    return $this;
1324
  }
1325
1326
  /**
1327
   * @param $attrName
1328
   * @param $attrValue
1329
   *
1330
   * @return string
1331
   */
1332 11
  private function sortCssClassNames($attrName, $attrValue): string
1333
  {
1334 11
    if ($attrName !== 'class' || !$attrValue) {
1335 11
      return $attrValue;
1336
    }
1337
1338 6
    $classes = \array_unique(
1339 6
        \explode(' ', $attrValue)
1340
    );
1341 6
    \sort($classes);
1342
1343 6
    $attrValue = '';
1344 6
    foreach ($classes as $class) {
1345
1346 6
      if (!$class) {
1347 2
        continue;
1348
      }
1349
1350 6
      $attrValue .= \trim($class) . ' ';
1351
    }
1352 6
    $attrValue = \trim($attrValue);
1353
1354 6
    return $attrValue;
1355
  }
1356
1357
  /**
1358
   * Sum-up extra whitespace from dom-nodes.
1359
   *
1360
   * @param HtmlDomParser $dom
1361
   *
1362
   * @return HtmlDomParser
1363
   */
1364 22
  private function sumUpWhitespace(HtmlDomParser $dom): HtmlDomParser
1365
  {
1366 22
    $textnodes = $dom->find('//text()');
1367 22
    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...
1368
      /* @var $textnode \DOMNode */
1369 18
      $textnode = $textnodeWrapper->getNode();
1370 18
      $xp = $textnode->getNodePath();
1371
1372 18
      $doSkip = false;
1373 18
      foreach (self::$skipTagsForRemoveWhitespace as $pattern) {
1374 18
        if (\strpos($xp, "/$pattern") !== false) {
1375 3
          $doSkip = true;
1376 18
          break;
1377
        }
1378
      }
1379 18
      if ($doSkip) {
1380 3
        continue;
1381
      }
1382
1383 18
      $textnode->nodeValue = \preg_replace(self::$regExSpace, ' ', $textnode->nodeValue);
1384
    }
1385
1386 22
    $dom->getDocument()->normalizeDocument();
1387
1388 22
    return $dom;
1389
  }
1390
}
1391