MediaWiki
REL1_24
|
00001 <?php 00037 class CSSJanus { 00038 // Patterns defined as null are built dynamically by buildPatterns() 00039 private static $patterns = array( 00040 'tmpToken' => '`TMP`', 00041 'nonAscii' => '[\200-\377]', 00042 'unicode' => '(?:(?:\\[0-9a-f]{1,6})(?:\r\n|\s)?)', 00043 'num' => '(?:[0-9]*\.[0-9]+|[0-9]+)', 00044 'unit' => '(?:em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)', 00045 'body_selector' => 'body\s*{\s*', 00046 'direction' => 'direction\s*:\s*', 00047 'escape' => null, 00048 'nmstart' => null, 00049 'nmchar' => null, 00050 'ident' => null, 00051 'quantity' => null, 00052 'possibly_negative_quantity' => null, 00053 'color' => null, 00054 'url_special_chars' => '[!#$%&*-~]', 00055 'valid_after_uri_chars' => '[\'\"]?\s*', 00056 'url_chars' => null, 00057 'lookahead_not_open_brace' => null, 00058 'lookahead_not_closing_paren' => null, 00059 'lookahead_for_closing_paren' => null, 00060 'lookahead_not_letter' => '(?![a-zA-Z])', 00061 'lookbehind_not_letter' => '(?<![a-zA-Z])', 00062 'chars_within_selector' => '[^\}]*?', 00063 'noflip_annotation' => '\/\*\!?\s*@noflip\s*\*\/', 00064 'noflip_single' => null, 00065 'noflip_class' => null, 00066 'comment' => '/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//', 00067 'direction_ltr' => null, 00068 'direction_rtl' => null, 00069 'left' => null, 00070 'right' => null, 00071 'left_in_url' => null, 00072 'right_in_url' => null, 00073 'ltr_in_url' => null, 00074 'rtl_in_url' => null, 00075 'cursor_east' => null, 00076 'cursor_west' => null, 00077 'four_notation_quantity' => null, 00078 'four_notation_color' => null, 00079 'border_radius' => null, 00080 'box_shadow' => null, 00081 'text_shadow1' => null, 00082 'text_shadow2' => null, 00083 'bg_horizontal_percentage' => null, 00084 'bg_horizontal_percentage_x' => null, 00085 ); 00086 00090 private static function buildPatterns() { 00091 if (!is_null(self::$patterns['escape'])) { 00092 // Patterns have already been built 00093 return; 00094 } 00095 00096 // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong 00097 $patterns =& self::$patterns; 00098 $patterns['escape'] = "(?:{$patterns['unicode']}|\\[^\r\n\f0-9a-f])"; 00099 $patterns['nmstart'] = "(?:[_a-z]|{$patterns['nonAscii']}|{$patterns['escape']})"; 00100 $patterns['nmchar'] = "(?:[_a-z0-9-]|{$patterns['nonAscii']}|{$patterns['escape']})"; 00101 $patterns['ident'] = "-?{$patterns['nmstart']}{$patterns['nmchar']}*"; 00102 $patterns['quantity'] = "{$patterns['num']}(?:\s*{$patterns['unit']}|{$patterns['ident']})?"; 00103 $patterns['possibly_negative_quantity'] = "((?:-?{$patterns['quantity']})|(?:inherit|auto))"; 00104 $patterns['color'] = "(#?{$patterns['nmchar']}+|(?:rgba?|hsla?)\([ \d.,%-]+\))"; 00105 $patterns['url_chars'] = "(?:{$patterns['url_special_chars']}|{$patterns['nonAscii']}|{$patterns['escape']})*"; 00106 $patterns['lookahead_not_open_brace'] = "(?!({$patterns['nmchar']}|\r?\n|\s|#|\:|\.|\,|\+|>|\(|\)|\[|\]|=|\*=|~=|\^=|'[^']*'])*?{)"; 00107 $patterns['lookahead_not_closing_paren'] = "(?!{$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))"; 00108 $patterns['lookahead_for_closing_paren'] = "(?={$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))"; 00109 $patterns['noflip_single'] = "/({$patterns['noflip_annotation']}{$patterns['lookahead_not_open_brace']}[^;}]+;?)/i"; 00110 $patterns['noflip_class'] = "/({$patterns['noflip_annotation']}{$patterns['chars_within_selector']}})/i"; 00111 $patterns['direction_ltr'] = "/({$patterns['direction']})ltr/i"; 00112 $patterns['direction_rtl'] = "/({$patterns['direction']})rtl/i"; 00113 $patterns['left'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i"; 00114 $patterns['right'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i"; 00115 $patterns['left_in_url'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_for_closing_paren']}/i"; 00116 $patterns['right_in_url'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_for_closing_paren']}/i"; 00117 $patterns['ltr_in_url'] = "/{$patterns['lookbehind_not_letter']}(ltr){$patterns['lookahead_for_closing_paren']}/i"; 00118 $patterns['rtl_in_url'] = "/{$patterns['lookbehind_not_letter']}(rtl){$patterns['lookahead_for_closing_paren']}/i"; 00119 $patterns['cursor_east'] = "/{$patterns['lookbehind_not_letter']}([ns]?)e-resize/"; 00120 $patterns['cursor_west'] = "/{$patterns['lookbehind_not_letter']}([ns]?)w-resize/"; 00121 $patterns['four_notation_quantity_props'] = "((?:margin|padding|border-width)\s*:\s*)"; 00122 $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"; 00123 $patterns['four_notation_color'] = "/((?:-color|border-style)\s*:\s*){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s*[;}])/i"; 00124 $patterns['border_radius'] = "/(border-radius\s*:\s*)([^;}]*)/"; 00125 $patterns['box_shadow'] = "/(box-shadow\s*:\s*(?:inset\s*)?){$patterns['possibly_negative_quantity']}/i"; 00126 $patterns['text_shadow1'] = "/(text-shadow\s*:\s*){$patterns['color']}(\s*){$patterns['possibly_negative_quantity']}/i"; 00127 $patterns['text_shadow2'] = "/(text-shadow\s*:\s*){$patterns['possibly_negative_quantity']}/i"; 00128 $patterns['bg_horizontal_percentage'] = "/(background(?:-position)?\s*:\s*(?:[^:;}\s]+\s+)*?)({$patterns['quantity']})/i"; 00129 $patterns['bg_horizontal_percentage_x'] = "/(background-position-x\s*:\s*)(-?{$patterns['num']}%)/i"; 00130 // @codingStandardsIgnoreEnd 00131 00132 } 00133 00141 public static function transform($css, $swapLtrRtlInURL = false, $swapLeftRightInURL = false) { 00142 // We wrap tokens in ` , not ~ like the original implementation does. 00143 // This was done because ` is not a legal character in CSS and can only 00144 // occur in URLs, where we escape it to %60 before inserting our tokens. 00145 $css = str_replace('`', '%60', $css); 00146 00147 self::buildPatterns(); 00148 00149 // Tokenize single line rules with /* @noflip */ 00150 $noFlipSingle = new CSSJanusTokenizer(self::$patterns['noflip_single'], '`NOFLIP_SINGLE`'); 00151 $css = $noFlipSingle->tokenize($css); 00152 00153 // Tokenize class rules with /* @noflip */ 00154 $noFlipClass = new CSSJanusTokenizer(self::$patterns['noflip_class'], '`NOFLIP_CLASS`'); 00155 $css = $noFlipClass->tokenize($css); 00156 00157 // Tokenize comments 00158 $comments = new CSSJanusTokenizer(self::$patterns['comment'], '`C`'); 00159 $css = $comments->tokenize($css); 00160 00161 // LTR->RTL fixes start here 00162 $css = self::fixDirection($css); 00163 if ($swapLtrRtlInURL) { 00164 $css = self::fixLtrRtlInURL($css); 00165 } 00166 00167 if ($swapLeftRightInURL) { 00168 $css = self::fixLeftRightInURL($css); 00169 } 00170 $css = self::fixLeftAndRight($css); 00171 $css = self::fixCursorProperties($css); 00172 $css = self::fixFourPartNotation($css); 00173 $css = self::fixBorderRadius($css); 00174 $css = self::fixBackgroundPosition($css); 00175 $css = self::fixShadows($css); 00176 00177 // Detokenize stuff we tokenized before 00178 $css = $comments->detokenize($css); 00179 $css = $noFlipClass->detokenize($css); 00180 $css = $noFlipSingle->detokenize($css); 00181 00182 return $css; 00183 } 00184 00197 private static function fixDirection($css) { 00198 $css = preg_replace( 00199 self::$patterns['direction_ltr'], 00200 '$1' . self::$patterns['tmpToken'], 00201 $css 00202 ); 00203 $css = preg_replace(self::$patterns['direction_rtl'], '$1ltr', $css); 00204 $css = str_replace(self::$patterns['tmpToken'], 'rtl', $css); 00205 00206 return $css; 00207 } 00208 00214 private static function fixLtrRtlInURL($css) { 00215 $css = preg_replace(self::$patterns['ltr_in_url'], self::$patterns['tmpToken'], $css); 00216 $css = preg_replace(self::$patterns['rtl_in_url'], 'ltr', $css); 00217 $css = str_replace(self::$patterns['tmpToken'], 'rtl', $css); 00218 00219 return $css; 00220 } 00221 00227 private static function fixLeftRightInURL($css) { 00228 $css = preg_replace(self::$patterns['left_in_url'], self::$patterns['tmpToken'], $css); 00229 $css = preg_replace(self::$patterns['right_in_url'], 'left', $css); 00230 $css = str_replace(self::$patterns['tmpToken'], 'right', $css); 00231 00232 return $css; 00233 } 00234 00240 private static function fixLeftAndRight($css) { 00241 $css = preg_replace(self::$patterns['left'], self::$patterns['tmpToken'], $css); 00242 $css = preg_replace(self::$patterns['right'], 'left', $css); 00243 $css = str_replace(self::$patterns['tmpToken'], 'right', $css); 00244 00245 return $css; 00246 } 00247 00253 private static function fixCursorProperties($css) { 00254 $css = preg_replace( 00255 self::$patterns['cursor_east'], 00256 '$1' . self::$patterns['tmpToken'], 00257 $css 00258 ); 00259 $css = preg_replace(self::$patterns['cursor_west'], '$1e-resize', $css); 00260 $css = str_replace(self::$patterns['tmpToken'], 'w-resize', $css); 00261 00262 return $css; 00263 } 00264 00277 private static function fixFourPartNotation($css) { 00278 $css = preg_replace(self::$patterns['four_notation_quantity'], '$1$2$3$8$5$6$7$4$9', $css); 00279 $css = preg_replace(self::$patterns['four_notation_color'], '$1$2$3$8$5$6$7$4$9', $css); 00280 return $css; 00281 } 00282 00289 private static function fixBorderRadius($css) { 00290 $css = preg_replace_callback(self::$patterns['border_radius'], function ($matches) { 00291 $pre = $matches[1]; 00292 $values = $matches[2]; 00293 $numValues = count(preg_split('/\s+/', trim($values))); 00294 switch ($numValues) { 00295 case 4: 00296 $values = preg_replace('/^(\S+)(\s*)(\S+)(\s*)(\S+)(\s*)(\S+)/', '$3$2$1$4$7$6$5', $values); 00297 break; 00298 case 3: 00299 case 2: 00300 $values = preg_replace('/^(\S+)(\s*)(\S+)/', '$3$2$1', $values); 00301 break; 00302 } 00303 return $pre . $values; 00304 }, $css); 00305 00306 return $css; 00307 } 00308 00315 private static function fixShadows($css) { 00316 // Flips the sign of a CSS value, possibly with a unit. 00317 // (We can't just negate the value with unary minus due to the units.) 00318 $flipSign = function ($cssValue) { 00319 // Don't mangle zeroes 00320 if (floatval($cssValue) === 0.0) { 00321 return $cssValue; 00322 } elseif ($cssValue[0] === '-') { 00323 return substr($cssValue, 1); 00324 } else { 00325 return "-" . $cssValue; 00326 } 00327 }; 00328 00329 $css = preg_replace_callback(self::$patterns['box_shadow'], function ($matches) use ($flipSign) { 00330 return $matches[1] . $flipSign($matches[2]); 00331 }, $css); 00332 00333 $css = preg_replace_callback(self::$patterns['text_shadow1'], function ($matches) use ($flipSign) { 00334 return $matches[1] . $matches[2] . $matches[3] . $flipSign($matches[4]); 00335 }, $css); 00336 00337 $css = preg_replace_callback(self::$patterns['text_shadow2'], function ($matches) use ($flipSign) { 00338 return $matches[1] . $flipSign($matches[2]); 00339 }, $css); 00340 00341 return $css; 00342 } 00343 00349 private static function fixBackgroundPosition($css) { 00350 $replaced = preg_replace_callback( 00351 self::$patterns['bg_horizontal_percentage'], 00352 array('self', 'calculateNewBackgroundPosition'), 00353 $css 00354 ); 00355 if ($replaced !== null) { 00356 // preg_replace_callback() sometimes returns null 00357 $css = $replaced; 00358 } 00359 $replaced = preg_replace_callback( 00360 self::$patterns['bg_horizontal_percentage_x'], 00361 array('self', 'calculateNewBackgroundPosition'), 00362 $css 00363 ); 00364 if ($replaced !== null) { 00365 $css = $replaced; 00366 } 00367 00368 return $css; 00369 } 00370 00376 private static function calculateNewBackgroundPosition($matches) { 00377 $value = $matches[2]; 00378 if (substr($value, -1) === '%') { 00379 $idx = strpos($value, '.'); 00380 if ($idx !== false) { 00381 $len = strlen($value) - $idx - 2; 00382 $value = number_format(100 - $value, $len) . '%'; 00383 } else { 00384 $value = (100 - $value) . '%'; 00385 } 00386 } 00387 return $matches[1] . $value; 00388 } 00389 } 00390 00396 class CSSJanusTokenizer { 00397 private $regex; 00398 private $token; 00399 private $originals; 00400 00406 public function __construct($regex, $token) { 00407 $this->regex = $regex; 00408 $this->token = $token; 00409 $this->originals = array(); 00410 } 00411 00418 public function tokenize($str) { 00419 return preg_replace_callback($this->regex, array($this, 'tokenizeCallback'), $str); 00420 } 00421 00426 private function tokenizeCallback($matches) { 00427 $this->originals[] = $matches[0]; 00428 return $this->token; 00429 } 00430 00437 public function detokenize($str) { 00438 // PHP has no function to replace only the first occurrence or to 00439 // replace occurrences of the same string with different values, 00440 // so we use preg_replace_callback() even though we don't really need a regex 00441 return preg_replace_callback( 00442 '/' . preg_quote($this->token, '/') . '/', 00443 array($this, 'detokenizeCallback'), 00444 $str 00445 ); 00446 } 00447 00452 private function detokenizeCallback($matches) { 00453 $retval = current($this->originals); 00454 next($this->originals); 00455 00456 return $retval; 00457 } 00458 }