[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * PHP port of CSSJanus. 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 */ 22 23 /** 24 * This is a PHP port of CSSJanus, a utility that transforms CSS style sheets 25 * written for LTR to RTL. 26 * 27 * The original Python version of CSSJanus is Copyright 2008 by Google Inc. and 28 * is distributed under the Apache license. This PHP port is Copyright 2010 by 29 * Roan Kattouw and is dual-licensed under the GPL (as in the comment above) and 30 * the Apache (as in the original code) licenses. 31 * 32 * Original code: http://code.google.com/p/cssjanus/source/browse/trunk/cssjanus.py 33 * License of original code: http://code.google.com/p/cssjanus/source/browse/trunk/LICENSE 34 * @author Roan Kattouw 35 * 36 */ 37 class CSSJanus { 38 // Patterns defined as null are built dynamically by buildPatterns() 39 private static $patterns = array( 40 'tmpToken' => '`TMP`', 41 'nonAscii' => '[\200-\377]', 42 'unicode' => '(?:(?:\\[0-9a-f]{1,6})(?:\r\n|\s)?)', 43 'num' => '(?:[0-9]*\.[0-9]+|[0-9]+)', 44 'unit' => '(?:em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)', 45 'body_selector' => 'body\s*{\s*', 46 'direction' => 'direction\s*:\s*', 47 'escape' => null, 48 'nmstart' => null, 49 'nmchar' => null, 50 'ident' => null, 51 'quantity' => null, 52 'possibly_negative_quantity' => null, 53 'color' => null, 54 'url_special_chars' => '[!#$%&*-~]', 55 'valid_after_uri_chars' => '[\'\"]?\s*', 56 'url_chars' => null, 57 'lookahead_not_open_brace' => null, 58 'lookahead_not_closing_paren' => null, 59 'lookahead_for_closing_paren' => null, 60 'lookahead_not_letter' => '(?![a-zA-Z])', 61 'lookbehind_not_letter' => '(?<![a-zA-Z])', 62 'chars_within_selector' => '[^\}]*?', 63 'noflip_annotation' => '\/\*\!?\s*@noflip\s*\*\/', 64 'noflip_single' => null, 65 'noflip_class' => null, 66 'comment' => '/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//', 67 'direction_ltr' => null, 68 'direction_rtl' => null, 69 'left' => null, 70 'right' => null, 71 'left_in_url' => null, 72 'right_in_url' => null, 73 'ltr_in_url' => null, 74 'rtl_in_url' => null, 75 'cursor_east' => null, 76 'cursor_west' => null, 77 'four_notation_quantity' => null, 78 'four_notation_color' => null, 79 'border_radius' => null, 80 'box_shadow' => null, 81 'text_shadow1' => null, 82 'text_shadow2' => null, 83 'bg_horizontal_percentage' => null, 84 'bg_horizontal_percentage_x' => null, 85 ); 86 87 /** 88 * Build patterns we can't define above because they depend on other patterns. 89 */ 90 private static function buildPatterns() { 91 if (!is_null(self::$patterns['escape'])) { 92 // Patterns have already been built 93 return; 94 } 95 96 // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong 97 $patterns =& self::$patterns; 98 $patterns['escape'] = "(?:{$patterns['unicode']}|\\[^\r\n\f0-9a-f])"; 99 $patterns['nmstart'] = "(?:[_a-z]|{$patterns['nonAscii']}|{$patterns['escape']})"; 100 $patterns['nmchar'] = "(?:[_a-z0-9-]|{$patterns['nonAscii']}|{$patterns['escape']})"; 101 $patterns['ident'] = "-?{$patterns['nmstart']}{$patterns['nmchar']}*"; 102 $patterns['quantity'] = "{$patterns['num']}(?:\s*{$patterns['unit']}|{$patterns['ident']})?"; 103 $patterns['possibly_negative_quantity'] = "((?:-?{$patterns['quantity']})|(?:inherit|auto))"; 104 $patterns['color'] = "(#?{$patterns['nmchar']}+|(?:rgba?|hsla?)\([ \d.,%-]+\))"; 105 $patterns['url_chars'] = "(?:{$patterns['url_special_chars']}|{$patterns['nonAscii']}|{$patterns['escape']})*"; 106 $patterns['lookahead_not_open_brace'] = "(?!({$patterns['nmchar']}|\r?\n|\s|#|\:|\.|\,|\+|>|\(|\)|\[|\]|=|\*=|~=|\^=|'[^']*'])*?{)"; 107 $patterns['lookahead_not_closing_paren'] = "(?!{$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))"; 108 $patterns['lookahead_for_closing_paren'] = "(?={$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))"; 109 $patterns['noflip_single'] = "/({$patterns['noflip_annotation']}{$patterns['lookahead_not_open_brace']}[^;}]+;?)/i"; 110 $patterns['noflip_class'] = "/({$patterns['noflip_annotation']}{$patterns['chars_within_selector']}})/i"; 111 $patterns['direction_ltr'] = "/({$patterns['direction']})ltr/i"; 112 $patterns['direction_rtl'] = "/({$patterns['direction']})rtl/i"; 113 $patterns['left'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i"; 114 $patterns['right'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i"; 115 $patterns['left_in_url'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_for_closing_paren']}/i"; 116 $patterns['right_in_url'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_for_closing_paren']}/i"; 117 $patterns['ltr_in_url'] = "/{$patterns['lookbehind_not_letter']}(ltr){$patterns['lookahead_for_closing_paren']}/i"; 118 $patterns['rtl_in_url'] = "/{$patterns['lookbehind_not_letter']}(rtl){$patterns['lookahead_for_closing_paren']}/i"; 119 $patterns['cursor_east'] = "/{$patterns['lookbehind_not_letter']}([ns]?)e-resize/"; 120 $patterns['cursor_west'] = "/{$patterns['lookbehind_not_letter']}([ns]?)w-resize/"; 121 $patterns['four_notation_quantity_props'] = "((?:margin|padding|border-width)\s*:\s*)"; 122 $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']}(\s*[;}])/i"; 123 $patterns['four_notation_color'] = "/((?:-color|border-style)\s*:\s*){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s*[;}])/i"; 124 $patterns['border_radius'] = "/(border-radius\s*:\s*)([^;}]*)/"; 125 $patterns['box_shadow'] = "/(box-shadow\s*:\s*(?:inset\s*)?){$patterns['possibly_negative_quantity']}/i"; 126 $patterns['text_shadow1'] = "/(text-shadow\s*:\s*){$patterns['color']}(\s*){$patterns['possibly_negative_quantity']}/i"; 127 $patterns['text_shadow2'] = "/(text-shadow\s*:\s*){$patterns['possibly_negative_quantity']}/i"; 128 $patterns['bg_horizontal_percentage'] = "/(background(?:-position)?\s*:\s*(?:[^:;}\s]+\s+)*?)({$patterns['quantity']})/i"; 129 $patterns['bg_horizontal_percentage_x'] = "/(background-position-x\s*:\s*)(-?{$patterns['num']}%)/i"; 130 // @codingStandardsIgnoreEnd 131 132 } 133 134 /** 135 * Transform an LTR stylesheet to RTL 136 * @param string $css stylesheet to transform 137 * @param $swapLtrRtlInURL Boolean: If true, swap 'ltr' and 'rtl' in URLs 138 * @param $swapLeftRightInURL Boolean: If true, swap 'left' and 'right' in URLs 139 * @return string Transformed stylesheet 140 */ 141 public static function transform($css, $swapLtrRtlInURL = false, $swapLeftRightInURL = false) { 142 // We wrap tokens in ` , not ~ like the original implementation does. 143 // This was done because ` is not a legal character in CSS and can only 144 // occur in URLs, where we escape it to %60 before inserting our tokens. 145 $css = str_replace('`', '%60', $css); 146 147 self::buildPatterns(); 148 149 // Tokenize single line rules with /* @noflip */ 150 $noFlipSingle = new CSSJanusTokenizer(self::$patterns['noflip_single'], '`NOFLIP_SINGLE`'); 151 $css = $noFlipSingle->tokenize($css); 152 153 // Tokenize class rules with /* @noflip */ 154 $noFlipClass = new CSSJanusTokenizer(self::$patterns['noflip_class'], '`NOFLIP_CLASS`'); 155 $css = $noFlipClass->tokenize($css); 156 157 // Tokenize comments 158 $comments = new CSSJanusTokenizer(self::$patterns['comment'], '`C`'); 159 $css = $comments->tokenize($css); 160 161 // LTR->RTL fixes start here 162 $css = self::fixDirection($css); 163 if ($swapLtrRtlInURL) { 164 $css = self::fixLtrRtlInURL($css); 165 } 166 167 if ($swapLeftRightInURL) { 168 $css = self::fixLeftRightInURL($css); 169 } 170 $css = self::fixLeftAndRight($css); 171 $css = self::fixCursorProperties($css); 172 $css = self::fixFourPartNotation($css); 173 $css = self::fixBorderRadius($css); 174 $css = self::fixBackgroundPosition($css); 175 $css = self::fixShadows($css); 176 177 // Detokenize stuff we tokenized before 178 $css = $comments->detokenize($css); 179 $css = $noFlipClass->detokenize($css); 180 $css = $noFlipSingle->detokenize($css); 181 182 return $css; 183 } 184 185 /** 186 * Replace direction: ltr; with direction: rtl; and vice versa. 187 * 188 * The original implementation only does this inside body selectors 189 * and misses "body\n{\ndirection:ltr;\n}". This function does not have 190 * these problems. 191 * 192 * See https://code.google.com/p/cssjanus/issues/detail?id=15 193 * 194 * @param $css string 195 * @return string 196 */ 197 private static function fixDirection($css) { 198 $css = preg_replace( 199 self::$patterns['direction_ltr'], 200 '$1' . self::$patterns['tmpToken'], 201 $css 202 ); 203 $css = preg_replace(self::$patterns['direction_rtl'], '$1ltr', $css); 204 $css = str_replace(self::$patterns['tmpToken'], 'rtl', $css); 205 206 return $css; 207 } 208 209 /** 210 * Replace 'ltr' with 'rtl' and vice versa in background URLs 211 * @param $css string 212 * @return string 213 */ 214 private static function fixLtrRtlInURL($css) { 215 $css = preg_replace(self::$patterns['ltr_in_url'], self::$patterns['tmpToken'], $css); 216 $css = preg_replace(self::$patterns['rtl_in_url'], 'ltr', $css); 217 $css = str_replace(self::$patterns['tmpToken'], 'rtl', $css); 218 219 return $css; 220 } 221 222 /** 223 * Replace 'left' with 'right' and vice versa in background URLs 224 * @param $css string 225 * @return string 226 */ 227 private static function fixLeftRightInURL($css) { 228 $css = preg_replace(self::$patterns['left_in_url'], self::$patterns['tmpToken'], $css); 229 $css = preg_replace(self::$patterns['right_in_url'], 'left', $css); 230 $css = str_replace(self::$patterns['tmpToken'], 'right', $css); 231 232 return $css; 233 } 234 235 /** 236 * Flip rules like left: , padding-right: , etc. 237 * @param $css string 238 * @return string 239 */ 240 private static function fixLeftAndRight($css) { 241 $css = preg_replace(self::$patterns['left'], self::$patterns['tmpToken'], $css); 242 $css = preg_replace(self::$patterns['right'], 'left', $css); 243 $css = str_replace(self::$patterns['tmpToken'], 'right', $css); 244 245 return $css; 246 } 247 248 /** 249 * Flip East and West in rules like cursor: nw-resize; 250 * @param $css string 251 * @return string 252 */ 253 private static function fixCursorProperties($css) { 254 $css = preg_replace( 255 self::$patterns['cursor_east'], 256 '$1' . self::$patterns['tmpToken'], 257 $css 258 ); 259 $css = preg_replace(self::$patterns['cursor_west'], '$1e-resize', $css); 260 $css = str_replace(self::$patterns['tmpToken'], 'w-resize', $css); 261 262 return $css; 263 } 264 265 /** 266 * Swap the second and fourth parts in four-part notation rules like 267 * padding: 1px 2px 3px 4px; 268 * 269 * Unlike the original implementation, this function doesn't suffer from 270 * the bug where whitespace is not preserved when flipping four-part rules 271 * and four-part color rules with multiple whitespace characters between 272 * colors are not recognized. 273 * See https://code.google.com/p/cssjanus/issues/detail?id=16 274 * @param $css string 275 * @return string 276 */ 277 private static function fixFourPartNotation($css) { 278 $css = preg_replace(self::$patterns['four_notation_quantity'], '$1$2$3$8$5$6$7$4$9', $css); 279 $css = preg_replace(self::$patterns['four_notation_color'], '$1$2$3$8$5$6$7$4$9', $css); 280 return $css; 281 } 282 283 /** 284 * Swaps appropriate corners in border-radius values. 285 * 286 * @param $css string 287 * @return string 288 */ 289 private static function fixBorderRadius($css) { 290 $css = preg_replace_callback(self::$patterns['border_radius'], function ($matches) { 291 $pre = $matches[1]; 292 $values = $matches[2]; 293 $numValues = count(preg_split('/\s+/', trim($values))); 294 switch ($numValues) { 295 case 4: 296 $values = preg_replace('/^(\S+)(\s*)(\S+)(\s*)(\S+)(\s*)(\S+)/', '$3$2$1$4$7$6$5', $values); 297 break; 298 case 3: 299 case 2: 300 $values = preg_replace('/^(\S+)(\s*)(\S+)/', '$3$2$1', $values); 301 break; 302 } 303 return $pre . $values; 304 }, $css); 305 306 return $css; 307 } 308 309 /** 310 * Negates horizontal offset in box-shadow and text-shadow rules. 311 * 312 * @param $css string 313 * @return string 314 */ 315 private static function fixShadows($css) { 316 // Flips the sign of a CSS value, possibly with a unit. 317 // (We can't just negate the value with unary minus due to the units.) 318 $flipSign = function ($cssValue) { 319 // Don't mangle zeroes 320 if (floatval($cssValue) === 0.0) { 321 return $cssValue; 322 } elseif ($cssValue[0] === '-') { 323 return substr($cssValue, 1); 324 } else { 325 return "-" . $cssValue; 326 } 327 }; 328 329 $css = preg_replace_callback(self::$patterns['box_shadow'], function ($matches) use ($flipSign) { 330 return $matches[1] . $flipSign($matches[2]); 331 }, $css); 332 333 $css = preg_replace_callback(self::$patterns['text_shadow1'], function ($matches) use ($flipSign) { 334 return $matches[1] . $matches[2] . $matches[3] . $flipSign($matches[4]); 335 }, $css); 336 337 $css = preg_replace_callback(self::$patterns['text_shadow2'], function ($matches) use ($flipSign) { 338 return $matches[1] . $flipSign($matches[2]); 339 }, $css); 340 341 return $css; 342 } 343 344 /** 345 * Flip horizontal background percentages. 346 * @param $css string 347 * @return string 348 */ 349 private static function fixBackgroundPosition($css) { 350 $replaced = preg_replace_callback( 351 self::$patterns['bg_horizontal_percentage'], 352 array('self', 'calculateNewBackgroundPosition'), 353 $css 354 ); 355 if ($replaced !== null) { 356 // preg_replace_callback() sometimes returns null 357 $css = $replaced; 358 } 359 $replaced = preg_replace_callback( 360 self::$patterns['bg_horizontal_percentage_x'], 361 array('self', 'calculateNewBackgroundPosition'), 362 $css 363 ); 364 if ($replaced !== null) { 365 $css = $replaced; 366 } 367 368 return $css; 369 } 370 371 /** 372 * Callback for fixBackgroundPosition() 373 * @param $matches array 374 * @return string 375 */ 376 private static function calculateNewBackgroundPosition($matches) { 377 $value = $matches[2]; 378 if (substr($value, -1) === '%') { 379 $idx = strpos($value, '.'); 380 if ($idx !== false) { 381 $len = strlen($value) - $idx - 2; 382 $value = number_format(100 - $value, $len) . '%'; 383 } else { 384 $value = (100 - $value) . '%'; 385 } 386 } 387 return $matches[1] . $value; 388 } 389 } 390 391 /** 392 * Utility class used by CSSJanus that tokenizes and untokenizes things we want 393 * to protect from being janused. 394 * @author Roan Kattouw 395 */ 396 class CSSJanusTokenizer { 397 private $regex; 398 private $token; 399 private $originals; 400 401 /** 402 * Constructor 403 * @param string $regex Regular expression whose matches to replace by a token. 404 * @param string $token Token 405 */ 406 public function __construct($regex, $token) { 407 $this->regex = $regex; 408 $this->token = $token; 409 $this->originals = array(); 410 } 411 412 /** 413 * Replace all occurrences of $regex in $str with a token and remember 414 * the original strings. 415 * @param string $str to tokenize 416 * @return string Tokenized string 417 */ 418 public function tokenize($str) { 419 return preg_replace_callback($this->regex, array($this, 'tokenizeCallback'), $str); 420 } 421 422 /** 423 * @param $matches array 424 * @return string 425 */ 426 private function tokenizeCallback($matches) { 427 $this->originals[] = $matches[0]; 428 return $this->token; 429 } 430 431 /** 432 * Replace tokens with their originals. If multiple strings were tokenized, it's important they be 433 * detokenized in exactly the SAME ORDER. 434 * @param string $str previously run through tokenize() 435 * @return string Original string 436 */ 437 public function detokenize($str) { 438 // PHP has no function to replace only the first occurrence or to 439 // replace occurrences of the same string with different values, 440 // so we use preg_replace_callback() even though we don't really need a regex 441 return preg_replace_callback( 442 '/' . preg_quote($this->token, '/') . '/', 443 array($this, 'detokenizeCallback'), 444 $str 445 ); 446 } 447 448 /** 449 * @param $matches 450 * @return mixed 451 */ 452 private function detokenizeCallback($matches) { 453 $retval = current($this->originals); 454 next($this->originals); 455 456 return $retval; 457 } 458 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |