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 string[] |
19
|
|
|
* |
20
|
|
|
* @psalm-var array<string, string> |
21
|
|
|
*/ |
22
|
|
|
private static $executableScriptsMimeTypes = [ |
23
|
|
|
'text/javascript' => '', |
24
|
|
|
'text/ecmascript' => '', |
25
|
|
|
'text/jscript' => '', |
26
|
|
|
'application/javascript' => '', |
27
|
|
|
'application/x-javascript' => '', |
28
|
|
|
'application/ecmascript' => '', |
29
|
|
|
]; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Receive dom elements before the minification. |
33
|
|
|
* |
34
|
|
|
* @param SimpleHtmlDomInterface $element |
35
|
|
|
* @param HtmlMinInterface $htmlMin |
36
|
|
|
* |
37
|
|
|
* @return void |
38
|
|
|
*/ |
39
|
51 |
|
public function domElementBeforeMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin) |
40
|
|
|
{ |
41
|
51 |
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Receive dom elements after the minification. |
45
|
|
|
* |
46
|
|
|
* @param SimpleHtmlDomInterface $element |
47
|
|
|
* @param HtmlMinInterface $htmlMin |
48
|
|
|
* |
49
|
|
|
* @return void |
50
|
|
|
*/ |
51
|
51 |
|
public function domElementAfterMinification(SimpleHtmlDomInterface $element, HtmlMinInterface $htmlMin) |
52
|
|
|
{ |
53
|
51 |
|
$attributes = $element->getAllAttributes(); |
54
|
51 |
|
if ($attributes === null) { |
55
|
48 |
|
return; |
56
|
|
|
} |
57
|
|
|
|
58
|
34 |
|
$attrs = []; |
59
|
34 |
|
foreach ((array) $attributes as $attrName => $attrValue) { |
60
|
|
|
|
61
|
|
|
// ------------------------------------------------------------------------- |
62
|
|
|
// Remove optional "http:"-prefix from attributes. |
63
|
|
|
// ------------------------------------------------------------------------- |
64
|
|
|
|
65
|
34 |
|
if ($htmlMin->isDoRemoveHttpPrefixFromAttributes()) { |
66
|
5 |
|
$attrValue = $this->removeHttpPrefixHelper( |
67
|
5 |
|
$attrValue, |
68
|
|
|
$attrName, |
69
|
5 |
|
'http', |
70
|
|
|
$attributes, |
71
|
|
|
$htmlMin |
72
|
|
|
); |
73
|
|
|
} |
74
|
|
|
|
75
|
34 |
|
if ($htmlMin->isDoRemoveHttpsPrefixFromAttributes()) { |
76
|
1 |
|
$attrValue = $this->removeHttpPrefixHelper( |
77
|
1 |
|
$attrValue, |
78
|
|
|
$attrName, |
79
|
1 |
|
'https', |
80
|
|
|
$attributes, |
81
|
|
|
$htmlMin |
82
|
|
|
); |
83
|
|
|
} |
84
|
|
|
|
85
|
34 |
|
if ($htmlMin->isDoMakeSameDomainLinksRelative()) { |
86
|
2 |
|
if (!$htmlMin->isLocalDomainSet()){ |
87
|
|
|
$htmlMin->setLocalDomain(); |
88
|
|
|
} |
89
|
2 |
|
$localDomain = $htmlMin->getLocalDomain(); |
90
|
|
View Code Duplication |
if ( |
|
|
|
|
91
|
2 |
|
(($attrName === 'href') || $attrName === 'src' || $attrName === 'srcset' || $attrName === 'action') |
92
|
|
|
&& |
93
|
2 |
|
!(isset($attributes['rel']) && $attributes['rel'] === 'external') |
94
|
|
|
&& |
95
|
2 |
|
!(isset($attributes['target']) && $attributes['target'] === '_blank') |
96
|
|
|
) { |
97
|
2 |
|
$attrValue = \preg_replace("/^((https?:)?\/\/)?{$localDomain}(?!\w)(\/?)/", '/', $attrValue); |
98
|
|
|
} |
99
|
|
|
} |
100
|
|
|
|
101
|
34 |
|
if ($this->removeAttributeHelper($element->tag, $attrName, $attrValue, $attributes, $htmlMin)) { |
|
|
|
|
102
|
6 |
|
$element->{$attrName} = null; |
103
|
|
|
|
104
|
6 |
|
continue; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
// ------------------------------------------------------------------------- |
108
|
|
|
// Sort css-class-names, for better gzip results. |
109
|
|
|
// ------------------------------------------------------------------------- |
110
|
|
|
|
111
|
34 |
|
if ($htmlMin->isDoSortCssClassNames()) { |
112
|
33 |
|
$attrValue = $this->sortCssClassNames($attrName, $attrValue); |
113
|
|
|
} |
114
|
|
|
|
115
|
34 |
|
if ($htmlMin->isDoSortHtmlAttributes()) { |
116
|
33 |
|
$attrs[$attrName] = $attrValue; |
117
|
33 |
|
$element->{$attrName} = null; |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
// ------------------------------------------------------------------------- |
122
|
|
|
// Sort html-attributes, for better gzip results. |
123
|
|
|
// ------------------------------------------------------------------------- |
124
|
|
|
|
125
|
34 |
|
if ($htmlMin->isDoSortHtmlAttributes()) { |
126
|
33 |
|
\ksort($attrs); |
127
|
33 |
|
foreach ($attrs as $attrName => $attrValue) { |
128
|
33 |
|
$attrValue = HtmlDomParser::replaceToPreserveHtmlEntities($attrValue); |
129
|
33 |
|
$element->setAttribute((string) $attrName, $attrValue, true); |
130
|
|
|
} |
131
|
|
|
} |
132
|
34 |
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Check if the attribute can be removed. |
136
|
|
|
* |
137
|
|
|
* @param string $tag |
138
|
|
|
* @param string $attrName |
139
|
|
|
* @param string $attrValue |
140
|
|
|
* @param array $allAttr |
141
|
|
|
* @param HtmlMinInterface $htmlMin |
142
|
|
|
* |
143
|
|
|
* @return bool |
144
|
|
|
*/ |
145
|
34 |
|
private function removeAttributeHelper($tag, $attrName, $attrValue, $allAttr, HtmlMinInterface $htmlMin): bool |
146
|
|
|
{ |
147
|
|
|
// remove defaults |
148
|
34 |
|
if ($htmlMin->isDoRemoveDefaultAttributes()) { |
149
|
1 |
|
if ($tag === 'script' && $attrName === 'language' && $attrValue === 'javascript') { |
150
|
|
|
return true; |
151
|
|
|
} |
152
|
|
|
|
153
|
1 |
|
if ($tag === 'form' && $attrName === 'method' && $attrValue === 'get') { |
154
|
|
|
return true; |
155
|
|
|
} |
156
|
|
|
|
157
|
1 |
|
if ($tag === 'input' && $attrName === 'type' && $attrValue === 'text') { |
158
|
|
|
return true; |
159
|
|
|
} |
160
|
|
|
|
161
|
1 |
|
if ($tag === 'area' && $attrName === 'shape' && $attrValue === 'rect') { |
162
|
|
|
return true; |
163
|
|
|
} |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
// remove deprecated charset-attribute (the browser will use the charset from the HTTP-Header, anyway) |
167
|
34 |
|
if ($htmlMin->isDoRemoveDeprecatedScriptCharsetAttribute()) { |
168
|
|
|
/** @noinspection NestedPositiveIfStatementsInspection */ |
169
|
33 |
|
if ($tag === 'script' && $attrName === 'charset' && !isset($allAttr['src'])) { |
170
|
|
|
return true; |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
// remove deprecated anchor-jump |
175
|
34 |
View Code Duplication |
if ($htmlMin->isDoRemoveDeprecatedAnchorName()) { |
|
|
|
|
176
|
|
|
/** @noinspection NestedPositiveIfStatementsInspection */ |
177
|
33 |
|
if ($tag === 'a' && $attrName === 'name' && isset($allAttr['id']) && $allAttr['id'] === $attrValue) { |
178
|
|
|
return true; |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
// remove "type=text/css" for css links |
183
|
34 |
View Code Duplication |
if ($htmlMin->isDoRemoveDeprecatedTypeFromStylesheetLink()) { |
|
|
|
|
184
|
|
|
/** @noinspection NestedPositiveIfStatementsInspection */ |
185
|
33 |
|
if ($tag === 'link' && $attrName === 'type' && $attrValue === 'text/css' && isset($allAttr['rel']) && $allAttr['rel'] === 'stylesheet') { |
186
|
1 |
|
return true; |
187
|
|
|
} |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
// remove deprecated script-mime-types |
191
|
34 |
View Code Duplication |
if ($htmlMin->isDoRemoveDeprecatedTypeFromScriptTag()) { |
|
|
|
|
192
|
|
|
/** @noinspection NestedPositiveIfStatementsInspection */ |
193
|
33 |
|
if ($tag === 'script' && $attrName === 'type' && isset($allAttr['src'], self::$executableScriptsMimeTypes[$attrValue])) { |
194
|
1 |
|
return true; |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
// remove 'value=""' from <input type="text"> |
199
|
34 |
View Code Duplication |
if ($htmlMin->isDoRemoveValueFromEmptyInput()) { |
|
|
|
|
200
|
|
|
/** @noinspection NestedPositiveIfStatementsInspection */ |
201
|
33 |
|
if ($tag === 'input' && $attrName === 'value' && $attrValue === '' && isset($allAttr['type']) && $allAttr['type'] === 'text') { |
202
|
1 |
|
return true; |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
// remove some empty attributes |
207
|
34 |
|
if ($htmlMin->isDoRemoveEmptyAttributes()) { |
208
|
|
|
/** @noinspection NestedPositiveIfStatementsInspection */ |
209
|
33 |
|
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)) { |
210
|
5 |
|
return true; |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
|
214
|
34 |
|
return false; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* @param string $attrValue |
219
|
|
|
* @param string $attrName |
220
|
|
|
* @param string $scheme |
221
|
|
|
* @param array $attributes |
222
|
|
|
* |
223
|
|
|
* @return string |
224
|
|
|
*/ |
225
|
5 |
|
private function removeHttpPrefixHelper( |
226
|
|
|
string $attrValue, |
227
|
|
|
string $attrName, |
228
|
|
|
string $scheme, |
229
|
|
|
array $attributes, |
230
|
|
|
HtmlMinInterface $htmlMin |
231
|
|
|
): string { |
232
|
|
|
/** @noinspection InArrayCanBeUsedInspection */ |
233
|
|
View Code Duplication |
if ( |
|
|
|
|
234
|
5 |
|
(($attrName === 'href' && !$htmlMin->isKeepPrefixOnExternalAttributes()) || $attrName === 'src' || $attrName === 'srcset' || $attrName === 'action') |
235
|
|
|
&& |
236
|
5 |
|
!(isset($attributes['rel']) && $attributes['rel'] === 'external') |
237
|
|
|
&& |
238
|
5 |
|
!(isset($attributes['target']) && $attributes['target'] === '_blank') |
239
|
|
|
) { |
240
|
4 |
|
$attrValue = \str_replace($scheme . '://', '//', $attrValue); |
241
|
|
|
} |
242
|
|
|
|
243
|
5 |
|
return $attrValue; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* @param string $attrName |
248
|
|
|
* @param string $attrValue |
249
|
|
|
* |
250
|
|
|
* @return string |
251
|
|
|
*/ |
252
|
33 |
|
private function sortCssClassNames($attrName, $attrValue): string |
253
|
|
|
{ |
254
|
33 |
|
if ($attrName !== 'class' || !$attrValue) { |
255
|
28 |
|
return $attrValue; |
256
|
|
|
} |
257
|
|
|
|
258
|
18 |
|
$classes = \array_unique( |
259
|
18 |
|
\explode(' ', $attrValue) |
260
|
|
|
); |
261
|
18 |
|
\sort($classes); |
262
|
|
|
|
263
|
18 |
|
$attrValue = ''; |
264
|
18 |
|
foreach ($classes as $class) { |
265
|
18 |
|
if (!$class) { |
266
|
3 |
|
continue; |
267
|
|
|
} |
268
|
|
|
|
269
|
18 |
|
$attrValue .= \trim($class) . ' '; |
270
|
|
|
} |
271
|
|
|
|
272
|
18 |
|
return \trim($attrValue); |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
|
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.