5 """Converts a LeftToRight Cascading Style Sheet into a RightToLeft one.
7 This is a utility script for replacing "left" oriented things in a CSS file
8 like float, padding, margin with "right" oriented values.
9 It also does the opposite.
10 The goal is to be able to conditionally serve one large, cat'd, compiled CSS
11 file appropriate for LeftToRight oriented languages and RightToLeft ones.
12 This utility will hopefully help your structural layout done in CSS in
13 terms of its RTL compatibility. It will not help with some of the more
14 complicated bidirectional text issues.
17 __author__ =
'elsigh@google.com (Lindsey Simon)'
28 logging.getLogger().setLevel(logging.INFO)
31 SWAP_LTR_RTL_IN_URL_DEFAULT =
False
32 SWAP_LEFT_RIGHT_IN_URL_DEFAULT =
False
33 FLAGS = {
'swap_ltr_rtl_in_url': SWAP_LTR_RTL_IN_URL_DEFAULT,
34 'swap_left_right_in_url': SWAP_LEFT_RIGHT_IN_URL_DEFAULT}
40 TMP_TOKEN =
'%sTMP%s' % (TOKEN_DELIMITER, TOKEN_DELIMITER)
43 TOKEN_LINES =
'%sJ%s' % (TOKEN_DELIMITER, TOKEN_DELIMITER)
54 LOOKBEHIND_NOT_LETTER =
r'(?<![a-zA-Z])'
61 LOOKAHEAD_NOT_OPEN_BRACE = (
r'(?!(?:%s|%s|%s|#|\:|\.|\,|\+|>)*?{)' %
62 (csslex.NMCHAR, TOKEN_LINES, csslex.SPACE))
68 VALID_AFTER_URI_CHARS =
r'[\'\"]?%s' % csslex.WHITESPACE
69 LOOKAHEAD_NOT_CLOSING_PAREN =
r'(?!%s?%s\))' % (csslex.URL_CHARS,
70 VALID_AFTER_URI_CHARS)
71 LOOKAHEAD_FOR_CLOSING_PAREN =
r'(?=%s?%s\))' % (csslex.URL_CHARS,
72 VALID_AFTER_URI_CHARS)
77 POSSIBLY_NEGATIVE_QUANTITY =
r'((?:-?%s)|(?:inherit|auto))' % csslex.QUANTITY
78 POSSIBLY_NEGATIVE_QUANTITY_SPACE =
r'%s%s%s' % (POSSIBLY_NEGATIVE_QUANTITY,
81 FOUR_NOTATION_QUANTITY_RE = re.compile(
r'%s%s%s%s' %
82 (POSSIBLY_NEGATIVE_QUANTITY_SPACE,
83 POSSIBLY_NEGATIVE_QUANTITY_SPACE,
84 POSSIBLY_NEGATIVE_QUANTITY_SPACE,
85 POSSIBLY_NEGATIVE_QUANTITY),
87 COLOR =
r'(%s|%s)' % (csslex.NAME, csslex.HASH)
88 COLOR_SPACE =
r'%s%s' % (COLOR, csslex.SPACE)
89 FOUR_NOTATION_COLOR_RE = re.compile(
r'(-color%s:%s)%s%s%s(%s)' %
99 CURSOR_EAST_RE = re.compile(LOOKBEHIND_NOT_LETTER +
'([ns]?)e-resize')
100 CURSOR_WEST_RE = re.compile(LOOKBEHIND_NOT_LETTER +
'([ns]?)w-resize')
107 BG_HORIZONTAL_PERCENTAGE_RE = re.compile(
r'background(-position)?(%s:%s)'
109 '(%s(?:%s|%s))' % (csslex.WHITESPACE,
116 BG_HORIZONTAL_PERCENTAGE_X_RE = re.compile(
r'background-position-x(%s:%s)'
117 '(%s)%%' % (csslex.WHITESPACE,
122 BODY_SELECTOR =
r'body%s{%s' % (csslex.WHITESPACE, csslex.WHITESPACE)
125 CHARS_WITHIN_SELECTOR =
r'[^\}]*?'
128 DIRECTION_RE =
r'direction%s:%s' % (csslex.WHITESPACE, csslex.WHITESPACE)
132 BODY_DIRECTION_LTR_RE = re.compile(
r'(%s)(%s)(%s)(ltr)' %
133 (BODY_SELECTOR, CHARS_WITHIN_SELECTOR,
136 BODY_DIRECTION_RTL_RE = re.compile(
r'(%s)(%s)(%s)(rtl)' %
137 (BODY_SELECTOR, CHARS_WITHIN_SELECTOR,
144 DIRECTION_LTR_RE = re.compile(
r'%s(ltr)' % DIRECTION_RE)
145 DIRECTION_RTL_RE = re.compile(
r'%s(rtl)' % DIRECTION_RE)
152 LEFT_RE = re.compile(
'%s(%s)%s%s' % (LOOKBEHIND_NOT_LETTER,
154 LOOKAHEAD_NOT_CLOSING_PAREN,
155 LOOKAHEAD_NOT_OPEN_BRACE),
157 RIGHT_RE = re.compile(
'%s(%s)%s%s' % (LOOKBEHIND_NOT_LETTER,
159 LOOKAHEAD_NOT_CLOSING_PAREN,
160 LOOKAHEAD_NOT_OPEN_BRACE),
162 LEFT_IN_URL_RE = re.compile(
'%s(%s)%s' % (LOOKBEHIND_NOT_LETTER,
164 LOOKAHEAD_FOR_CLOSING_PAREN),
166 RIGHT_IN_URL_RE = re.compile(
'%s(%s)%s' % (LOOKBEHIND_NOT_LETTER,
168 LOOKAHEAD_FOR_CLOSING_PAREN),
170 LTR_IN_URL_RE = re.compile(
'%s(%s)%s' % (LOOKBEHIND_NOT_LETTER,
172 LOOKAHEAD_FOR_CLOSING_PAREN),
174 RTL_IN_URL_RE = re.compile(
'%s(%s)%s' % (LOOKBEHIND_NOT_LETTER,
176 LOOKAHEAD_FOR_CLOSING_PAREN),
179 COMMENT_RE = re.compile(
'(%s)' % csslex.COMMENT, re.I)
181 NOFLIP_TOKEN =
r'\@noflip'
185 NOFLIP_ANNOTATION =
r'/\*%s%s%s\*/' % (csslex.WHITESPACE,
193 NOFLIP_SINGLE_RE = re.compile(
r'(%s%s[^;}]+;?)' % (NOFLIP_ANNOTATION,
194 LOOKAHEAD_NOT_OPEN_BRACE),
200 NOFLIP_CLASS_RE = re.compile(
r'(%s%s})' % (NOFLIP_ANNOTATION,
201 CHARS_WITHIN_SELECTOR),
206 """Replaces any CSS comments with string tokens and vice versa."""
209 """Constructor for the Tokenizer.
212 token_re: A regex for the string to be replace by a token.
213 token_string: The string to put between token delimiters when tokenizing.
215 logging.debug(
'Tokenizer::init token_string=%s' % token_string)
221 """Replaces any string matching token_re in line with string tokens.
223 By passing a function as an argument to the re.sub line below, we bypass
224 the usual rule where re.sub will only replace the left-most occurrence of
225 a match by calling the passed in function for each occurrence.
228 line: A line to replace token_re matches in.
231 line: A line with token_re matches tokenized.
234 logging.debug(
'Tokenizer::Tokenize returns: %s' % line)
238 """Replaces tokens with the original string.
241 line: A line with tokens.
244 line with any tokens replaced by the original string.
248 for i, original
in enumerate(self.
originals):
249 token =
'%s%s_%s%s' % (TOKEN_DELIMITER, self.
token_string, i + 1,
251 line = line.replace(token, original)
252 logging.debug(
'Tokenizer::DeTokenize i:%s w/%s' % (i, token))
253 logging.debug(
'Tokenizer::DeTokenize returns: %s' % line)
257 """Replaces matches with tokens and stores the originals.
263 A string token which replaces the CSS comment.
265 logging.debug(
'Tokenizer::TokenizeMatches %s' % m.group(1))
267 return '%s%s_%s%s' % (TOKEN_DELIMITER,
274 """Replaces ltr with rtl and vice versa ONLY in the body direction.
277 line: A string to replace instances of ltr with rtl.
279 line with direction: ltr and direction: rtl swapped only in body selector.
280 line = FixBodyDirectionLtrAndRtl('body { direction:ltr }')
281 line will now be 'body { direction:rtl }'.
284 line = BODY_DIRECTION_LTR_RE.sub(
'\\1\\2\\3%s' % TMP_TOKEN, line)
285 line = BODY_DIRECTION_RTL_RE.sub(
'\\1\\2\\3%s' % LTR, line)
286 line = line.replace(TMP_TOKEN, RTL)
287 logging.debug(
'FixBodyDirectionLtrAndRtl returns: %s' % line)
292 """Replaces left with right and vice versa in line.
295 line: A string in which to perform the replacement.
298 line with left and right swapped. For example:
299 line = FixLeftAndRight('padding-left: 2px; margin-right: 1px;')
300 line will now be 'padding-right: 2px; margin-left: 1px;'.
303 line = LEFT_RE.sub(TMP_TOKEN, line)
304 line = RIGHT_RE.sub(LEFT, line)
305 line = line.replace(TMP_TOKEN, RIGHT)
306 logging.debug(
'FixLeftAndRight returns: %s' % line)
311 """Replaces left with right and vice versa ONLY within background urls.
314 line: A string in which to replace left with right and vice versa.
317 line with left and right swapped in the url string. For example:
318 line = FixLeftAndRightInUrl('background:url(right.png)')
319 line will now be 'background:url(left.png)'.
322 line = LEFT_IN_URL_RE.sub(TMP_TOKEN, line)
323 line = RIGHT_IN_URL_RE.sub(LEFT, line)
324 line = line.replace(TMP_TOKEN, RIGHT)
325 logging.debug(
'FixLeftAndRightInUrl returns: %s' % line)
330 """Replaces ltr with rtl and vice versa ONLY within background urls.
333 line: A string in which to replace ltr with rtl and vice versa.
336 line with left and right swapped. For example:
337 line = FixLtrAndRtlInUrl('background:url(rtl.png)')
338 line will now be 'background:url(ltr.png)'.
341 line = LTR_IN_URL_RE.sub(TMP_TOKEN, line)
342 line = RTL_IN_URL_RE.sub(LTR, line)
343 line = line.replace(TMP_TOKEN, RTL)
344 logging.debug(
'FixLtrAndRtlInUrl returns: %s' % line)
349 """Fixes directional CSS cursor properties.
352 line: A string to fix CSS cursor properties in.
355 line reformatted with the cursor properties substituted. For example:
356 line = FixCursorProperties('cursor: ne-resize')
357 line will now be 'cursor: nw-resize'.
360 line = CURSOR_EAST_RE.sub(
'\\1' + TMP_TOKEN, line)
361 line = CURSOR_WEST_RE.sub(
'\\1e-resize', line)
362 line = line.replace(TMP_TOKEN,
'w-resize')
363 logging.debug(
'FixCursorProperties returns: %s' % line)
368 """Fixes the second and fourth positions in 4 part CSS notation.
371 line: A string to fix 4 part CSS notation in.
374 line reformatted with the 4 part notations swapped. For example:
375 line = FixFourPartNotation('padding: 1px 2px 3px 4px')
376 line will now be 'padding: 1px 4px 3px 2px'.
378 line = FOUR_NOTATION_QUANTITY_RE.sub(
'\\1 \\4 \\3 \\2', line)
379 line = FOUR_NOTATION_COLOR_RE.sub(
'\\1\\2 \\5 \\4 \\3', line)
380 logging.debug(
'FixFourPartNotation returns: %s' % line)
385 """Fixes horizontal background percentage values in line.
388 line: A string to fix horizontal background position values in.
391 line reformatted with the 4 part notations swapped.
393 line = BG_HORIZONTAL_PERCENTAGE_RE.sub(CalculateNewBackgroundPosition, line)
394 line = BG_HORIZONTAL_PERCENTAGE_X_RE.sub(CalculateNewBackgroundPositionX,
396 logging.debug(
'FixBackgroundPosition returns: %s' % line)
401 """Fixes horizontal background-position percentages.
403 This function should be used as an argument to re.sub since it needs to
404 perform replacement specific calculations.
410 A string with the horizontal background position percentage fixed.
411 BG_HORIZONTAL_PERCENTAGE_RE.sub(FixBackgroundPosition,
412 'background-position: 75% 50%')
413 will return 'background-position: 25% 50%'.
417 new_x = str(100-int(m.group(4)))
421 position_string = m.group(1)
425 return 'background%s%s%s%s%%%s' % (position_string, m.group(2), m.group(3),
430 """Fixes percent based background-position-x.
432 This function should be used as an argument to re.sub since it needs to
433 perform replacement specific calculations.
439 A string with the background-position-x percentage fixed.
440 BG_HORIZONTAL_PERCENTAGE_X_RE.sub(CalculateNewBackgroundPosition,
441 'background-position-x: 75%')
442 will return 'background-position-x: 25%'.
446 new_x = str(100-int(m.group(2)))
448 return 'background-position-x%s%s%%' % (m.group(1), new_x)
452 swap_ltr_rtl_in_url=None,
453 swap_left_right_in_url=None):
454 """Turns lines into a stream and runs the fixing functions against it.
457 lines: An list of CSS lines.
458 swap_ltr_rtl_in_url: Overrides this flag if param is set.
459 swap_left_right_in_url: Overrides this flag if param is set.
462 The same lines, but with left and right fixes.
468 logging.debug(
'ChangeLeftToRightToLeft swap_ltr_rtl_in_url=%s, '
469 'swap_left_right_in_url=%s' % (swap_ltr_rtl_in_url,
470 swap_left_right_in_url))
471 if swap_ltr_rtl_in_url
is None:
472 swap_ltr_rtl_in_url = FLAGS[
'swap_ltr_rtl_in_url']
473 if swap_left_right_in_url
is None:
474 swap_left_right_in_url = FLAGS[
'swap_left_right_in_url']
477 logging.debug(
'LINES COUNT: %s' % len(lines))
478 line = TOKEN_LINES.join(lines)
481 noflip_single_tokenizer =
Tokenizer(NOFLIP_SINGLE_RE,
'NOFLIP_SINGLE')
482 line = noflip_single_tokenizer.Tokenize(line)
485 noflip_class_tokenizer =
Tokenizer(NOFLIP_CLASS_RE,
'NOFLIP_CLASS')
486 line = noflip_class_tokenizer.Tokenize(line)
489 comment_tokenizer =
Tokenizer(COMMENT_RE,
'C')
490 line = comment_tokenizer.Tokenize(line)
495 if swap_left_right_in_url:
498 if swap_ltr_rtl_in_url:
507 line = noflip_single_tokenizer.DeTokenize(line)
510 line = noflip_class_tokenizer.DeTokenize(line)
513 line = comment_tokenizer.DeTokenize(line)
516 lines = line.split(TOKEN_LINES)
521 """Prints out usage information."""
524 print ' ./cssjanus.py < file.css > file-rtl.css'
526 print ' --swap_left_right_in_url: Fixes "left"/"right" string within urls.'
527 print ' Ex: ./cssjanus.py --swap_left_right_in_url < file.css > file_rtl.css'
528 print ' --swap_ltr_rtl_in_url: Fixes "ltr"/"rtl" string within urls.'
529 print ' Ex: ./cssjanus --swap_ltr_rtl_in_url < file.css > file_rtl.css'
532 """Parse the passed in command line arguments and set the FLAGS global.
535 opts: getopt iterable intercepted from argv.
541 for opt, arg
in opts:
542 logging.debug(
'opt: %s, arg: %s' % (opt, arg))
543 if opt
in (
"-h",
"--help"):
546 elif opt
in (
"-d",
"--debug"):
547 logging.getLogger().setLevel(logging.DEBUG)
548 elif opt ==
'--swap_ltr_rtl_in_url':
549 FLAGS[
'swap_ltr_rtl_in_url'] =
True
550 elif opt ==
'--swap_left_right_in_url':
551 FLAGS[
'swap_left_right_in_url'] =
True
555 """Sends stdin lines to ChangeLeftToRightToLeft and writes to stdout."""
559 opts, args = getopt.getopt(argv,
'hd', [
'help',
'debug',
560 'swap_left_right_in_url',
561 'swap_ltr_rtl_in_url'])
562 except getopt.GetoptError:
571 sys.stdout.write(
''.join(fixed_lines))
573 if __name__ ==
'__main__':