MediaWiki  REL1_24
CSSJanus.php
Go to the documentation of this file.
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 }