1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace voku\helper; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* HtmlMinDomObserverOptimizeAttributes: Optimize html attributes. [protected html is still protected] |
9
|
|
|
* |
10
|
|
|
* Sort HTML-Attributes, so that gzip can do better work and remove some default attributes... |
11
|
|
|
*/ |
12
|
|
|
final class HtmlMinDomObserverOptimizeAttributes implements HtmlMinDomObserverInterface |
13
|
|
|
{ |
14
|
|
|
/** |
15
|
|
|
* // https://mathiasbynens.be/demo/javascript-mime-type |
16
|
|
|
* // https://developer.mozilla.org/en/docs/Web/HTML/Element/script#attr-type |
17
|
|
|
* |
18
|
|
|
* @var array |
19
|
|
|
*/ |
20
|
|
|
private static $executableScriptsMimeTypes = [ |
21
|
|
|
'text/javascript' => '', |
22
|
|
|
'text/ecmascript' => '', |
23
|
|
|
'text/jscript' => '', |
24
|
|
|
'application/javascript' => '', |
25
|
|
|
'application/x-javascript' => '', |
26
|
|
|
'application/ecmascript' => '', |
27
|
|
|
]; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Receive dom elements before the minification. |
31
|
|
|
* |
32
|
|
|
* @param SimpleHtmlDom $element |
33
|
|
|
* @param HtmlMin $htmlMin |
34
|
|
|
*/ |
35
|
33 |
|
public function domElementBeforeMinification(SimpleHtmlDom $element, HtmlMin $htmlMin) { |
36
|
|
|
|
37
|
33 |
|
} |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Receive dom elements after the minification. |
41
|
|
|
* |
42
|
|
|
* @param SimpleHtmlDom $element |
43
|
|
|
* @param HtmlMin $htmlMin |
44
|
|
|
*/ |
45
|
33 |
|
public function domElementAfterMinification(SimpleHtmlDom $element, HtmlMin $htmlMin) { |
46
|
33 |
|
$attributes = $element->getAllAttributes(); |
47
|
33 |
|
if ($attributes === null) { |
48
|
33 |
|
return; |
49
|
|
|
} |
50
|
|
|
|
51
|
20 |
|
$attrs = []; |
52
|
20 |
|
foreach ((array) $attributes as $attrName => $attrValue) { |
53
|
|
|
|
54
|
|
|
// ------------------------------------------------------------------------- |
55
|
|
|
// Remove optional "http:"-prefix from attributes. |
56
|
|
|
// ------------------------------------------------------------------------- |
57
|
|
|
|
58
|
20 |
|
if ($htmlMin->isDoRemoveHttpPrefixFromAttributes() === true) { |
59
|
|
|
if ( |
60
|
3 |
|
($attrName === 'href' || $attrName === 'src' || $attrName === 'action') |
61
|
|
|
&& |
62
|
3 |
|
!(isset($attributes['rel']) && $attributes['rel'] === 'external') |
63
|
|
|
&& |
64
|
3 |
|
!(isset($attributes['target']) && $attributes['target'] === '_blank') |
65
|
|
|
) { |
66
|
2 |
|
$attrValue = \str_replace('http://', '//', $attrValue); |
67
|
|
|
} |
68
|
|
|
} |
69
|
|
|
|
70
|
20 |
|
if ($this->removeAttributeHelper($element->tag, $attrName, $attrValue, $attributes, $htmlMin)) { |
71
|
4 |
|
$element->{$attrName} = null; |
72
|
|
|
|
73
|
4 |
|
continue; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
// ------------------------------------------------------------------------- |
77
|
|
|
// Sort css-class-names, for better gzip results. |
78
|
|
|
// ------------------------------------------------------------------------- |
79
|
|
|
|
80
|
20 |
|
if ($htmlMin->isDoSortCssClassNames() === true) { |
81
|
19 |
|
$attrValue = $this->sortCssClassNames($attrName, $attrValue); |
82
|
|
|
} |
83
|
|
|
|
84
|
20 |
|
if ($htmlMin->isDoSortHtmlAttributes() === true) { |
85
|
19 |
|
$attrs[$attrName] = $attrValue; |
86
|
20 |
|
$element->{$attrName} = null; |
87
|
|
|
} |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
// ------------------------------------------------------------------------- |
91
|
|
|
// Sort html-attributes, for better gzip results. |
92
|
|
|
// ------------------------------------------------------------------------- |
93
|
|
|
|
94
|
20 |
|
if ($htmlMin->isDoSortHtmlAttributes() === true) { |
95
|
19 |
|
\ksort($attrs); |
96
|
19 |
|
foreach ($attrs as $attrName => $attrValue) { |
97
|
19 |
|
$attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue); |
98
|
19 |
|
$element->setAttribute($attrName, $attrValue, true); |
99
|
|
|
} |
100
|
|
|
} |
101
|
20 |
|
} |
102
|
|
|
|
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Check if the attribute can be removed. |
106
|
|
|
* |
107
|
|
|
* @param string $tag |
108
|
|
|
* @param string $attrName |
109
|
|
|
* @param string $attrValue |
110
|
|
|
* @param array $allAttr |
111
|
|
|
* @param HtmlMin $htmlMin |
112
|
|
|
* |
113
|
|
|
* @return bool |
114
|
|
|
*/ |
115
|
20 |
|
private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr, HtmlMin $htmlMin): bool |
116
|
|
|
{ |
117
|
|
|
// remove defaults |
118
|
20 |
|
if ($htmlMin->isDoRemoveDefaultAttributes() === true) { |
119
|
1 |
|
if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') { |
120
|
|
|
return true; |
121
|
|
|
} |
122
|
|
|
|
123
|
1 |
|
if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') { |
124
|
|
|
return true; |
125
|
|
|
} |
126
|
|
|
|
127
|
1 |
|
if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') { |
128
|
|
|
return true; |
129
|
|
|
} |
130
|
|
|
|
131
|
1 |
|
if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') { |
132
|
|
|
return true; |
133
|
|
|
} |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
// remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway) |
137
|
20 |
|
if ($htmlMin->isDoRemoveDeprecatedScriptCharsetAttribute() === true) { |
138
|
19 |
|
if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) { |
139
|
|
|
return true; |
140
|
|
|
} |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
// remove deprecated anchor-jump |
144
|
20 |
|
if ($htmlMin->isDoRemoveDeprecatedAnchorName() === true) { |
145
|
19 |
|
if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) { |
146
|
|
|
return true; |
147
|
|
|
} |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
// remove "type=text/css" for css links |
151
|
20 |
View Code Duplication |
if ($htmlMin->isDoRemoveDeprecatedTypeFromStylesheetLink() === true) { |
|
|
|
|
152
|
19 |
|
if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet') { |
153
|
1 |
|
return true; |
154
|
|
|
} |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
// remove deprecated script-mime-types |
158
|
20 |
|
if ($htmlMin->isDoRemoveDeprecatedTypeFromScriptTag() === true) { |
159
|
19 |
|
if ($tag === 'script' && $attrName === 'type' && isset($allAttr['src'], self::$executableScriptsMimeTypes[$attrValue])) { |
160
|
1 |
|
return true; |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
// remove 'value=""' from <input type="text"> |
165
|
20 |
View Code Duplication |
if ($htmlMin->isDoRemoveValueFromEmptyInput() === true) { |
|
|
|
|
166
|
19 |
|
if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') { |
167
|
1 |
|
return true; |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
// remove some empty attributes |
172
|
20 |
|
if ($htmlMin->isDoRemoveEmptyAttributes() === true) { |
173
|
19 |
|
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)) { |
174
|
3 |
|
return true; |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
|
178
|
20 |
|
return false; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* @param $attrName |
184
|
|
|
* @param $attrValue |
185
|
|
|
* |
186
|
|
|
* @return string |
187
|
|
|
*/ |
188
|
19 |
|
private function sortCssClassNames($attrName, $attrValue): string |
189
|
|
|
{ |
190
|
19 |
|
if ($attrName !== 'class' || !$attrValue) { |
191
|
16 |
|
return $attrValue; |
192
|
|
|
} |
193
|
|
|
|
194
|
10 |
|
$classes = \array_unique( |
195
|
10 |
|
\explode(' ', $attrValue) |
196
|
|
|
); |
197
|
10 |
|
\sort($classes); |
198
|
|
|
|
199
|
10 |
|
$attrValue = ''; |
200
|
10 |
|
foreach ($classes as $class) { |
201
|
10 |
|
if (!$class) { |
202
|
3 |
|
continue; |
203
|
|
|
} |
204
|
|
|
|
205
|
10 |
|
$attrValue .= \trim($class) . ' '; |
206
|
|
|
} |
207
|
|
|
|
208
|
10 |
|
return \trim($attrValue); |
209
|
|
|
} |
210
|
|
|
} |
211
|
|
|
|
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.