00001
00002
00003
00004
00005 """Converts a LeftToRight Cascading Style Sheet into a RightToLeft one.
00006
00007 This is a utility script for replacing "left" oriented things in a CSS file
00008 like float, padding, margin with "right" oriented values.
00009 It also does the opposite.
00010 The goal is to be able to conditionally serve one large, cat'd, compiled CSS
00011 file appropriate for LeftToRight oriented languages and RightToLeft ones.
00012 This utility will hopefully help your structural layout done in CSS in
00013 terms of its RTL compatibility. It will not help with some of the more
00014 complicated bidirectional text issues.
00015 """
00016
00017 __author__ = 'elsigh@google.com (Lindsey Simon)'
00018 __version__ = '0.1'
00019
00020 import logging
00021 import re
00022 import sys
00023 import getopt
00024 import os
00025
00026 import csslex
00027
00028 logging.getLogger().setLevel(logging.INFO)
00029
00030
00031 SWAP_LTR_RTL_IN_URL_DEFAULT = False
00032 SWAP_LEFT_RIGHT_IN_URL_DEFAULT = False
00033 FLAGS = {'swap_ltr_rtl_in_url': SWAP_LTR_RTL_IN_URL_DEFAULT,
00034 'swap_left_right_in_url': SWAP_LEFT_RIGHT_IN_URL_DEFAULT}
00035
00036
00037 TOKEN_DELIMITER = '~'
00038
00039
00040 TMP_TOKEN = '%sTMP%s' % (TOKEN_DELIMITER, TOKEN_DELIMITER)
00041
00042
00043 TOKEN_LINES = '%sJ%s' % (TOKEN_DELIMITER, TOKEN_DELIMITER)
00044
00045
00046 LTR = 'ltr'
00047 RTL = 'rtl'
00048 LEFT = 'left'
00049 RIGHT = 'right'
00050
00051
00052
00053
00054 LOOKBEHIND_NOT_LETTER = r'(?<![a-zA-Z])'
00055
00056
00057
00058
00059
00060
00061 LOOKAHEAD_NOT_OPEN_BRACE = (r'(?!(?:%s|%s|%s|#|\:|\.|\,|\+|>)*?{)' %
00062 (csslex.NMCHAR, TOKEN_LINES, csslex.SPACE))
00063
00064
00065
00066
00067
00068 VALID_AFTER_URI_CHARS = r'[\'\"]?%s' % csslex.WHITESPACE
00069 LOOKAHEAD_NOT_CLOSING_PAREN = r'(?!%s?%s\))' % (csslex.URL_CHARS,
00070 VALID_AFTER_URI_CHARS)
00071 LOOKAHEAD_FOR_CLOSING_PAREN = r'(?=%s?%s\))' % (csslex.URL_CHARS,
00072 VALID_AFTER_URI_CHARS)
00073
00074
00075
00076
00077 POSSIBLY_NEGATIVE_QUANTITY = r'((?:-?%s)|(?:inherit|auto))' % csslex.QUANTITY
00078 POSSIBLY_NEGATIVE_QUANTITY_SPACE = r'%s%s%s' % (POSSIBLY_NEGATIVE_QUANTITY,
00079 csslex.SPACE,
00080 csslex.WHITESPACE)
00081 FOUR_NOTATION_QUANTITY_RE = re.compile(r'%s%s%s%s' %
00082 (POSSIBLY_NEGATIVE_QUANTITY_SPACE,
00083 POSSIBLY_NEGATIVE_QUANTITY_SPACE,
00084 POSSIBLY_NEGATIVE_QUANTITY_SPACE,
00085 POSSIBLY_NEGATIVE_QUANTITY),
00086 re.I)
00087 COLOR = r'(%s|%s)' % (csslex.NAME, csslex.HASH)
00088 COLOR_SPACE = r'%s%s' % (COLOR, csslex.SPACE)
00089 FOUR_NOTATION_COLOR_RE = re.compile(r'(-color%s:%s)%s%s%s(%s)' %
00090 (csslex.WHITESPACE,
00091 csslex.WHITESPACE,
00092 COLOR_SPACE,
00093 COLOR_SPACE,
00094 COLOR_SPACE,
00095 COLOR),
00096 re.I)
00097
00098
00099 CURSOR_EAST_RE = re.compile(LOOKBEHIND_NOT_LETTER + '([ns]?)e-resize')
00100 CURSOR_WEST_RE = re.compile(LOOKBEHIND_NOT_LETTER + '([ns]?)w-resize')
00101
00102
00103
00104
00105
00106
00107 BG_HORIZONTAL_PERCENTAGE_RE = re.compile(r'background(-position)?(%s:%s)'
00108 '([^%%]*?)(%s)%%'
00109 '(%s(?:%s|%s))' % (csslex.WHITESPACE,
00110 csslex.WHITESPACE,
00111 csslex.NUM,
00112 csslex.WHITESPACE,
00113 csslex.QUANTITY,
00114 csslex.IDENT))
00115
00116 BG_HORIZONTAL_PERCENTAGE_X_RE = re.compile(r'background-position-x(%s:%s)'
00117 '(%s)%%' % (csslex.WHITESPACE,
00118 csslex.WHITESPACE,
00119 csslex.NUM))
00120
00121
00122 BODY_SELECTOR = r'body%s{%s' % (csslex.WHITESPACE, csslex.WHITESPACE)
00123
00124
00125 CHARS_WITHIN_SELECTOR = r'[^\}]*?'
00126
00127
00128 DIRECTION_RE = r'direction%s:%s' % (csslex.WHITESPACE, csslex.WHITESPACE)
00129
00130
00131
00132 BODY_DIRECTION_LTR_RE = re.compile(r'(%s)(%s)(%s)(ltr)' %
00133 (BODY_SELECTOR, CHARS_WITHIN_SELECTOR,
00134 DIRECTION_RE),
00135 re.I)
00136 BODY_DIRECTION_RTL_RE = re.compile(r'(%s)(%s)(%s)(rtl)' %
00137 (BODY_SELECTOR, CHARS_WITHIN_SELECTOR,
00138 DIRECTION_RE),
00139 re.I)
00140
00141
00142
00143
00144 DIRECTION_LTR_RE = re.compile(r'%s(ltr)' % DIRECTION_RE)
00145 DIRECTION_RTL_RE = re.compile(r'%s(rtl)' % DIRECTION_RE)
00146
00147
00148
00149
00150
00151
00152 LEFT_RE = re.compile('%s(%s)%s%s' % (LOOKBEHIND_NOT_LETTER,
00153 LEFT,
00154 LOOKAHEAD_NOT_CLOSING_PAREN,
00155 LOOKAHEAD_NOT_OPEN_BRACE),
00156 re.I)
00157 RIGHT_RE = re.compile('%s(%s)%s%s' % (LOOKBEHIND_NOT_LETTER,
00158 RIGHT,
00159 LOOKAHEAD_NOT_CLOSING_PAREN,
00160 LOOKAHEAD_NOT_OPEN_BRACE),
00161 re.I)
00162 LEFT_IN_URL_RE = re.compile('%s(%s)%s' % (LOOKBEHIND_NOT_LETTER,
00163 LEFT,
00164 LOOKAHEAD_FOR_CLOSING_PAREN),
00165 re.I)
00166 RIGHT_IN_URL_RE = re.compile('%s(%s)%s' % (LOOKBEHIND_NOT_LETTER,
00167 RIGHT,
00168 LOOKAHEAD_FOR_CLOSING_PAREN),
00169 re.I)
00170 LTR_IN_URL_RE = re.compile('%s(%s)%s' % (LOOKBEHIND_NOT_LETTER,
00171 LTR,
00172 LOOKAHEAD_FOR_CLOSING_PAREN),
00173 re.I)
00174 RTL_IN_URL_RE = re.compile('%s(%s)%s' % (LOOKBEHIND_NOT_LETTER,
00175 RTL,
00176 LOOKAHEAD_FOR_CLOSING_PAREN),
00177 re.I)
00178
00179 COMMENT_RE = re.compile('(%s)' % csslex.COMMENT, re.I)
00180
00181 NOFLIP_TOKEN = r'\@noflip'
00182
00183
00184
00185 NOFLIP_ANNOTATION = r'/\*%s%s%s\*/' % (csslex.WHITESPACE,
00186 NOFLIP_TOKEN,
00187 csslex. WHITESPACE)
00188
00189
00190
00191
00192
00193 NOFLIP_SINGLE_RE = re.compile(r'(%s%s[^;}]+;?)' % (NOFLIP_ANNOTATION,
00194 LOOKAHEAD_NOT_OPEN_BRACE),
00195 re.I)
00196
00197
00198
00199
00200 NOFLIP_CLASS_RE = re.compile(r'(%s%s})' % (NOFLIP_ANNOTATION,
00201 CHARS_WITHIN_SELECTOR),
00202 re.I)
00203
00204
00205 class Tokenizer:
00206 """Replaces any CSS comments with string tokens and vice versa."""
00207
00208 def __init__(self, token_re, token_string):
00209 """Constructor for the Tokenizer.
00210
00211 Args:
00212 token_re: A regex for the string to be replace by a token.
00213 token_string: The string to put between token delimiters when tokenizing.
00214 """
00215 logging.debug('Tokenizer::init token_string=%s' % token_string)
00216 self.token_re = token_re
00217 self.token_string = token_string
00218 self.originals = []
00219
00220 def Tokenize(self, line):
00221 """Replaces any string matching token_re in line with string tokens.
00222
00223 By passing a function as an argument to the re.sub line below, we bypass
00224 the usual rule where re.sub will only replace the left-most occurrence of
00225 a match by calling the passed in function for each occurrence.
00226
00227 Args:
00228 line: A line to replace token_re matches in.
00229
00230 Returns:
00231 line: A line with token_re matches tokenized.
00232 """
00233 line = self.token_re.sub(self.TokenizeMatches, line)
00234 logging.debug('Tokenizer::Tokenize returns: %s' % line)
00235 return line
00236
00237 def DeTokenize(self, line):
00238 """Replaces tokens with the original string.
00239
00240 Args:
00241 line: A line with tokens.
00242
00243 Returns:
00244 line with any tokens replaced by the original string.
00245 """
00246
00247
00248 for i, original in enumerate(self.originals):
00249 token = '%s%s_%s%s' % (TOKEN_DELIMITER, self.token_string, i + 1,
00250 TOKEN_DELIMITER)
00251 line = line.replace(token, original)
00252 logging.debug('Tokenizer::DeTokenize i:%s w/%s' % (i, token))
00253 logging.debug('Tokenizer::DeTokenize returns: %s' % line)
00254 return line
00255
00256 def TokenizeMatches(self, m):
00257 """Replaces matches with tokens and stores the originals.
00258
00259 Args:
00260 m: A match object.
00261
00262 Returns:
00263 A string token which replaces the CSS comment.
00264 """
00265 logging.debug('Tokenizer::TokenizeMatches %s' % m.group(1))
00266 self.originals.append(m.group(1))
00267 return '%s%s_%s%s' % (TOKEN_DELIMITER,
00268 self.token_string,
00269 len(self.originals),
00270 TOKEN_DELIMITER)
00271
00272
00273 def FixBodyDirectionLtrAndRtl(line):
00274 """Replaces ltr with rtl and vice versa ONLY in the body direction.
00275
00276 Args:
00277 line: A string to replace instances of ltr with rtl.
00278 Returns:
00279 line with direction: ltr and direction: rtl swapped only in body selector.
00280 line = FixBodyDirectionLtrAndRtl('body { direction:ltr }')
00281 line will now be 'body { direction:rtl }'.
00282 """
00283
00284 line = BODY_DIRECTION_LTR_RE.sub('\\1\\2\\3%s' % TMP_TOKEN, line)
00285 line = BODY_DIRECTION_RTL_RE.sub('\\1\\2\\3%s' % LTR, line)
00286 line = line.replace(TMP_TOKEN, RTL)
00287 logging.debug('FixBodyDirectionLtrAndRtl returns: %s' % line)
00288 return line
00289
00290
00291 def FixLeftAndRight(line):
00292 """Replaces left with right and vice versa in line.
00293
00294 Args:
00295 line: A string in which to perform the replacement.
00296
00297 Returns:
00298 line with left and right swapped. For example:
00299 line = FixLeftAndRight('padding-left: 2px; margin-right: 1px;')
00300 line will now be 'padding-right: 2px; margin-left: 1px;'.
00301 """
00302
00303 line = LEFT_RE.sub(TMP_TOKEN, line)
00304 line = RIGHT_RE.sub(LEFT, line)
00305 line = line.replace(TMP_TOKEN, RIGHT)
00306 logging.debug('FixLeftAndRight returns: %s' % line)
00307 return line
00308
00309
00310 def FixLeftAndRightInUrl(line):
00311 """Replaces left with right and vice versa ONLY within background urls.
00312
00313 Args:
00314 line: A string in which to replace left with right and vice versa.
00315
00316 Returns:
00317 line with left and right swapped in the url string. For example:
00318 line = FixLeftAndRightInUrl('background:url(right.png)')
00319 line will now be 'background:url(left.png)'.
00320 """
00321
00322 line = LEFT_IN_URL_RE.sub(TMP_TOKEN, line)
00323 line = RIGHT_IN_URL_RE.sub(LEFT, line)
00324 line = line.replace(TMP_TOKEN, RIGHT)
00325 logging.debug('FixLeftAndRightInUrl returns: %s' % line)
00326 return line
00327
00328
00329 def FixLtrAndRtlInUrl(line):
00330 """Replaces ltr with rtl and vice versa ONLY within background urls.
00331
00332 Args:
00333 line: A string in which to replace ltr with rtl and vice versa.
00334
00335 Returns:
00336 line with left and right swapped. For example:
00337 line = FixLtrAndRtlInUrl('background:url(rtl.png)')
00338 line will now be 'background:url(ltr.png)'.
00339 """
00340
00341 line = LTR_IN_URL_RE.sub(TMP_TOKEN, line)
00342 line = RTL_IN_URL_RE.sub(LTR, line)
00343 line = line.replace(TMP_TOKEN, RTL)
00344 logging.debug('FixLtrAndRtlInUrl returns: %s' % line)
00345 return line
00346
00347
00348 def FixCursorProperties(line):
00349 """Fixes directional CSS cursor properties.
00350
00351 Args:
00352 line: A string to fix CSS cursor properties in.
00353
00354 Returns:
00355 line reformatted with the cursor properties substituted. For example:
00356 line = FixCursorProperties('cursor: ne-resize')
00357 line will now be 'cursor: nw-resize'.
00358 """
00359
00360 line = CURSOR_EAST_RE.sub('\\1' + TMP_TOKEN, line)
00361 line = CURSOR_WEST_RE.sub('\\1e-resize', line)
00362 line = line.replace(TMP_TOKEN, 'w-resize')
00363 logging.debug('FixCursorProperties returns: %s' % line)
00364 return line
00365
00366
00367 def FixFourPartNotation(line):
00368 """Fixes the second and fourth positions in 4 part CSS notation.
00369
00370 Args:
00371 line: A string to fix 4 part CSS notation in.
00372
00373 Returns:
00374 line reformatted with the 4 part notations swapped. For example:
00375 line = FixFourPartNotation('padding: 1px 2px 3px 4px')
00376 line will now be 'padding: 1px 4px 3px 2px'.
00377 """
00378 line = FOUR_NOTATION_QUANTITY_RE.sub('\\1 \\4 \\3 \\2', line)
00379 line = FOUR_NOTATION_COLOR_RE.sub('\\1\\2 \\5 \\4 \\3', line)
00380 logging.debug('FixFourPartNotation returns: %s' % line)
00381 return line
00382
00383
00384 def FixBackgroundPosition(line):
00385 """Fixes horizontal background percentage values in line.
00386
00387 Args:
00388 line: A string to fix horizontal background position values in.
00389
00390 Returns:
00391 line reformatted with the 4 part notations swapped.
00392 """
00393 line = BG_HORIZONTAL_PERCENTAGE_RE.sub(CalculateNewBackgroundPosition, line)
00394 line = BG_HORIZONTAL_PERCENTAGE_X_RE.sub(CalculateNewBackgroundPositionX,
00395 line)
00396 logging.debug('FixBackgroundPosition returns: %s' % line)
00397 return line
00398
00399
00400 def CalculateNewBackgroundPosition(m):
00401 """Fixes horizontal background-position percentages.
00402
00403 This function should be used as an argument to re.sub since it needs to
00404 perform replacement specific calculations.
00405
00406 Args:
00407 m: A match object.
00408
00409 Returns:
00410 A string with the horizontal background position percentage fixed.
00411 BG_HORIZONTAL_PERCENTAGE_RE.sub(FixBackgroundPosition,
00412 'background-position: 75% 50%')
00413 will return 'background-position: 25% 50%'.
00414 """
00415
00416
00417 new_x = str(100-int(m.group(4)))
00418
00419
00420 if m.group(1):
00421 position_string = m.group(1)
00422 else:
00423 position_string = ''
00424
00425 return 'background%s%s%s%s%%%s' % (position_string, m.group(2), m.group(3),
00426 new_x, m.group(5))
00427
00428
00429 def CalculateNewBackgroundPositionX(m):
00430 """Fixes percent based background-position-x.
00431
00432 This function should be used as an argument to re.sub since it needs to
00433 perform replacement specific calculations.
00434
00435 Args:
00436 m: A match object.
00437
00438 Returns:
00439 A string with the background-position-x percentage fixed.
00440 BG_HORIZONTAL_PERCENTAGE_X_RE.sub(CalculateNewBackgroundPosition,
00441 'background-position-x: 75%')
00442 will return 'background-position-x: 25%'.
00443 """
00444
00445
00446 new_x = str(100-int(m.group(2)))
00447
00448 return 'background-position-x%s%s%%' % (m.group(1), new_x)
00449
00450
00451 def ChangeLeftToRightToLeft(lines,
00452 swap_ltr_rtl_in_url=None,
00453 swap_left_right_in_url=None):
00454 """Turns lines into a stream and runs the fixing functions against it.
00455
00456 Args:
00457 lines: An list of CSS lines.
00458 swap_ltr_rtl_in_url: Overrides this flag if param is set.
00459 swap_left_right_in_url: Overrides this flag if param is set.
00460
00461 Returns:
00462 The same lines, but with left and right fixes.
00463 """
00464
00465 global FLAGS
00466
00467
00468 logging.debug('ChangeLeftToRightToLeft swap_ltr_rtl_in_url=%s, '
00469 'swap_left_right_in_url=%s' % (swap_ltr_rtl_in_url,
00470 swap_left_right_in_url))
00471 if swap_ltr_rtl_in_url is None:
00472 swap_ltr_rtl_in_url = FLAGS['swap_ltr_rtl_in_url']
00473 if swap_left_right_in_url is None:
00474 swap_left_right_in_url = FLAGS['swap_left_right_in_url']
00475
00476
00477 logging.debug('LINES COUNT: %s' % len(lines))
00478 line = TOKEN_LINES.join(lines)
00479
00480
00481 noflip_single_tokenizer = Tokenizer(NOFLIP_SINGLE_RE, 'NOFLIP_SINGLE')
00482 line = noflip_single_tokenizer.Tokenize(line)
00483
00484
00485 noflip_class_tokenizer = Tokenizer(NOFLIP_CLASS_RE, 'NOFLIP_CLASS')
00486 line = noflip_class_tokenizer.Tokenize(line)
00487
00488
00489 comment_tokenizer = Tokenizer(COMMENT_RE, 'C')
00490 line = comment_tokenizer.Tokenize(line)
00491
00492
00493 line = FixBodyDirectionLtrAndRtl(line)
00494
00495 if swap_left_right_in_url:
00496 line = FixLeftAndRightInUrl(line)
00497
00498 if swap_ltr_rtl_in_url:
00499 line = FixLtrAndRtlInUrl(line)
00500
00501 line = FixLeftAndRight(line)
00502 line = FixCursorProperties(line)
00503 line = FixFourPartNotation(line)
00504 line = FixBackgroundPosition(line)
00505
00506
00507 line = noflip_single_tokenizer.DeTokenize(line)
00508
00509
00510 line = noflip_class_tokenizer.DeTokenize(line)
00511
00512
00513 line = comment_tokenizer.DeTokenize(line)
00514
00515
00516 lines = line.split(TOKEN_LINES)
00517
00518 return lines
00519
00520 def usage():
00521 """Prints out usage information."""
00522
00523 print 'Usage:'
00524 print ' ./cssjanus.py < file.css > file-rtl.css'
00525 print 'Flags:'
00526 print ' --swap_left_right_in_url: Fixes "left"/"right" string within urls.'
00527 print ' Ex: ./cssjanus.py --swap_left_right_in_url < file.css > file_rtl.css'
00528 print ' --swap_ltr_rtl_in_url: Fixes "ltr"/"rtl" string within urls.'
00529 print ' Ex: ./cssjanus --swap_ltr_rtl_in_url < file.css > file_rtl.css'
00530
00531 def setflags(opts):
00532 """Parse the passed in command line arguments and set the FLAGS global.
00533
00534 Args:
00535 opts: getopt iterable intercepted from argv.
00536 """
00537
00538 global FLAGS
00539
00540
00541 for opt, arg in opts:
00542 logging.debug('opt: %s, arg: %s' % (opt, arg))
00543 if opt in ("-h", "--help"):
00544 usage()
00545 sys.exit()
00546 elif opt in ("-d", "--debug"):
00547 logging.getLogger().setLevel(logging.DEBUG)
00548 elif opt == '--swap_ltr_rtl_in_url':
00549 FLAGS['swap_ltr_rtl_in_url'] = True
00550 elif opt == '--swap_left_right_in_url':
00551 FLAGS['swap_left_right_in_url'] = True
00552
00553
00554 def main(argv):
00555 """Sends stdin lines to ChangeLeftToRightToLeft and writes to stdout."""
00556
00557
00558 try:
00559 opts, args = getopt.getopt(argv, 'hd', ['help', 'debug',
00560 'swap_left_right_in_url',
00561 'swap_ltr_rtl_in_url'])
00562 except getopt.GetoptError:
00563 usage()
00564 sys.exit(2)
00565
00566
00567 setflags(opts)
00568
00569
00570 fixed_lines = ChangeLeftToRightToLeft(sys.stdin.readlines())
00571 sys.stdout.write(''.join(fixed_lines))
00572
00573 if __name__ == '__main__':
00574 main(sys.argv[1:])