1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace ZWF; |
4
|
|
|
|
5
|
|
|
use ZWF\GoogleFontsCollection as Collection; |
6
|
|
|
|
7
|
|
|
class GoogleFontsOptimizerUtils |
8
|
|
|
{ |
9
|
|
|
/** |
10
|
|
|
* Replaces any occurences of un-encoded ampersands in the given string |
11
|
|
|
* with the value given in the `$amp` parameter (`&` by default). |
12
|
|
|
* |
13
|
|
|
* @param string $url |
14
|
|
|
* @param string $amp |
15
|
|
|
* |
16
|
|
|
* @return string |
17
|
|
|
*/ |
18
|
7 |
|
public static function encodeUnencodedAmpersands($url, $amp = '&') |
19
|
7 |
|
{ |
20
|
7 |
|
$amp = trim($amp); |
21
|
7 |
|
if (empty($amp)) { |
22
|
3 |
|
$amp = '&'; |
23
|
|
|
} |
24
|
|
|
|
25
|
7 |
|
return preg_replace('/&(?!#?\w+;)/', $amp, $url); |
26
|
|
|
} |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Turns protocol-relative or non-https URLs into their https versions. |
30
|
|
|
* |
31
|
|
|
* @param string $link |
32
|
|
|
* |
33
|
|
|
* @return string |
34
|
|
|
*/ |
35
|
6 |
|
public static function httpsify($link) |
36
|
6 |
|
{ |
37
|
6 |
|
$is_protocol_relative = ('/' === $link{0} && '/' === $link{1}); |
38
|
|
|
|
39
|
6 |
|
if ($is_protocol_relative) { |
40
|
1 |
|
$link = 'https:' . $link; |
41
|
|
|
} else { |
42
|
5 |
|
$link = preg_replace('/^(https?):\/\//mi', '', $link); |
43
|
5 |
|
$link = 'https://' . $link; |
44
|
|
|
} |
45
|
|
|
|
46
|
6 |
|
return $link; |
47
|
|
|
} |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Returns true if a given url is a google font url. |
51
|
|
|
* |
52
|
|
|
* @param string $url |
53
|
|
|
* |
54
|
|
|
* @return bool |
55
|
|
|
*/ |
56
|
12 |
|
public static function isGoogleWebFontUrl($url) |
57
|
12 |
|
{ |
58
|
12 |
|
return (substr_count($url, 'fonts.googleapis.com/css') > 0); |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Returns true if given `$string` contains the HTML5 doctype. |
63
|
|
|
* |
64
|
|
|
* @param string $string |
65
|
|
|
* |
66
|
|
|
* @return bool |
67
|
|
|
*/ |
68
|
2 |
|
public static function hasHtml5Doctype($string) |
69
|
2 |
|
{ |
70
|
2 |
|
return (preg_match('/^<!DOCTYPE.+html>/i', $string) > 0); |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Returns true when given `$string` contains an XSL stylesheet element. |
75
|
|
|
* |
76
|
|
|
* @param string $string |
77
|
|
|
* |
78
|
|
|
* @return bool |
79
|
|
|
*/ |
80
|
2 |
|
public static function hasXslStylesheet($string) |
81
|
2 |
|
{ |
82
|
2 |
|
return (false !== stripos($string, '<xsl:stylesheet')); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Returns true when given `$string` contains the beginnings of an `<html>` tag. |
87
|
|
|
* |
88
|
|
|
* @param string $string |
89
|
|
|
* @return bool |
90
|
|
|
*/ |
91
|
2 |
|
public static function hasHtmlTag($string) |
92
|
2 |
|
{ |
93
|
2 |
|
return (false !== stripos($string, '<html')); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Given a key => value map in which the value is a single string or |
98
|
|
|
* a list of comma-separeted strings, it returns a new array with the |
99
|
|
|
* given keys, but the values are transformed into an array and any |
100
|
|
|
* potential duplicate values are removed. |
101
|
|
|
* If the $sort parameter is given, the list of values is sorted using |
102
|
|
|
* `sort()` and the $sort param is treated as a sort flag. |
103
|
|
|
* |
104
|
|
|
* @param array $data |
105
|
|
|
* @param bool|int $sort If false, no sorting, otherwise an int representing |
106
|
|
|
* sort flags. See http://php.net/sort |
107
|
|
|
* |
108
|
|
|
* @return array |
109
|
|
|
*/ |
110
|
5 |
|
public static function dedupValues(array $data, $sort = false) |
111
|
5 |
|
{ |
112
|
5 |
|
foreach ($data as $key => $values) { |
113
|
5 |
|
if (is_array($values)) { |
114
|
5 |
|
$parts = $values; |
115
|
|
|
} else { |
116
|
5 |
|
$parts = explode(',', $values); |
117
|
|
|
} |
118
|
|
|
|
119
|
5 |
|
$parts = array_unique($parts); |
120
|
|
|
|
121
|
|
|
// Perform sort if specified |
122
|
5 |
|
if (false !== $sort) { |
123
|
5 |
|
sort($parts, (int) $sort); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
// Store back |
127
|
5 |
|
$data[$key] = $parts; |
128
|
|
|
} |
129
|
|
|
|
130
|
5 |
|
return $data; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Given a GoogleFontsCollection it builds needed `<link rel="stylesheet">` markup. |
135
|
|
|
* |
136
|
|
|
* @param Collection $fonts |
137
|
|
|
* |
138
|
|
|
* @return string |
139
|
|
|
*/ |
140
|
2 |
|
public static function buildFontsMarkupLinks(Collection $fonts) |
141
|
2 |
|
{ |
142
|
2 |
|
$font_url = $fonts->getCombinedUrl(); |
143
|
2 |
|
$href = self::encodeUnencodedAmpersands($font_url); |
144
|
2 |
|
$markup = '<link rel="stylesheet" type="text/css" href="' . $href . '">'; |
145
|
|
|
|
146
|
2 |
|
foreach ($fonts->getTextUrls() as $url) { |
147
|
1 |
|
$markup .= '<link rel="stylesheet" type="text/css"'; |
148
|
1 |
|
$markup .= ' href="' . self::encodeUnencodedAmpersands($url) . '">'; |
149
|
|
|
} |
150
|
|
|
|
151
|
2 |
|
return $markup; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Given a GoogleFontsCollection it builds the WebFont loader script markup. |
156
|
|
|
* |
157
|
|
|
* @param Collection $fonts |
158
|
|
|
* |
159
|
|
|
* @return string |
160
|
|
|
*/ |
161
|
2 |
|
public static function buildFontsMarkupScript(Collection $collection) |
162
|
2 |
|
{ |
163
|
2 |
|
$families_array = []; |
164
|
|
|
|
165
|
2 |
|
list($names, $mapping) = $collection->getScriptData(); |
166
|
|
|
|
167
|
2 |
|
foreach ($names as $name => $sizes) { |
168
|
2 |
|
$family = $name . ':' . implode(',', $sizes); |
169
|
2 |
|
if (isset($mapping[$name])) { |
170
|
2 |
|
$family .= ':' . implode(',', $mapping[$name]); |
171
|
|
|
} |
172
|
2 |
|
$families_array[] = $family; |
173
|
|
|
} |
174
|
2 |
|
$families = "'" . implode("', '", $families_array) . "'"; |
175
|
|
|
|
176
|
|
|
// Load 'text' requests with the "custom" module |
177
|
2 |
|
$custom = ''; |
178
|
2 |
|
if ($collection->hasText()) { |
179
|
1 |
|
$custom = ",\n custom: {\n"; |
180
|
1 |
|
$custom .= " families: [ '" . implode("', '", $collection->getTextNames()) . "' ],\n"; |
181
|
1 |
|
$custom .= " urls: [ '" . implode("', '", $collection->getTextUrls()) . "' ]\n"; |
182
|
1 |
|
$custom .= ' }'; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
$markup = <<<MARKUP |
186
|
|
|
<script type="text/javascript"> |
187
|
|
|
WebFontConfig = { |
188
|
2 |
|
google: { families: [ {$families} ] }{$custom} |
189
|
|
|
}; |
190
|
|
|
(function() { |
191
|
|
|
var wf = document.createElement('script'); |
192
|
|
|
wf.src = ('https:' == document.location.protocol ? 'https' : 'http') + |
193
|
|
|
'://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js'; |
194
|
|
|
wf.type = 'text/javascript'; |
195
|
|
|
wf.async = 'true'; |
196
|
|
|
var s = document.getElementsByTagName('script')[0]; |
197
|
|
|
s.parentNode.insertBefore(wf, s); |
198
|
|
|
})(); |
199
|
|
|
</script> |
200
|
|
|
MARKUP; |
201
|
|
|
|
202
|
2 |
|
return $markup; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Builds a combined Google Font URL for multiple font families/subsets. |
207
|
|
|
* |
208
|
|
|
* Usage examples: |
209
|
|
|
* ``` |
210
|
|
|
* ZWF\GoogleFontsOptimizerUtils::buildGoogleFontsUrl( |
211
|
|
|
* [ |
212
|
|
|
* 'Open Sans' => [ '400', '400italic', '700', '700italic' ], |
213
|
|
|
* 'Ubuntu' => [ '400', '400italic', '700', '700italic' ], |
214
|
|
|
* ), |
215
|
|
|
* [ 'latin', 'latin-ext' ] |
216
|
|
|
* ); |
217
|
|
|
* ``` |
218
|
|
|
* |
219
|
|
|
* or |
220
|
|
|
* |
221
|
|
|
* ``` |
222
|
|
|
* ZWF\GoogleFontsOptimizerUtils::buildGoogleFontsUrl( |
223
|
|
|
* [ |
224
|
|
|
* 'Open Sans' => '400,400italic,700,700italic', |
225
|
|
|
* 'Ubuntu' => '400,400italic,700,700italic', |
226
|
|
|
* ], |
227
|
|
|
* 'latin,latin-ext' |
228
|
|
|
* ); |
229
|
|
|
* ``` |
230
|
|
|
* |
231
|
|
|
* @param array $fonts |
232
|
|
|
* @param array|string $subsets |
233
|
|
|
* |
234
|
|
|
* @return null|string |
235
|
|
|
*/ |
236
|
6 |
|
public static function buildGoogleFontsUrl(array $fonts, $subsets = []) |
237
|
6 |
|
{ |
238
|
6 |
|
$base_url = 'https://fonts.googleapis.com/css'; |
239
|
6 |
|
$font_args = []; |
240
|
6 |
|
$family = []; |
241
|
|
|
|
242
|
6 |
|
foreach ($fonts as $font_name => $font_weight) { |
243
|
6 |
|
if (is_array($font_weight)) { |
244
|
6 |
|
$font_weight = implode(',', $font_weight); |
245
|
|
|
} |
246
|
|
|
// Trimming end colon handles edge case of being given an empty $font_weight |
247
|
6 |
|
$family[] = trim(trim($font_name) . ':' . trim($font_weight), ':'); |
248
|
|
|
} |
249
|
|
|
|
250
|
6 |
|
$font_args['family'] = implode('|', $family); |
251
|
|
|
|
252
|
6 |
|
if (! empty($subsets)) { |
253
|
5 |
|
if (is_array($subsets)) { |
254
|
4 |
|
$subsets = array_unique($subsets); |
255
|
4 |
|
$subsets = implode(',', $subsets); |
256
|
|
|
} |
257
|
5 |
|
$font_args['subset'] = trim($subsets); |
258
|
|
|
} |
259
|
|
|
|
260
|
6 |
|
$url = $base_url . '?' . http_build_query($font_args); |
261
|
|
|
|
262
|
6 |
|
return $url; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Flatten an array without recursion. |
267
|
|
|
* |
268
|
|
|
* @param array $arr |
269
|
|
|
* |
270
|
|
|
* @return array |
271
|
|
|
*/ |
272
|
3 |
|
public static function arrayFlattenIterative(array $arr) |
273
|
3 |
|
{ |
274
|
3 |
|
$flat = []; |
275
|
3 |
|
$stack = array_values($arr); |
276
|
|
|
|
277
|
3 |
|
while ($stack) { |
|
|
|
|
278
|
3 |
|
$value = array_shift($stack); |
279
|
3 |
|
if (is_array($value)) { |
280
|
3 |
|
$stack = array_merge(array_values($value), $stack); |
281
|
|
|
} else { |
282
|
2 |
|
$flat[] = $value; |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
|
286
|
3 |
|
return $flat; |
287
|
|
|
} |
288
|
|
|
} |
289
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.