3 # Copyright 2008 Google Inc. All Rights Reserved.
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
)
30 # Global for the command line flags.
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
}
36 # Generic token delimiter character.
39 # This is a temporary match token we use when swapping strings.
40 TMP_TOKEN
= '%sTMP%s' % (TOKEN_DELIMITER
, TOKEN_DELIMITER
)
42 # Token to be used for joining lines.
43 TOKEN_LINES
= '%sJ%s' % (TOKEN_DELIMITER
, TOKEN_DELIMITER
)
45 # Global constant text strings for CSS value matches.
51 # This is a lookbehind match to ensure that we don't replace instances
52 # of our string token (left, rtl, etc...) if there's a letter in front of it.
53 # Specifically, this prevents replacements like 'background: url(bright.png)'.
54 LOOKBEHIND_NOT_LETTER
= r
'(?<![a-zA-Z])'
56 # This is a lookahead match to make sure we don't replace left and right
57 # in actual classnames, so that we don't break the HTML/CSS dependencies.
58 # Read literally, it says ignore cases where the word left, for instance, is
59 # directly followed by valid classname characters and a curly brace.
60 # ex: .column-left {float: left} will become .column-left {float: right}
61 LOOKAHEAD_NOT_OPEN_BRACE
= (r
'(?!(?:%s|%s|%s|#|\:|\.|\,|\+|>)*?{)' %
62 (csslex
.NMCHAR
, TOKEN_LINES
, csslex
.SPACE
))
65 # These two lookaheads are to test whether or not we are within a
66 # background: url(HERE) situation.
67 # Ref: http://www.w3.org/TR/CSS21/syndata.html#uri
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)
74 # Compile a regex to swap left and right values in 4 part notations.
75 # We need to match negatives and decimal numeric values.
76 # ex. 'margin
: .25em
-2px
3px
0' becomes 'margin
: .25em
0 3px
-2px
'.
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)' %
98 # Compile the cursor resize regexes
99 CURSOR_EAST_RE = re.compile(LOOKBEHIND_NOT_LETTER + '([ns
]?
)e
-resize
')
100 CURSOR_WEST_RE = re.compile(LOOKBEHIND_NOT_LETTER + '([ns
]?
)w
-resize
')
102 # Matches the condition where we need to replace the horizontal component
103 # of a background-position value when expressed in horizontal percentage.
104 # Had to make two regexes because in the case of position-x there is only
105 # one quantity, and otherwise we don't want to match
and change cases with only
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
,
121 # Matches the opening of a body selector.
122 BODY_SELECTOR
= r
'body%s{%s' % (csslex
.WHITESPACE
, csslex
.WHITESPACE
)
124 # Matches anything up until the closing of a selector.
125 CHARS_WITHIN_SELECTOR
= r
'[^\}]*?'
127 # Matches the direction property in a selector.
128 DIRECTION_RE
= r
'direction%s:%s' % (csslex
.WHITESPACE
, csslex
.WHITESPACE
)
130 # These allow us to swap "ltr" with "rtl" and vice versa ONLY within the
131 # body selector and on the same line.
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
,
142 # Allows us to swap "direction:ltr" with "direction:rtl" and
143 # vice versa anywhere in a line.
144 DIRECTION_LTR_RE
= re
.compile(r
'%s(ltr)' % DIRECTION_RE
)
145 DIRECTION_RTL_RE
= re
.compile(r
'%s(rtl)' % DIRECTION_RE
)
147 # We want to be able to switch left with right and vice versa anywhere
148 # we encounter left/right strings, EXCEPT inside the background:url(). The next
149 # two regexes are for that purpose. We have alternate IN_URL versions of the
150 # regexes compiled in case the user passes the flag that they do
151 # actually want to have left and right swapped inside of background:urls.
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'
182 # The NOFLIP_TOKEN inside of a comment. For now, this requires that comments
183 # be in the input, which means users of a css compiler would have to run
184 # this script first if they want this functionality.
185 NOFLIP_ANNOTATION
= r
'/\*%s%s%s\*/' % (csslex
.WHITESPACE
,
189 # After a NOFLIP_ANNOTATION, and within a class selector, we want to be able
190 # to set aside a single rule not to be flipped. We can do this by matching
191 # our NOFLIP annotation and then using a lookahead to make sure there is not
192 # an opening brace before the match.
193 NOFLIP_SINGLE_RE
= re
.compile(r
'(%s%s[^;}]+;?)' % (NOFLIP_ANNOTATION
,
194 LOOKAHEAD_NOT_OPEN_BRACE
),
197 # After a NOFLIP_ANNOTATION, we want to grab anything up until the next } which
198 # means the entire following class block. This will prevent all of its
199 # declarations from being flipped.
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."""
208 def __init__(self
, token_re
, token_string
):
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
)
216 self
.token_re
= token_re
217 self
.token_string
= token_string
220 def Tokenize(self
, line
):
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.
233 line
= self
.token_re
.sub(self
.TokenizeMatches
, line
)
234 logging
.debug('Tokenizer::Tokenize returns: %s' % line
)
237 def DeTokenize(self
, line
):
238 """Replaces tokens with the original string.
241 line: A line with tokens.
244 line with any tokens replaced by the original string.
247 # Put all of the comments back in by their comment token.
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
)
256 def TokenizeMatches(self
, m
):
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))
266 self
.originals
.append(m
.group(1))
267 return '%s%s_%s%s' % (TOKEN_DELIMITER
,
273 def FixBodyDirectionLtrAndRtl(line
):
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
)
291 def FixLeftAndRight(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
)
310 def FixLeftAndRightInUrl(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
)
329 def FixLtrAndRtlInUrl(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
)
348 def FixCursorProperties(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
)
367 def FixFourPartNotation(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
)
384 def FixBackgroundPosition(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
)
400 def CalculateNewBackgroundPosition(m
):
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%'.
416 # The flipped value is the offset from 100%
417 new_x
= str(100-int(m
.group(4)))
419 # Since m.group(1) may very well be None type and we need a string..
421 position_string
= m
.group(1)
425 return 'background%s%s%s%s%%%s' % (position_string
, m
.group(2), m
.group(3),
429 def CalculateNewBackgroundPositionX(m
):
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%'.
445 # The flipped value is the offset from 100%
446 new_x
= str(100-int(m
.group(2)))
448 return 'background-position-x%s%s%%' % (m
.group(1), new_x
)
451 def ChangeLeftToRightToLeft(lines
,
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.
467 # Possibly override flags with params.
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']
476 # Turns the array of lines into a single line stream.
477 logging
.debug('LINES COUNT: %s' % len(lines
))
478 line
= TOKEN_LINES
.join(lines
)
480 # Tokenize any single line rules with the /* noflip */ annotation.
481 noflip_single_tokenizer
= Tokenizer(NOFLIP_SINGLE_RE
, 'NOFLIP_SINGLE')
482 line
= noflip_single_tokenizer
.Tokenize(line
)
484 # Tokenize any class rules with the /* noflip */ annotation.
485 noflip_class_tokenizer
= Tokenizer(NOFLIP_CLASS_RE
, 'NOFLIP_CLASS')
486 line
= noflip_class_tokenizer
.Tokenize(line
)
488 # Tokenize the comments so we can preserve them through the changes.
489 comment_tokenizer
= Tokenizer(COMMENT_RE
, 'C')
490 line
= comment_tokenizer
.Tokenize(line
)
492 # Here starteth the various left/right orientation fixes.
493 line
= FixBodyDirectionLtrAndRtl(line
)
495 if swap_left_right_in_url
:
496 line
= FixLeftAndRightInUrl(line
)
498 if swap_ltr_rtl_in_url
:
499 line
= FixLtrAndRtlInUrl(line
)
501 line
= FixLeftAndRight(line
)
502 line
= FixCursorProperties(line
)
503 line
= FixFourPartNotation(line
)
504 line
= FixBackgroundPosition(line
)
506 # DeTokenize the single line noflips.
507 line
= noflip_single_tokenizer
.DeTokenize(line
)
509 # DeTokenize the class-level noflips.
510 line
= noflip_class_tokenizer
.DeTokenize(line
)
512 # DeTokenize the comments.
513 line
= comment_tokenizer
.DeTokenize(line
)
515 # Rejoin the lines back together.
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.
540 # Parse the arguments.
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
:
566 # Parse and set the flags.
569 # Call the main routine with all our functionality.
570 fixed_lines
= ChangeLeftToRightToLeft(sys
.stdin
.readlines())
571 sys
.stdout
.write(''.join(fixed_lines
))
573 if __name__
== '__main__':