Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.24% |
229 / 243 |
|
76.19% |
16 / 21 |
CRAP | |
50.00% |
1 / 2 |
CSSJanus | |
100.00% |
229 / 229 |
|
100.00% |
16 / 16 |
32 | |
100.00% |
1 / 1 |
buildPatterns | |
100.00% |
89 / 89 |
|
100.00% |
1 / 1 |
2 | |||
transform | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
4 | |||
fixDirection | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
fixLtrRtlInURL | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
fixLeftRightInURL | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
fixLeftAndRight | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
fixCursorProperties | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
fixFourPartNotation | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
fixBorderRadius | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
calculateBorderRadius | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
flipBorderRadiusValues | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
flipSign | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
fixShadows | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
1 | |||
fixTranslate | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
fixBackgroundPosition | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
3 | |||
calculateNewBackgroundPosition | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
CSSJanusTokenizer | |
0.00% |
0 / 14 |
|
0.00% |
0 / 5 |
30 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
tokenize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tokenizeCallback | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
detokenize | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
detokenizeCallback | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * PHP port of CSSJanus. https://www.mediawiki.org/wiki/CSSJanus |
4 | * |
5 | * Copyright 2020 Timo Tijhof |
6 | * Copyright 2014 Trevor Parscal |
7 | * Copyright 2010 Roan Kattouw |
8 | * Copyright 2008 Google Inc. |
9 | * |
10 | * Licensed under the Apache License, Version 2.0 (the "License"); |
11 | * you may not use this file except in compliance with the License. |
12 | * You may obtain a copy of the License at |
13 | * |
14 | * http://www.apache.org/licenses/LICENSE-2.0 |
15 | * |
16 | * Unless required by applicable law or agreed to in writing, software |
17 | * distributed under the License is distributed on an "AS IS" BASIS, |
18 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
19 | * See the License for the specific language governing permissions and |
20 | * limitations under the License. |
21 | * |
22 | * @file |
23 | */ |
24 | |
25 | /** |
26 | * CSSJanus is a utility that converts CSS stylesheets |
27 | * from left-to-right (LTR) to right-to-left (RTL). |
28 | */ |
29 | class CSSJanus { |
30 | private const TOKEN_TMP = '`TMP`'; |
31 | private const TOKEN_COMMENT = '`COMMENT`'; |
32 | |
33 | private static $patterns = null; |
34 | |
35 | private static function buildPatterns() { |
36 | if ( self::$patterns !== null ) { |
37 | return; |
38 | } |
39 | // Patterns defined as null are built dynamically |
40 | $patterns = [ |
41 | 'tmpToken' => '`TMP`', |
42 | 'nonAscii' => '[\200-\377]', |
43 | 'unicode' => '(?:(?:\\\\[0-9a-f]{1,6})(?:\r\n|\s)?)', |
44 | 'num' => '(?:[0-9]*\.[0-9]+|[0-9]+)', |
45 | 'unit' => '(?:em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)', |
46 | 'body_selector' => 'body\s*{\s*', |
47 | 'direction' => 'direction\s*:\s*', |
48 | 'escape' => null, |
49 | 'nmstart' => null, |
50 | 'nmchar' => null, |
51 | 'ident' => null, |
52 | 'quantity' => null, |
53 | 'possibly_negative_quantity' => null, |
54 | 'color' => null, |
55 | 'url_special_chars' => '[!#$%&*-~]', |
56 | 'valid_after_uri_chars' => '[\'\"]?\s*', |
57 | 'url_chars' => null, |
58 | 'lookahead_not_open_brace' => null, |
59 | 'lookahead_not_closing_paren' => null, |
60 | 'lookahead_for_closing_paren' => null, |
61 | 'lookahead_not_letter' => '(?![a-zA-Z])', |
62 | 'lookbehind_not_letter' => '(?<![a-zA-Z])', |
63 | 'chars_within_selector' => '[^\}]*?', |
64 | 'noflip_annotation' => '\/\*\!?\s*@noflip\s*\*\/', |
65 | 'noflip_single' => null, |
66 | 'noflip_class' => null, |
67 | 'comment' => '/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//', |
68 | 'direction_ltr' => null, |
69 | 'direction_rtl' => null, |
70 | 'left' => null, |
71 | 'right' => null, |
72 | 'left_in_url' => null, |
73 | 'right_in_url' => null, |
74 | 'ltr_in_url' => null, |
75 | 'rtl_in_url' => null, |
76 | 'cursor_east' => null, |
77 | 'cursor_west' => null, |
78 | 'four_notation_quantity' => null, |
79 | 'four_notation_color' => null, |
80 | 'border_radius' => null, |
81 | 'box_shadow' => null, |
82 | 'text_shadow1' => null, |
83 | 'text_shadow2' => null, |
84 | 'bg_horizontal_percentage' => null, |
85 | 'bg_horizontal_percentage_x' => null, |
86 | 'suffix' => '(\s*(?:!important\s*)?[;}])' |
87 | ]; |
88 | |
89 | // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong |
90 | $patterns['escape'] = "(?:{$patterns['unicode']}|\\\\[^\\r\\n\\f0-9a-f])"; |
91 | $patterns['nmstart'] = "(?:[_a-z]|{$patterns['nonAscii']}|{$patterns['escape']})"; |
92 | $patterns['nmchar'] = "(?:[_a-z0-9-]|{$patterns['nonAscii']}|{$patterns['escape']})"; |
93 | $patterns['ident'] = "-?{$patterns['nmstart']}{$patterns['nmchar']}*"; |
94 | $patterns['quantity'] = "{$patterns['num']}(?:\s*{$patterns['unit']}|{$patterns['ident']})?"; |
95 | $patterns['possibly_negative_quantity'] = "((?:-?{$patterns['quantity']})|(?:inherit|auto))"; |
96 | $patterns['color'] = "(#?{$patterns['nmchar']}+|(?:rgba?|hsla?)\([ \d.,%-]+\))"; |
97 | // Use "*+" instead of "*?" to avoid reaching the backtracking limit. |
98 | // <https://phabricator.wikimedia.org/T326481>, <https://phabricator.wikimedia.org/T215746#4944830>. |
99 | $patterns['url_chars'] = "(?:{$patterns['url_special_chars']}|{$patterns['nonAscii']}|{$patterns['escape']})*+"; |
100 | $patterns['lookahead_not_open_brace'] = "(?!({$patterns['nmchar']}|\\r?\\n|\s|#|\:|\.|\,|\+|>|~|\(|\)|\[|\]|=|\*=|~=|\^=|'[^']*'|\"[^\"]*\"|" . self::TOKEN_COMMENT . ")*+{)"; |
101 | $patterns['lookahead_not_closing_paren'] = "(?!{$patterns['url_chars']}{$patterns['valid_after_uri_chars']}\))"; |
102 | $patterns['lookahead_for_closing_paren'] = "(?={$patterns['url_chars']}{$patterns['valid_after_uri_chars']}\))"; |
103 | $patterns['noflip_single'] = "/({$patterns['noflip_annotation']}{$patterns['lookahead_not_open_brace']}[^;}]+;?)/i"; |
104 | $patterns['noflip_class'] = "/({$patterns['noflip_annotation']}{$patterns['chars_within_selector']}})/i"; |
105 | $patterns['direction_ltr'] = "/({$patterns['direction']})ltr/i"; |
106 | $patterns['direction_rtl'] = "/({$patterns['direction']})rtl/i"; |
107 | $patterns['left'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i"; |
108 | $patterns['right'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i"; |
109 | $patterns['left_in_url'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_for_closing_paren']}/i"; |
110 | $patterns['right_in_url'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_for_closing_paren']}/i"; |
111 | $patterns['ltr_in_url'] = "/{$patterns['lookbehind_not_letter']}(ltr){$patterns['lookahead_for_closing_paren']}/i"; |
112 | $patterns['rtl_in_url'] = "/{$patterns['lookbehind_not_letter']}(rtl){$patterns['lookahead_for_closing_paren']}/i"; |
113 | $patterns['cursor_east'] = "/{$patterns['lookbehind_not_letter']}([ns]?)e-resize/"; |
114 | $patterns['cursor_west'] = "/{$patterns['lookbehind_not_letter']}([ns]?)w-resize/"; |
115 | $patterns['four_notation_quantity_props'] = "((?:margin|padding|border-width)\s*:\s*)"; |
116 | $patterns['four_notation_quantity'] = "/{$patterns['four_notation_quantity_props']}{$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}{$patterns['suffix']}/i"; |
117 | $patterns['four_notation_color'] = "/((?:-color|border-style)\s*:\s*){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}{$patterns['suffix']}/i"; |
118 | // border-radius: <length or percentage>{1,4} [optional: / <length or percentage>{1,4} ] |
119 | $patterns['border_radius'] = '/(border-radius\s*:\s*)' . $patterns['possibly_negative_quantity'] |
120 | . '(?:(?:\s+' . $patterns['possibly_negative_quantity'] . ')(?:\s+' . $patterns['possibly_negative_quantity'] . ')?(?:\s+' . $patterns['possibly_negative_quantity'] . ')?)?' |
121 | . '(?:(?:(?:\s*\/\s*)' . $patterns['possibly_negative_quantity'] . ')(?:\s+' . $patterns['possibly_negative_quantity'] . ')?(?:\s+' . $patterns['possibly_negative_quantity'] . ')?(?:\s+' . $patterns['possibly_negative_quantity'] . ')?)?' . $patterns['suffix'] |
122 | . '/i'; |
123 | $patterns['box_shadow'] = "/(box-shadow\s*:\s*(?:inset\s*)?){$patterns['possibly_negative_quantity']}/i"; |
124 | $patterns['text_shadow1'] = "/(text-shadow\s*:\s*){$patterns['possibly_negative_quantity']}(\s*){$patterns['color']}/i"; |
125 | $patterns['text_shadow2'] = "/(text-shadow\s*:\s*){$patterns['color']}(\s*){$patterns['possibly_negative_quantity']}/i"; |
126 | $patterns['text_shadow3'] = "/(text-shadow\s*:\s*){$patterns['possibly_negative_quantity']}/i"; |
127 | $patterns['bg_horizontal_percentage'] = "/(background(?:-position)?\s*:\s*(?:[^:;}\s]+\s+)*?)({$patterns['quantity']})/i"; |
128 | $patterns['bg_horizontal_percentage_x'] = "/(background-position-x\s*:\s*)(-?{$patterns['num']}%)/i"; |
129 | $patterns['translate_x'] = "/(transform\s*:[^;}]*)(translateX\s*\(\s*){$patterns['possibly_negative_quantity']}(\s*\))/i"; |
130 | $patterns['translate'] = "/(transform\s*:[^;}]*)(translate\s*\(\s*){$patterns['possibly_negative_quantity']}((?:\s*,\s*{$patterns['possibly_negative_quantity']}){0,2}\s*\))/i"; |
131 | // @codingStandardsIgnoreEnd |
132 | |
133 | self::$patterns = $patterns; |
134 | } |
135 | |
136 | /** |
137 | * Transform an LTR stylesheet to RTL |
138 | * |
139 | * @param string $css Stylesheet to transform |
140 | * @param bool|array{transformDirInUrl?:bool,transformEdgeInUrl?:bool} $options Options array, |
141 | * or value of transformDirInUrl option (back-compat) |
142 | * - transformDirInUrl: Transform directions in URLs (ltr/rtl). Default: false. |
143 | * - transformEdgeInUrl: Transform edges in URLs (left/right). Default: false. |
144 | * @param bool $transformEdgeInUrl [optional] For back-compat |
145 | * @return string Transformed stylesheet |
146 | */ |
147 | public static function transform( $css, $options = [], $transformEdgeInUrl = false ) { |
148 | if ( !is_array( $options ) ) { |
149 | $options = [ |
150 | 'transformDirInUrl' => (bool)$options, |
151 | 'transformEdgeInUrl' => (bool)$transformEdgeInUrl, |
152 | ]; |
153 | } |
154 | |
155 | // Defaults |
156 | $options += [ |
157 | 'transformDirInUrl' => false, |
158 | 'transformEdgeInUrl' => false, |
159 | ]; |
160 | |
161 | self::buildPatterns(); |
162 | |
163 | // We wrap tokens in ` , not ~ like the original implementation does. |
164 | // This was done because ` is not a legal character in CSS and can only |
165 | // occur in URLs, where we escape it to %60 before inserting our tokens. |
166 | $css = str_replace( '`', '%60', $css ); |
167 | |
168 | // Tokenize single line rules with /* @noflip */ |
169 | $noFlipSingle = new CSSJanusTokenizer( self::$patterns['noflip_single'], '`NOFLIP_SINGLE`' ); |
170 | $css = $noFlipSingle->tokenize( $css ); |
171 | |
172 | // Tokenize class rules with /* @noflip */ |
173 | $noFlipClass = new CSSJanusTokenizer( self::$patterns['noflip_class'], '`NOFLIP_CLASS`' ); |
174 | $css = $noFlipClass->tokenize( $css ); |
175 | |
176 | // Tokenize comments |
177 | $comments = new CSSJanusTokenizer( self::$patterns['comment'], self::TOKEN_COMMENT ); |
178 | $css = $comments->tokenize( $css ); |
179 | |
180 | // LTR->RTL fixes start here |
181 | $css = self::fixDirection( $css ); |
182 | if ( $options['transformDirInUrl'] ) { |
183 | $css = self::fixLtrRtlInURL( $css ); |
184 | } |
185 | |
186 | if ( $options['transformEdgeInUrl'] ) { |
187 | $css = self::fixLeftRightInURL( $css ); |
188 | } |
189 | $css = self::fixLeftAndRight( $css ); |
190 | $css = self::fixCursorProperties( $css ); |
191 | $css = self::fixFourPartNotation( $css ); |
192 | $css = self::fixBorderRadius( $css ); |
193 | $css = self::fixBackgroundPosition( $css ); |
194 | $css = self::fixShadows( $css ); |
195 | $css = self::fixTranslate( $css ); |
196 | |
197 | // Detokenize stuff we tokenized before |
198 | $css = $comments->detokenize( $css ); |
199 | $css = $noFlipClass->detokenize( $css ); |
200 | $css = $noFlipSingle->detokenize( $css ); |
201 | |
202 | return $css; |
203 | } |
204 | |
205 | /** |
206 | * Replace direction: ltr; with direction: rtl; and vice versa. |
207 | * |
208 | * The original implementation only does this inside body selectors |
209 | * and misses "body\n{\ndirection:ltr;\n}". This function does not have |
210 | * these problems. |
211 | * |
212 | * See https://code.google.com/p/cssjanus/issues/detail?id=15 |
213 | * |
214 | * @param string $css |
215 | * @return string |
216 | */ |
217 | private static function fixDirection( $css ) { |
218 | $css = preg_replace( |
219 | self::$patterns['direction_ltr'], |
220 | '$1' . self::TOKEN_TMP, |
221 | $css |
222 | ); |
223 | $css = preg_replace( self::$patterns['direction_rtl'], '$1ltr', $css ); |
224 | $css = str_replace( self::TOKEN_TMP, 'rtl', $css ); |
225 | |
226 | return $css; |
227 | } |
228 | |
229 | /** |
230 | * Replace 'ltr' with 'rtl' and vice versa in background URLs |
231 | * @param string $css |
232 | * @return string |
233 | */ |
234 | private static function fixLtrRtlInURL( $css ) { |
235 | $css = preg_replace( self::$patterns['ltr_in_url'], self::TOKEN_TMP, $css ); |
236 | $css = preg_replace( self::$patterns['rtl_in_url'], 'ltr', $css ); |
237 | $css = str_replace( self::TOKEN_TMP, 'rtl', $css ); |
238 | |
239 | return $css; |
240 | } |
241 | |
242 | /** |
243 | * Replace 'left' with 'right' and vice versa in background URLs |
244 | * @param string $css |
245 | * @return string |
246 | */ |
247 | private static function fixLeftRightInURL( $css ) { |
248 | $css = preg_replace( self::$patterns['left_in_url'], self::TOKEN_TMP, $css ); |
249 | $css = preg_replace( self::$patterns['right_in_url'], 'left', $css ); |
250 | $css = str_replace( self::TOKEN_TMP, 'right', $css ); |
251 | |
252 | return $css; |
253 | } |
254 | |
255 | /** |
256 | * Flip rules like left: , padding-right: , etc. |
257 | * @param string $css |
258 | * @return string |
259 | */ |
260 | private static function fixLeftAndRight( $css ) { |
261 | $css = preg_replace( self::$patterns['left'], self::TOKEN_TMP, $css ); |
262 | $css = preg_replace( self::$patterns['right'], 'left', $css ); |
263 | $css = str_replace( self::TOKEN_TMP, 'right', $css ); |
264 | |
265 | return $css; |
266 | } |
267 | |
268 | /** |
269 | * Flip East and West in rules like cursor: nw-resize; |
270 | * @param string $css |
271 | * @return string |
272 | */ |
273 | private static function fixCursorProperties( $css ) { |
274 | $css = preg_replace( |
275 | self::$patterns['cursor_east'], |
276 | '$1' . self::TOKEN_TMP, |
277 | $css |
278 | ); |
279 | $css = preg_replace( self::$patterns['cursor_west'], '$1e-resize', $css ); |
280 | $css = str_replace( self::TOKEN_TMP, 'w-resize', $css ); |
281 | |
282 | return $css; |
283 | } |
284 | |
285 | /** |
286 | * Swap the second and fourth parts in four-part notation rules like |
287 | * padding: 1px 2px 3px 4px; |
288 | * |
289 | * Unlike the original implementation, this function doesn't suffer from |
290 | * the bug where whitespace is not preserved when flipping four-part rules |
291 | * and four-part color rules with multiple whitespace characters between |
292 | * colors are not recognized. |
293 | * See https://code.google.com/p/cssjanus/issues/detail?id=16 |
294 | * @param string $css |
295 | * @return string |
296 | */ |
297 | private static function fixFourPartNotation( $css ) { |
298 | $css = preg_replace( self::$patterns['four_notation_quantity'], '$1$2$3$8$5$6$7$4$9', $css ); |
299 | $css = preg_replace( self::$patterns['four_notation_color'], '$1$2$3$8$5$6$7$4$9', $css ); |
300 | return $css; |
301 | } |
302 | |
303 | /** |
304 | * Swaps appropriate corners in border-radius values. |
305 | * |
306 | * @param string $css |
307 | * @return string |
308 | */ |
309 | private static function fixBorderRadius( $css ) { |
310 | return preg_replace_callback( |
311 | self::$patterns['border_radius'], |
312 | [ self::class, 'calculateBorderRadius' ], |
313 | $css |
314 | ); |
315 | } |
316 | |
317 | /** |
318 | * Callback for fixBorderRadius() |
319 | * @param array $matches |
320 | * @return string |
321 | */ |
322 | private static function calculateBorderRadius( $matches ) { |
323 | $pre = $matches[1]; |
324 | $firstGroup = array_filter( array_slice( $matches, 2, 4 ), 'strlen' ); |
325 | $secondGroup = array_filter( array_slice( $matches, 6, 4 ), 'strlen' ); |
326 | $post = $matches[10] ?: ''; |
327 | |
328 | if ( $secondGroup ) { |
329 | $values = self::flipBorderRadiusValues( $firstGroup ) |
330 | . ' / ' . self::flipBorderRadiusValues( $secondGroup ); |
331 | } else { |
332 | $values = self::flipBorderRadiusValues( $firstGroup ); |
333 | } |
334 | |
335 | return $pre . $values . $post; |
336 | } |
337 | |
338 | /** |
339 | * Callback for fixBorderRadius() |
340 | * @param array $values Matched values |
341 | * @return string Flipped values |
342 | */ |
343 | private static function flipBorderRadiusValues( $values ) { |
344 | switch ( count( $values ) ) { |
345 | case 4: |
346 | $values = [ $values[1], $values[0], $values[3], $values[2] ]; |
347 | break; |
348 | case 3: |
349 | $values = [ $values[1], $values[0], $values[1], $values[2] ]; |
350 | break; |
351 | case 2: |
352 | $values = [ $values[1], $values[0] ]; |
353 | break; |
354 | case 1: |
355 | $values = [ $values[0] ]; |
356 | break; |
357 | } |
358 | return implode( ' ', $values ); |
359 | } |
360 | |
361 | /** |
362 | * Flips the sign of a CSS value, possibly with a unit. |
363 | * |
364 | * We can't just negate the value with unary minus due to the units. |
365 | * |
366 | * @param string $cssValue |
367 | * @return string |
368 | */ |
369 | private static function flipSign( $cssValue ) { |
370 | // Don't mangle zeroes |
371 | if ( floatval( $cssValue ) === 0.0 ) { |
372 | return $cssValue; |
373 | } elseif ( $cssValue[0] === '-' ) { |
374 | return substr( $cssValue, 1 ); |
375 | } else { |
376 | return "-" . $cssValue; |
377 | } |
378 | } |
379 | |
380 | /** |
381 | * Negates horizontal offset in box-shadow and text-shadow rules. |
382 | * |
383 | * @param string $css |
384 | * @return string |
385 | */ |
386 | private static function fixShadows( $css ) { |
387 | $css = preg_replace_callback( self::$patterns['box_shadow'], function ( $matches ) { |
388 | return $matches[1] . self::flipSign( $matches[2] ); |
389 | }, $css ); |
390 | |
391 | $css = preg_replace_callback( self::$patterns['text_shadow1'], function ( $matches ) { |
392 | return $matches[1] . $matches[2] . $matches[3] . self::flipSign( $matches[4] ); |
393 | }, $css ); |
394 | |
395 | $css = preg_replace_callback( self::$patterns['text_shadow2'], function ( $matches ) { |
396 | return $matches[1] . $matches[2] . $matches[3] . self::flipSign( $matches[4] ); |
397 | }, $css ); |
398 | |
399 | $css = preg_replace_callback( self::$patterns['text_shadow3'], function ( $matches ) { |
400 | return $matches[1] . self::flipSign( $matches[2] ); |
401 | }, $css ); |
402 | |
403 | return $css; |
404 | } |
405 | |
406 | /** |
407 | * Negates horizontal offset in tranform: translate() |
408 | * |
409 | * @param string $css |
410 | * @return string |
411 | */ |
412 | private static function fixTranslate( $css ) { |
413 | $css = preg_replace_callback( self::$patterns['translate'], function ( $matches ) { |
414 | return $matches[1] . $matches[2] . self::flipSign( $matches[3] ) . $matches[4]; |
415 | }, $css ); |
416 | |
417 | $css = preg_replace_callback( self::$patterns['translate_x'], function ( $matches ) { |
418 | return $matches[1] . $matches[2] . self::flipSign( $matches[3] ) . $matches[4]; |
419 | }, $css ); |
420 | |
421 | return $css; |
422 | } |
423 | |
424 | /** |
425 | * Flip horizontal background percentages. |
426 | * @param string $css |
427 | * @return string |
428 | */ |
429 | private static function fixBackgroundPosition( $css ) { |
430 | $replaced = preg_replace_callback( |
431 | self::$patterns['bg_horizontal_percentage'], |
432 | [ self::class, 'calculateNewBackgroundPosition' ], |
433 | $css |
434 | ); |
435 | if ( $replaced !== null ) { |
436 | // preg_replace_callback() sometimes returns null |
437 | $css = $replaced; |
438 | } |
439 | $replaced = preg_replace_callback( |
440 | self::$patterns['bg_horizontal_percentage_x'], |
441 | [ self::class, 'calculateNewBackgroundPosition' ], |
442 | $css |
443 | ); |
444 | if ( $replaced !== null ) { |
445 | $css = $replaced; |
446 | } |
447 | |
448 | return $css; |
449 | } |
450 | |
451 | /** |
452 | * Callback for fixBackgroundPosition() |
453 | * @param array $matches |
454 | * @return string |
455 | */ |
456 | private static function calculateNewBackgroundPosition( $matches ) { |
457 | $value = $matches[2]; |
458 | if ( substr( $value, -1 ) === '%' ) { |
459 | $idx = strpos( $value, '.' ); |
460 | if ( $idx !== false ) { |
461 | $len = strlen( $value ) - $idx - 2; |
462 | $value = number_format( 100 - (float)$value, $len ) . '%'; |
463 | } else { |
464 | $value = ( 100 - (float)$value ) . '%'; |
465 | } |
466 | } |
467 | return $matches[1] . $value; |
468 | } |
469 | } |
470 | |
471 | /** |
472 | * Utility class used by CSSJanus that tokenizes and untokenizes things we want |
473 | * to protect from being janused. |
474 | */ |
475 | class CSSJanusTokenizer { |
476 | private $regex; |
477 | private $token; |
478 | private $originals; |
479 | |
480 | /** |
481 | * Constructor |
482 | * @param string $regex Regular expression whose matches to replace by a token. |
483 | * @param string $token Token |
484 | */ |
485 | public function __construct( $regex, $token ) { |
486 | $this->regex = $regex; |
487 | $this->token = $token; |
488 | $this->originals = []; |
489 | } |
490 | |
491 | /** |
492 | * Replace all occurrences of $regex in $str with a token and remember |
493 | * the original strings. |
494 | * @param string $str to tokenize |
495 | * @return string Tokenized string |
496 | */ |
497 | public function tokenize( $str ) { |
498 | return preg_replace_callback( $this->regex, [ $this, 'tokenizeCallback' ], $str ); |
499 | } |
500 | |
501 | /** |
502 | * @param array $matches |
503 | * @return string |
504 | */ |
505 | private function tokenizeCallback( $matches ) { |
506 | $this->originals[] = $matches[0]; |
507 | return $this->token; |
508 | } |
509 | |
510 | /** |
511 | * Replace tokens with their originals. If multiple strings were tokenized, it's important they be |
512 | * detokenized in exactly the SAME ORDER. |
513 | * @param string $str previously run through tokenize() |
514 | * @return string Original string |
515 | */ |
516 | public function detokenize( $str ) { |
517 | // PHP has no function to replace only the first occurrence or to |
518 | // replace occurrences of the same string with different values, |
519 | // so we use preg_replace_callback() even though we don't really need a regex |
520 | return preg_replace_callback( |
521 | '/' . preg_quote( $this->token, '/' ) . '/', |
522 | [ $this, 'detokenizeCallback' ], |
523 | $str |
524 | ); |
525 | } |
526 | |
527 | /** |
528 | * @param array $matches |
529 | * @return mixed |
530 | */ |
531 | private function detokenizeCallback( $matches ) { |
532 | $retval = current( $this->originals ); |
533 | next( $this->originals ); |
534 | |
535 | return $retval; |
536 | } |
537 | } |