# Change the directionality of a block of CSS code from right-to-left to left-to-right. This includes not only
# altering the direction attribute but also altering the 4-argument version of things like padding
# to correctly reflect the change. CSS is also minified, in part to make the processing easier.
#
# Author:: Matt Sanford (mailto:matt@twitter.com)
# Copyright:: Copyright (c) 2011 Twitter, Inc.
# License:: Licensed under the Apache License, Version 2.0
require 'r2/shadow_flipper'
module R2
# Short cut method for providing a one-time CSS change
def self.r2(css)
::R2::Swapper.new.r2(css)
end
# Reuable class for CSS alterations
class Swapper
PROPERTY_MAP = {
'margin-left' => 'margin-right',
'margin-right' => 'margin-left',
'padding-left' => 'padding-right',
'padding-right' => 'padding-left',
'border-left' => 'border-right',
'border-right' => 'border-left',
'border-left-width' => 'border-right-width',
'border-right-width' => 'border-left-width',
'border-radius-bottomleft' => 'border-radius-bottomright',
'border-radius-bottomright' => 'border-radius-bottomleft',
'border-radius-topleft' => 'border-radius-topright',
'border-radius-topright' => 'border-radius-topleft',
'-moz-border-radius-bottomright' => '-moz-border-radius-bottomleft',
'-moz-border-radius-bottomleft' => '-moz-border-radius-bottomright',
'-moz-border-radius-topright' => '-moz-border-radius-topleft',
'-moz-border-radius-topleft' => '-moz-border-radius-topright',
'-webkit-border-top-right-radius' => '-webkit-border-top-left-radius',
'-webkit-border-top-left-radius' => '-webkit-border-top-right-radius',
'-webkit-border-bottom-right-radius' => '-webkit-border-bottom-left-radius',
'-webkit-border-bottom-left-radius' => '-webkit-border-bottom-right-radius',
'left' => 'right',
'right' => 'left'
}
VALUE_PROCS = {
'padding' => lambda {|obj,val| obj.quad_swap(val) },
'margin' => lambda {|obj,val| obj.quad_swap(val) },
'border-radius' => lambda {|obj,val| obj.border_radius_swap(val) },
'-moz-border-radius' => lambda {|obj,val| obj.border_radius_swap(val) },
'-webkit-border-radius' => lambda {|obj,val| obj.border_radius_swap(val) },
'text-align' => lambda {|obj,val| obj.side_swap(val) },
'float' => lambda {|obj,val| obj.side_swap(val) },
'box-shadow' => lambda {|obj,val| obj.shadow_swap(val) },
'-webkit-box-shadow' => lambda {|obj,val| obj.shadow_swap(val) },
'-moz-box-shadow' => lambda {|obj,val| obj.shadow_swap(val) },
'direction' => lambda {|obj,val| obj.direction_swap(val) },
'clear' => lambda {|obj,val| obj.side_swap(val) },
'background-position' => lambda {|obj,val| obj.background_position_swap(val) }
}
# Given a String of CSS perform the full directionality change
def r2(original_css)
css = minimize(original_css)
result = css.gsub(/([^\{\}]+[^\}]|[\}])+?/) do |rule|
# +rule+ can represent a selector (".foo {"), the closing "}" for a selector, or the complete
# body of a a selector
if rule.match(/[\{\}]/)
# it is a selector with "{" or a closing "}", insert as it is. This is
# things like ".foo {" and its matching "}"
rule_str = rule
else
# It is a declaration body, like "padding-left:4px;margin-left:5px;"
rule_str = ""
# Split up the individual rules in the body and process each swap. To handle the
# possible ";" in the url() definitions, like
# url("data;base64") and url("data:image/svg+xml;charset=...")
# a state machine is constructed.
url_rule = nil
rule.split(/;/).each do |part|
if part.match(/url\(/)
url_rule = part
elsif url_rule != nil
url_rule << ";" + part
if part.match(/\)$/)
rule_str << declaration_swap(url_rule)
url_rule = nil
end
else
rule_str << declaration_swap(part)
end
end
end
rule_str
end
return result
end
# Minimize the provided CSS by removing comments, and extra specs
def minimize(css)
return '' unless css
css.gsub(/\/\*[\s\S]+?\*\//, ''). # comments
gsub(/[\n\r]/, ''). # line breaks and carriage returns
gsub(/\s*([:;,\{\}])\s*/, '\1'). # space between selectors, declarations, properties and values
gsub(/\s+/, ' '). # replace multiple spaces with single spaces
gsub(/(\A\s+|\s+\z)/, '') # leading or trailing spaces
end
# Given a single CSS declaration rule (e.g. padding-left: 4px) return the opposing rule (so, padding-right:4px; in this example)
def declaration_swap(decl)
return '' unless decl
matched = decl.match(/([^:]+):(.+)$/)
return '' unless matched
property = matched[1]
value = matched[2]
property = PROPERTY_MAP[property] if PROPERTY_MAP.has_key?(property)
value = VALUE_PROCS[property].call(self, value) if VALUE_PROCS.has_key?(property)
return property + ':' + value + ';'
end
# Given a value of rtl or ltr return the opposing value. All other arguments are ignored and returned unmolested.
def direction_swap(val)
if val == "rtl"
"ltr"
elsif val == "ltr"
"rtl"
else
val
end
end
# Given a value of right or left return the opposing value. All other arguments are ignored and returned unmolested.
def side_swap(val)
if val == "right"
"left"
elsif val == "left"
"right"
else
val
end
end
# Given a 4-argument CSS declaration value (like that of padding or margin) return the opposing
# value. The opposing value swaps the left and right but not the top or bottom. Any unrecognized argument is returned
# unmolested (for example, 2-argument values)
def quad_swap(val)
# 1px 2px 3px 4px => 1px 4px 3px 2px
points = val.to_s.split(/\s+/)
if points && points.length == 4
[points[0], points[3], points[2], points[1]].join(' ')
else
val
end
end
# Given the 2-6 variable declaration for box-shadow convert the direction. Conversion requires inverting the
# horizontal measure only.
def shadow_swap(val)
ShadowFlipper::flip(val)
end
# Border radius uses top-left, top-right, bottom-left, bottom-right, so all values need to be swapped. Additionally,
# two and three value border-radius declarations need to be swapped as well. Vertical radius, specified with a /,
# should be left alone.
def border_radius_swap(val)
# 1px 2px 3px 4px => 1px 4px 3px 2px
points = val.to_s.split(/\s+/)
if points && points.length > 1 && !val.to_s.include?('/')
case points.length
when 4
[points[1], points[0], points[3], points[2]].join(' ')
when 3
[points[1], points[0], points[1], points[2]].join(' ')
when 2
[points[1], points[0]].join(' ')
else val
end
else
val
end
end
# Given a background-position such as left center or 0% 50% return the opposing value e.g right center or 100% 50%
def background_position_swap(val)
if val =~ /left/
val.gsub!('left', 'right')
elsif val =~ /right/
val.gsub!('right', 'left')
end
points = val.strip.split(/\s+/)
# If first point is a percentage-value
if match = points[0].match(/(\d+)%/)
inv = 100 - match[1].to_i # 30% => 70% (100 - x)
val = ["#{inv}%", points[1]].compact.join(' ')
end
# If first point is a unit-value
if match = points[0].match(/^(\d+[a-z]{2,3})/)
val = ["right", match[1], points[1] || "center"].compact.join(' ')
end
val
end
end
end