Completed
Push — master ( 6edde8...61afef )
by Lars
02:49
created

HtmlMin::domNodeToString()   D

Complexity

Conditions 23
Paths 49

Size

Total Lines 92
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 23.1798

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * Class HtmlMin
9
 *
10
 * Inspired by:
11
 * - JS: https://github.com/kangax/html-minifier/blob/gh-pages/src/htmlminifier.js
12
 * - PHP: https://github.com/searchturbine/phpwee-php-minifier
13
 * - PHP: https://github.com/WyriHaximus/HtmlCompress
14
 * - PHP: https://github.com/zaininnari/html-minifier
15
 * - PHP: https://github.com/ampaze/PHP-HTML-Minifier
16
 * - Java: https://code.google.com/archive/p/htmlcompressor/
17
 *
18
 * Ideas:
19
 * - http://perfectionkills.com/optimizing-html/
20
 *
21
 * @package voku\helper
22
 */
23
class HtmlMin
24
{
25
  /**
26
   * @var string
27
   */
28
  private static $regExSpace = "/[[:space:]]{2,}|[\r\n]+/u";
29
30
  /**
31
   * @var array
32
   */
33
  private static $optional_end_tags = [
34
      'html',
35
      'head',
36
      'body',
37
  ];
38
39
  /**
40
   * // https://mathiasbynens.be/demo/javascript-mime-type
41
   * // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type
42
   *
43
   * @var array
44
   */
45
  private static $executableScriptsMimeTypes = [
46
      'text/javascript'          => '',
47
      'text/ecmascript'          => '',
48
      'text/jscript'             => '',
49
      'application/javascript'   => '',
50
      'application/x-javascript' => '',
51
      'application/ecmascript'   => '',
52
  ];
53
54
  private static $selfClosingTags = [
55
      'area',
56
      'base',
57
      'basefont',
58
      'br',
59
      'col',
60
      'command',
61
      'embed',
62
      'frame',
63
      'hr',
64
      'img',
65
      'input',
66
      'isindex',
67
      'keygen',
68
      'link',
69
      'meta',
70
      'param',
71
      'source',
72
      'track',
73
      'wbr',
74
  ];
75
76
  private static $trimWhitespaceFromTags = [
77
      'article' => '',
78
      'br'      => '',
79
      'div'     => '',
80
      'footer'  => '',
81
      'hr'      => '',
82
      'nav'     => '',
83
      'p'       => '',
84
      'script'  => '',
85
  ];
86
87
  /**
88
   * @var array
89
   */
90
  private static $booleanAttributes = [
91
      'allowfullscreen' => '',
92
      'async'           => '',
93
      'autofocus'       => '',
94
      'autoplay'        => '',
95
      'checked'         => '',
96
      'compact'         => '',
97
      'controls'        => '',
98
      'declare'         => '',
99
      'default'         => '',
100
      'defaultchecked'  => '',
101
      'defaultmuted'    => '',
102
      'defaultselected' => '',
103
      'defer'           => '',
104
      'disabled'        => '',
105
      'enabled'         => '',
106
      'formnovalidate'  => '',
107
      'hidden'          => '',
108
      'indeterminate'   => '',
109
      'inert'           => '',
110
      'ismap'           => '',
111
      'itemscope'       => '',
112
      'loop'            => '',
113
      'multiple'        => '',
114
      'muted'           => '',
115
      'nohref'          => '',
116
      'noresize'        => '',
117
      'noshade'         => '',
118
      'novalidate'      => '',
119
      'nowrap'          => '',
120
      'open'            => '',
121
      'pauseonexit'     => '',
122
      'readonly'        => '',
123
      'required'        => '',
124
      'reversed'        => '',
125
      'scoped'          => '',
126
      'seamless'        => '',
127
      'selected'        => '',
128
      'sortable'        => '',
129
      'truespeed'       => '',
130
      'typemustmatch'   => '',
131
      'visible'         => '',
132
  ];
133
  /**
134
   * @var array
135
   */
136
  private static $skipTagsForRemoveWhitespace = [
137
      'code',
138
      'pre',
139
      'script',
140
      'style',
141
      'textarea',
142
  ];
143
144
  /**
145
   * @var array
146
   */
147
  private $protectedChildNodes = [];
148
149
  /**
150
   * @var string
151
   */
152
  private $protectedChildNodesHelper = 'html-min--voku--saved-content';
153
154
  /**
155
   * @var bool
156
   */
157
  private $doOptimizeViaHtmlDomParser = true;
158
159
  /**
160
   * @var bool
161
   */
162
  private $doOptimizeAttributes = true;
163
164
  /**
165
   * @var bool
166
   */
167
  private $doRemoveComments = true;
168
169
  /**
170
   * @var bool
171
   */
172
  private $doRemoveWhitespaceAroundTags = false;
173
174
  /**
175
   * @var bool
176
   */
177
  private $doRemoveOmittedQuotes = true;
178
179
  /**
180
   * @var bool
181
   */
182
  private $doRemoveOmittedHtmlTags = true;
183
184
  /**
185
   * @var bool
186
   */
187
  private $doRemoveHttpPrefixFromAttributes = false;
188
189
  /**
190
   * @var array
191
   */
192
  private $domainsToRemoveHttpPrefixFromAttributes = [
193
      'google.com',
194
      'google.de',
195
  ];
196
197
  /**
198
   * @var bool
199
   */
200
  private $doSortCssClassNames = true;
201
202
  /**
203
   * @var bool
204
   */
205
  private $doSortHtmlAttributes = true;
206
207
  /**
208
   * @var bool
209
   */
210
  private $doRemoveDeprecatedScriptCharsetAttribute = true;
211
212
  /**
213
   * @var bool
214
   */
215
  private $doRemoveDefaultAttributes = false;
216
217
  /**
218
   * @var bool
219
   */
220
  private $doRemoveDeprecatedAnchorName = true;
221
222
  /**
223
   * @var bool
224
   */
225
  private $doRemoveDeprecatedTypeFromStylesheetLink = true;
226
227
  /**
228
   * @var bool
229
   */
230
  private $doRemoveDeprecatedTypeFromScriptTag = true;
231
232
  /**
233
   * @var bool
234
   */
235
  private $doRemoveValueFromEmptyInput = true;
236
237
  /**
238
   * @var bool
239
   */
240
  private $doRemoveEmptyAttributes = true;
241
242
  /**
243
   * @var bool
244
   */
245
  private $doSumUpWhitespace = true;
246
247
  /**
248
   * @var bool
249
   */
250
  private $doRemoveSpacesBetweenTags = false;
251
252
  /**
253
   * @var
254
   */
255
  private $withDocType;
256
257
  /**
258
   * HtmlMin constructor.
259
   */
260 30
  public function __construct()
261
  {
262 30
  }
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 26
  private function domNodeAttributesToString(\DOMNode $node): string
481
  {
482
    # Remove quotes around attribute values, when allowed (<p class="foo"> → <p class=foo>)
483 26
    $attrstr = '';
484 26
    if ($node->attributes != null) {
485 26
      foreach ($node->attributes as $attribute) {
486 15
        $attrstr .= $attribute->name;
487
488
        if (
489 15
            $this->doOptimizeAttributes === true
490
            &&
491 15
            isset(self::$booleanAttributes[$attribute->name])
492
        ) {
493 6
          $attrstr .= ' ';
494 6
          continue;
495
        }
496
497 15
        $attrstr .= '=';
498
499
        # http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#attributes-0
500 15
        $omitquotes = $this->doRemoveOmittedQuotes
501
                      &&
502 15
                      $attribute->value != ''
503
                      &&
504 15
                      0 == \preg_match('/["\'=<>` \t\r\n\f]+/', $attribute->value);
505
506 15
        $attr_val = $attribute->value;
507 15
        $attrstr .= ($omitquotes ? '' : '"') . $attr_val . ($omitquotes ? '' : '"');
508 15
        $attrstr .= ' ';
509
      }
510
    }
511
512 26
    return \trim($attrstr);
513
  }
514
515 25
  private function domNodeClosingTagOptional(\DOMNode $node): bool
516
  {
517 25
    $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...
518 25
    $nextSibling = $this->getNextSiblingOfTypeDOMElement($node);
519
520
    // https://html.spec.whatwg.org/multipage/syntax.html#syntax-tag-omission
521
522
    // Implemented:
523
    //
524
    // 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.
525
    // 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.
526
    // 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.
527
    // 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.
528
    // 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.
529
    // 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.
530
    // A <dt> element's end tag may be omitted if the dt element is immediately followed by another dt element or a dd element.
531
    // 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.
532
    // 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.
533
534
    // TODO:
535
    //
536
    // <html> may be omitted if first thing inside is not comment
537
    // <head> may be omitted if first thing inside is an element
538
    // <body> may be omitted if first thing inside is not space, comment, <meta>, <link>, <script>, <style> or <template>
539
    // <colgroup> may be omitted if first thing inside is <col>
540
    // <tbody> may be omitted if first thing inside is <tr>
541
    // 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.
542
    // 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.)
543
    // A <colgroup> element's end tag may be omitted if the colgroup element is not immediately followed by ASCII whitespace or a comment.
544
    // A <caption> element's end tag may be omitted if the caption element is not immediately followed by ASCII whitespace or a comment.
545
    // A <thead> element's end tag may be omitted if the thead element is immediately followed by a tbody or tfoot element.
546
    // 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.)
547
    // 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.
548
    // A <tfoot> element's end tag may be omitted if there is no more content in the parent element.
549
    //
550
    // <-- However, a start tag must never be omitted if it has any attributes.
551
552 25
    return \in_array($tag_name, self::$optional_end_tags, true)
553
           ||
554
           (
555 22
               $tag_name == 'li'
556
               &&
557
               (
558 4
                   $nextSibling === null
559
                   ||
560
                   (
561 2
                       $nextSibling instanceof \DOMElement
562
                       &&
563 22
                       $nextSibling->tagName == 'li'
564
                   )
565
               )
566
           )
567
           ||
568
           (
569
               (
570 22
                   $tag_name == 'rp'
571
               )
572
               &&
573
               (
574
                   $nextSibling === null
575
                   ||
576
                   (
577
                       $nextSibling instanceof \DOMElement
578
                       &&
579
                       (
580
                           $nextSibling->tagName == 'rp'
581
                           ||
582 22
                           $nextSibling->tagName == 'rt'
583
                       )
584
                   )
585
               )
586
           )
587
           ||
588
           (
589 22
               $tag_name == 'tr'
590
               &&
591
               (
592 1
                   $nextSibling === null
593
                   ||
594
                   (
595 1
                       $nextSibling instanceof \DOMElement
596
                       &&
597 22
                       $nextSibling->tagName == 'tr'
598
                   )
599
               )
600
           )
601
           ||
602
           (
603
               (
604 22
                   $tag_name == 'td'
605
                   ||
606 22
                   $tag_name == 'th'
607
               )
608
               &&
609
               (
610 1
                   $nextSibling === null
611
                   ||
612
                   (
613 1
                       $nextSibling instanceof \DOMElement
614
                       &&
615
                       (
616 1
                           $nextSibling->tagName == 'td'
617
                           ||
618 22
                           $nextSibling->tagName == 'th'
619
                       )
620
                   )
621
               )
622
           )
623
           ||
624
           (
625
               (
626 22
                   $tag_name == 'dd'
627
                   ||
628 22
                   $tag_name == 'dt'
629
               )
630
               &&
631
               (
632
                   (
633 2
                       $nextSibling === null
634
                       &&
635 2
                       $tag_name == 'dd'
636
                   )
637
                   ||
638
                   (
639 2
                       $nextSibling instanceof \DOMElement
640
                       &&
641
                       (
642 2
                           $nextSibling->tagName == 'dd'
643
                           ||
644 22
                           $nextSibling->tagName == 'dt'
645
                       )
646
                   )
647
               )
648
           )
649
           ||
650
           (
651 22
               $tag_name == 'option'
652
               &&
653
               (
654
                   $nextSibling === null
655
                   ||
656
                   (
657
                       $nextSibling instanceof \DOMElement
658
                       &&
659
                       (
660
                           $nextSibling->tagName == 'option'
661
                           ||
662 22
                           $nextSibling->tagName == 'optgroup'
663
                       )
664
                   )
665
               )
666
           )
667
           ||
668
           (
669 22
               $tag_name == 'p'
670
               &&
671
               (
672
                   (
673 9
                       $nextSibling === null
674
                       &&
675
                       (
676 9
                           $node->parentNode !== null
677
                           &&
678 9
                           !\in_array(
679 9
                               $node->parentNode->tagName,
680
                               [
681 9
                                   'a',
682
                                   'audio',
683
                                   'del',
684
                                   'ins',
685
                                   'map',
686
                                   'noscript',
687
                                   'video',
688
                               ],
689 9
                               true
690
                           )
691
                       )
692
                   )
693
                   ||
694
                   (
695 5
                       $nextSibling instanceof \DOMElement
696
                       &&
697 5
                       \in_array(
698 5
                           $nextSibling->tagName,
699
                           [
700 5
                               'address',
701
                               'article',
702
                               'aside',
703
                               'blockquote',
704
                               'dir',
705
                               'div',
706
                               'dl',
707
                               'fieldset',
708
                               'footer',
709
                               'form',
710
                               'h1',
711
                               'h2',
712
                               'h3',
713
                               'h4',
714
                               'h5',
715
                               'h6',
716
                               'header',
717
                               'hgroup',
718
                               'hr',
719
                               'menu',
720
                               'nav',
721
                               'ol',
722
                               'p',
723
                               'pre',
724
                               'section',
725
                               'table',
726
                               'ul',
727
                           ],
728 25
                           true
729
                       )
730
                   )
731
               )
732
           );
733
  }
734
735 26
  protected function domNodeToString(\DOMNode $node): string
736
  {
737
    // init
738 26
    $html = '';
739 26
    $emptyStringTmp = '';
740
741 26
    foreach ($node->childNodes as $child) {
742
743 26
      if ($emptyStringTmp === 'is_empty') {
744 17
        $emptyStringTmp = 'last_was_empty';
745
      } else {
746 26
        $emptyStringTmp = '';
747
      }
748
749 26
      if ($child instanceof \DOMDocumentType) {
750
751
        // add the doc-type only if it wasn't generated by DomDocument
752 12
        if ($this->withDocType !== true) {
753 4
          continue;
754
        }
755
756 8
        if ($child->name) {
757
758 8
          if (!$child->publicId && $child->systemId) {
759
            $tmpTypeSystem = 'SYSTEM';
760
            $tmpTypePublic = '';
761
          } else {
762 8
            $tmpTypeSystem = '';
763 8
            $tmpTypePublic = 'PUBLIC';
764
          }
765
766 8
          $html .= '<!DOCTYPE ' . $child->name . ''
767 8
                   . ($child->publicId ? ' ' . $tmpTypePublic . ' "' . $child->publicId . '"' : '')
768 8
                   . ($child->systemId ? ' ' . $tmpTypeSystem . ' "' . $child->systemId . '"' : '')
769 8
                   . '>';
770
        }
771
772 26
      } elseif ($child instanceof \DOMElement) {
773
774 26
        $html .= trim('<' . $child->tagName . ' ' . $this->domNodeAttributesToString($child));
775 26
        $html .= '>' . $this->domNodeToString($child);
776
777
        if (
778 26
            $this->doRemoveOmittedHtmlTags === false
779
            ||
780 26
            !$this->domNodeClosingTagOptional($child)
781
        ) {
782 20
          $html .= '</' . $child->tagName . '>';
783
        }
784
785 26
        if ($this->doRemoveWhitespaceAroundTags === false) {
786
          if (
787 25
              $child->nextSibling instanceof \DOMText
788
              &&
789 25
              $child->nextSibling->wholeText === ' '
790
          ) {
791 16
            if ($emptyStringTmp !== 'last_was_empty') {
792 16
              $html .= ' ';
793
            }
794 26
            $emptyStringTmp = 'is_empty';
795
          }
796
        }
797
798 22
      } elseif ($child instanceof \DOMText) {
799
800 22
        if ($child->isElementContentWhitespace()) {
801
          if (
802 18
              $child->previousSibling !== null
803
              &&
804 18
              $child->nextSibling !== null
805
          ) {
806 9
            if ($emptyStringTmp !== 'last_was_empty') {
807 3
              $html .= ' ';
808
            }
809 18
            $emptyStringTmp = 'is_empty';
810
          }
811
812
        } else {
813
814 22
          $html .= $child->wholeText;
815
816
        }
817
818
      } elseif ($child instanceof \DOMComment) {
819
820 26
        $html .= $child->wholeText;
821
822
      }
823
    }
824
825 26
    return $html;
826
  }
827
828
  /**
829
   * @param \DOMNode $node
830
   *
831
   * @return \DOMNode|null
832
   */
833 25
  protected function getNextSiblingOfTypeDOMElement(\DOMNode $node)
834
  {
835
    do {
836 25
      $node = $node->nextSibling;
837 25
    } while (!($node === null || $node instanceof \DOMElement));
838
839 25
    return $node;
840
  }
841
842
  /**
843
   * Check if the current string is an conditional comment.
844
   *
845
   * INFO: since IE >= 10 conditional comment are not working anymore
846
   *
847
   * <!--[if expression]> HTML <![endif]-->
848
   * <![if expression]> HTML <![endif]>
849
   *
850
   * @param string $comment
851
   *
852
   * @return bool
853
   */
854 3
  private function isConditionalComment($comment): bool
855
  {
856 3
    if (preg_match('/^\[if [^\]]+\]/', $comment)) {
857 2
      return true;
858
    }
859
860 3
    if (preg_match('/\[endif\]$/', $comment)) {
861 1
      return true;
862
    }
863
864 3
    return false;
865
  }
866
867
  /**
868
   * @param string $html
869
   * @param bool   $decodeUtf8Specials <p>Use this only in special cases, e.g. for PHP 5.3</p>
870
   *
871
   * @return string
872
   */
873 30
  public function minify($html, $decodeUtf8Specials = false): string
874
  {
875 30
    $html = (string)$html;
876 30
    if (!isset($html[0])) {
877 1
      return '';
878
    }
879
880 30
    $html = \trim($html);
881 30
    if (!$html) {
882 3
      return '';
883
    }
884
885
    // init
886 27
    static $CACHE_SELF_CLOSING_TAGS = null;
887 27
    if ($CACHE_SELF_CLOSING_TAGS === null) {
888 1
      $CACHE_SELF_CLOSING_TAGS = \implode('|', self::$selfClosingTags);
889
    }
890
891
    // reset
892 27
    $this->protectedChildNodes = [];
893
894
    // save old content
895 27
    $origHtml = $html;
896 27
    $origHtmlLength = UTF8::strlen($html);
897
898
    // -------------------------------------------------------------------------
899
    // Minify the HTML via "HtmlDomParser"
900
    // -------------------------------------------------------------------------
901
902 27
    if ($this->doOptimizeViaHtmlDomParser === true) {
903 26
      $html = $this->minifyHtmlDom($html, $decodeUtf8Specials);
904
    }
905
906
    // -------------------------------------------------------------------------
907
    // Trim whitespace from html-string. [protected html is still protected]
908
    // -------------------------------------------------------------------------
909
910
    // Remove extra white-space(s) between HTML attribute(s)
911 27
    $html = (string)\preg_replace_callback(
912 27
        '#<([^\/\s<>!]+)(?:\s+([^<>]*?)\s*|\s*)(\/?)>#',
913 27
        function ($matches) {
914 27
          return '<' . $matches[1] . (string)\preg_replace('#([^\s=]+)(\=([\'"]?)(.*?)\3)?(\s+|$)#s', ' $1$2', $matches[2]) . $matches[3] . '>';
915 27
        },
916 27
        $html
917
    );
918
919 27
    if ($this->doRemoveSpacesBetweenTags === true) {
920
      // Remove spaces that are between > and <
921
      $html = (string)\preg_replace('/(>) (<)/', '>$2', $html);
922
    }
923
924
    // -------------------------------------------------------------------------
925
    // Restore protected HTML-code.
926
    // -------------------------------------------------------------------------
927
928 27
    $html = (string)\preg_replace_callback(
929 27
        '/<(?<element>' . $this->protectedChildNodesHelper . ')(?<attributes> [^>]*)?>(?<value>.*?)<\/' . $this->protectedChildNodesHelper . '>/',
930 27
        [$this, 'restoreProtectedHtml'],
931 27
        $html
932
    );
933
934
    // -------------------------------------------------------------------------
935
    // Restore protected HTML-entities.
936
    // -------------------------------------------------------------------------
937
938 27
    if ($this->doOptimizeViaHtmlDomParser === true) {
939 26
      $html = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($html);
940
    }
941
942
    // ------------------------------------
943
    // Final clean-up
944
    // ------------------------------------
945
946 27
    $html = UTF8::cleanup($html);
947
948 27
    $html = \str_replace(
949
        [
950 27
            'html>' . "\n",
951
            "\n" . '<html',
952
            'html/>' . "\n",
953
            "\n" . '</html',
954
            'head>' . "\n",
955
            "\n" . '<head',
956
            'head/>' . "\n",
957
            "\n" . '</head',
958
        ],
959
        [
960 27
            'html>',
961
            '<html',
962
            'html/>',
963
            '</html',
964
            'head>',
965
            '<head',
966
            'head/>',
967
            '</head',
968
        ],
969 27
        $html
970
    );
971
972
    // self closing tags, don't need a trailing slash ...
973 27
    $replace = [];
974 27
    $replacement = [];
975 27
    foreach (self::$selfClosingTags as $selfClosingTag) {
976 27
      $replace[] = '<' . $selfClosingTag . '/>';
977 27
      $replacement[] = '<' . $selfClosingTag . '>';
978 27
      $replace[] = '<' . $selfClosingTag . ' />';
979 27
      $replacement[] = '<' . $selfClosingTag . '>';
980
    }
981 27
    $html = \str_replace(
982 27
        $replace,
983 27
        $replacement,
984 27
        $html
985
    );
986
987 27
    $html = (string)\preg_replace('#<\b(' . $CACHE_SELF_CLOSING_TAGS . ')([^>]+)><\/\b\1>#', '<\\1\\2>', $html);
988
989
    // ------------------------------------
990
    // check if compression worked
991
    // ------------------------------------
992
993 27
    if ($origHtmlLength < UTF8::strlen($html)) {
994 2
      $html = $origHtml;
995
    }
996
997 27
    return $html;
998
  }
999
1000
  /**
1001
   * @param $html
1002
   * @param $decodeUtf8Specials
1003
   *
1004
   * @return string
1005
   */
1006 26
  private function minifyHtmlDom($html, $decodeUtf8Specials): string
1007
  {
1008
    // init dom
1009 26
    $dom = new HtmlDomParser();
1010 26
    $dom->getDocument()->preserveWhiteSpace = false; // remove redundant white space
1011 26
    $dom->getDocument()->formatOutput = false; // do not formats output with indentation
1012
1013
    // load dom
1014 26
    $dom->loadHtml($html);
1015
1016 26
    $this->withDocType = (stripos(trim($html), '<!DOCTYPE') === 0);
1017
1018
    // -------------------------------------------------------------------------
1019
    // Protect HTML tags and conditional comments.
1020
    // -------------------------------------------------------------------------
1021
1022 26
    $dom = $this->protectTags($dom);
1023
1024
    // -------------------------------------------------------------------------
1025
    // Remove default HTML comments. [protected html is still protected]
1026
    // -------------------------------------------------------------------------
1027
1028 26
    if ($this->doRemoveComments === true) {
1029 25
      $dom = $this->removeComments($dom);
1030
    }
1031
1032
    // -------------------------------------------------------------------------
1033
    // Sum-Up extra whitespace from the Dom. [protected html is still protected]
1034
    // -------------------------------------------------------------------------
1035
1036 26
    if ($this->doSumUpWhitespace === true) {
1037 25
      $dom = $this->sumUpWhitespace($dom);
1038
    }
1039
1040 26
    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...
1041
1042
      // -------------------------------------------------------------------------
1043
      // Optimize html attributes. [protected html is still protected]
1044
      // -------------------------------------------------------------------------
1045
1046 26
      if ($this->doOptimizeAttributes === true) {
1047 25
        $this->optimizeAttributes($element);
1048
      }
1049
1050
      // -------------------------------------------------------------------------
1051
      // Remove whitespace around tags. [protected html is still protected]
1052
      // -------------------------------------------------------------------------
1053
1054 26
      if ($this->doRemoveWhitespaceAroundTags === true) {
1055 26
        $this->removeWhitespaceAroundTags($element);
1056
      }
1057
    }
1058
1059
    // -------------------------------------------------------------------------
1060
    // Convert the Dom into a string.
1061
    // -------------------------------------------------------------------------
1062
1063 26
    $html = $dom->fixHtmlOutput(
1064 26
        $this->domNodeToString($dom->getDocument()),
1065 26
        $decodeUtf8Specials
1066
    );
1067
1068 26
    return $html;
1069
  }
1070
1071
  /**
1072
   * Sort HTML-Attributes, so that gzip can do better work and remove some default attributes...
1073
   *
1074
   * @param SimpleHtmlDom $element
1075
   *
1076
   * @return bool
1077
   */
1078 25
  private function optimizeAttributes(SimpleHtmlDom $element): bool
1079
  {
1080 25
    $attributes = $element->getAllAttributes();
1081 25
    if ($attributes === null) {
1082 25
      return false;
1083
    }
1084
1085 14
    $attrs = [];
1086 14
    foreach ((array)$attributes as $attrName => $attrValue) {
1087
1088
      // -------------------------------------------------------------------------
1089
      // Remove optional "http:"-prefix from attributes.
1090
      // -------------------------------------------------------------------------
1091
1092 14
      if ($this->doRemoveHttpPrefixFromAttributes === true) {
1093
        if (
1094 3
            ($attrName === 'href' || $attrName === 'src' || $attrName === 'action')
1095
            &&
1096 3
            !(isset($attributes['rel']) && $attributes['rel'] === 'external')
1097
            &&
1098 3
            !(isset($attributes['target']) && $attributes['target'] === '_blank')
1099
        ) {
1100 2
          $attrValue = \str_replace('http://', '//', $attrValue);
1101
        }
1102
      }
1103
1104 14
      if ($this->removeAttributeHelper($element->tag, $attrName, $attrValue, $attributes)) {
1105 3
        $element->{$attrName} = null;
1106 3
        continue;
1107
      }
1108
1109
      // -------------------------------------------------------------------------
1110
      // Sort css-class-names, for better gzip results.
1111
      // -------------------------------------------------------------------------
1112
1113 14
      if ($this->doSortCssClassNames === true) {
1114 14
        $attrValue = $this->sortCssClassNames($attrName, $attrValue);
1115
      }
1116
1117 14
      if ($this->doSortHtmlAttributes === true) {
1118 14
        $attrs[$attrName] = $attrValue;
1119 14
        $element->{$attrName} = null;
1120
      }
1121
    }
1122
1123
    // -------------------------------------------------------------------------
1124
    // Sort html-attributes, for better gzip results.
1125
    // -------------------------------------------------------------------------
1126
1127 14
    if ($this->doSortHtmlAttributes === true) {
1128 14
      \ksort($attrs);
1129 14
      foreach ($attrs as $attrName => $attrValue) {
1130 14
        $attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue);
1131 14
        $element->setAttribute($attrName, $attrValue, true);
1132
      }
1133
    }
1134
1135 14
    return true;
1136
  }
1137
1138
  /**
1139
   * Prevent changes of inline "styles" and "scripts".
1140
   *
1141
   * @param HtmlDomParser $dom
1142
   *
1143
   * @return HtmlDomParser
1144
   */
1145 26
  private function protectTags(HtmlDomParser $dom): HtmlDomParser
1146
  {
1147
    // init
1148 26
    $counter = 0;
1149
1150 26
    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...
1151
1152
      // skip external links
1153 3
      if ($element->tag === 'script' || $element->tag === 'style') {
1154 3
        $attributes = $element->getAllAttributes();
1155 3
        if (isset($attributes['src'])) {
1156 2
          continue;
1157
        }
1158
      }
1159
1160 2
      $this->protectedChildNodes[$counter] = $element->text();
1161 2
      $element->getNode()->nodeValue = '<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $counter . '"></' . $this->protectedChildNodesHelper . '>';
1162
1163 2
      ++$counter;
1164
    }
1165
1166 26
    $dom->getDocument()->normalizeDocument();
1167
1168 26
    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...
1169 3
      $text = $element->text();
1170
1171
      // skip normal comments
1172 3
      if ($this->isConditionalComment($text) === false) {
1173 3
        continue;
1174
      }
1175
1176 2
      $this->protectedChildNodes[$counter] = '<!--' . $text . '-->';
1177
1178
      /* @var $node \DOMComment */
1179 2
      $node = $element->getNode();
1180 2
      $child = new \DOMText('<' . $this->protectedChildNodesHelper . ' data-' . $this->protectedChildNodesHelper . '="' . $counter . '"></' . $this->protectedChildNodesHelper . '>');
1181 2
      $element->getNode()->parentNode->replaceChild($child, $node);
1182
1183 2
      ++$counter;
1184
    }
1185
1186 26
    $dom->getDocument()->normalizeDocument();
1187
1188 26
    return $dom;
1189
  }
1190
1191
  /**
1192
   * Check if the attribute can be removed.
1193
   *
1194
   * @param string $tag
1195
   * @param string $attrName
1196
   * @param string $attrValue
1197
   * @param array  $allAttr
1198
   *
1199
   * @return bool
1200
   */
1201 14
  private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr): bool
1202
  {
1203
    // remove defaults
1204 14
    if ($this->doRemoveDefaultAttributes === true) {
1205
1206 1
      if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') {
1207
        return true;
1208
      }
1209
1210 1
      if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') {
1211
        return true;
1212
      }
1213
1214 1
      if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') {
1215
        return true;
1216
      }
1217
1218 1
      if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') {
1219
        return true;
1220
      }
1221
    }
1222
1223
    // remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway)
1224 14 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...
1225 14
      if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) {
1226
        return true;
1227
      }
1228
    }
1229
1230
    // remove deprecated anchor-jump
1231 14 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...
1232 14
      if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) {
1233
        return true;
1234
      }
1235
    }
1236
1237
    // remove "type=text/css" for css links
1238 14 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...
1239 14
      if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet') {
1240 1
        return true;
1241
      }
1242
    }
1243
1244
    // remove deprecated script-mime-types
1245 14 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...
1246 14
      if ($tag === 'script' && $attrName === 'type' && isset($allAttr['src'], self::$executableScriptsMimeTypes[$attrValue])) {
1247 1
        return true;
1248
      }
1249
    }
1250
1251
    // remove 'value=""' from <input type="text">
1252 14
    if ($this->doRemoveValueFromEmptyInput === true) {
1253 14
      if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') {
1254 1
        return true;
1255
      }
1256
    }
1257
1258
    // remove some empty attributes
1259 14
    if ($this->doRemoveEmptyAttributes === true) {
1260 14
      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)) {
1261 2
        return true;
1262
      }
1263
    }
1264
1265 14
    return false;
1266
  }
1267
1268
  /**
1269
   * Remove comments in the dom.
1270
   *
1271
   * @param HtmlDomParser $dom
1272
   *
1273
   * @return HtmlDomParser
1274
   */
1275 25
  private function removeComments(HtmlDomParser $dom): HtmlDomParser
1276
  {
1277 25
    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...
1278 3
      $comment = $commentWrapper->getNode();
1279 3
      $val = $comment->nodeValue;
1280 3
      if (\strpos($val, '[') === false) {
1281 3
        $comment->parentNode->removeChild($comment);
1282
      }
1283
    }
1284
1285 25
    $dom->getDocument()->normalizeDocument();
1286
1287 25
    return $dom;
1288
  }
1289
1290
  /**
1291
   * Trim tags in the dom.
1292
   *
1293
   * @param SimpleHtmlDom $element
1294
   *
1295
   * @return void
1296
   */
1297 3
  private function removeWhitespaceAroundTags(SimpleHtmlDom $element)
1298
  {
1299 3
    if (isset(self::$trimWhitespaceFromTags[$element->tag])) {
1300 1
      $node = $element->getNode();
1301
1302 1
      $candidates = [];
1303 1
      if ($node->childNodes->length > 0) {
1304 1
        $candidates[] = $node->firstChild;
1305 1
        $candidates[] = $node->lastChild;
1306 1
        $candidates[] = $node->previousSibling;
1307 1
        $candidates[] = $node->nextSibling;
1308
      }
1309
1310 1
      foreach ($candidates as &$candidate) {
1311 1
        if ($candidate === null) {
1312
          continue;
1313
        }
1314
1315 1
        if ($candidate->nodeType === 3) {
1316 1
          $candidate->nodeValue = \preg_replace(self::$regExSpace, ' ', $candidate->nodeValue);
1317
        }
1318
      }
1319
    }
1320 3
  }
1321
1322
  /**
1323
   * Callback function for preg_replace_callback use.
1324
   *
1325
   * @param array $matches PREG matches
1326
   *
1327
   * @return string
1328
   */
1329 2
  private function restoreProtectedHtml($matches): string
1330
  {
1331 2
    \preg_match('/.*"(?<id>\d*)"/', $matches['attributes'], $matchesInner);
1332
1333 2
    $html = '';
1334 2
    if (isset($this->protectedChildNodes[$matchesInner['id']])) {
1335 2
      $html .= $this->protectedChildNodes[$matchesInner['id']];
1336
    }
1337
1338 2
    return $html;
1339
  }
1340
1341
  /**
1342
   * @param array $domainsToRemoveHttpPrefixFromAttributes
1343
   *
1344
   * @return $this
1345
   */
1346 2
  public function setDomainsToRemoveHttpPrefixFromAttributes($domainsToRemoveHttpPrefixFromAttributes)
1347
  {
1348 2
    $this->domainsToRemoveHttpPrefixFromAttributes = $domainsToRemoveHttpPrefixFromAttributes;
1349
1350 2
    return $this;
1351
  }
1352
1353
  /**
1354
   * @param $attrName
1355
   * @param $attrValue
1356
   *
1357
   * @return string
1358
   */
1359 14
  private function sortCssClassNames($attrName, $attrValue): string
1360
  {
1361 14
    if ($attrName !== 'class' || !$attrValue) {
1362 13
      return $attrValue;
1363
    }
1364
1365 8
    $classes = \array_unique(
1366 8
        \explode(' ', $attrValue)
1367
    );
1368 8
    \sort($classes);
1369
1370 8
    $attrValue = '';
1371 8
    foreach ($classes as $class) {
1372
1373 8
      if (!$class) {
1374 2
        continue;
1375
      }
1376
1377 8
      $attrValue .= \trim($class) . ' ';
1378
    }
1379 8
    $attrValue = \trim($attrValue);
1380
1381 8
    return $attrValue;
1382
  }
1383
1384
  /**
1385
   * Sum-up extra whitespace from dom-nodes.
1386
   *
1387
   * @param HtmlDomParser $dom
1388
   *
1389
   * @return HtmlDomParser
1390
   */
1391 25
  private function sumUpWhitespace(HtmlDomParser $dom): HtmlDomParser
1392
  {
1393 25
    $textnodes = $dom->find('//text()');
1394 25
    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...
1395
      /* @var $textnode \DOMNode */
1396 21
      $textnode = $textnodeWrapper->getNode();
1397 21
      $xp = $textnode->getNodePath();
1398
1399 21
      $doSkip = false;
1400 21
      foreach (self::$skipTagsForRemoveWhitespace as $pattern) {
1401 21
        if (\strpos($xp, "/$pattern") !== false) {
1402 3
          $doSkip = true;
1403 21
          break;
1404
        }
1405
      }
1406 21
      if ($doSkip) {
1407 3
        continue;
1408
      }
1409
1410 21
      $textnode->nodeValue = \preg_replace(self::$regExSpace, ' ', $textnode->nodeValue);
1411
    }
1412
1413 25
    $dom->getDocument()->normalizeDocument();
1414
1415 25
    return $dom;
1416
  }
1417
}
1418