Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #18 from grundprinzip/master

Automatic Color Extraction and Vendor Prefixed Modules
  • Loading branch information...
commit 6f1cf20fc1e2d0cefad52c92a31f6836715c82bf 2 parents 201177f + 4aca3ff
@thomaspierson authored
Showing with 307 additions and 5 deletions.
  1. +28 −1 bin/css2less
  2. +151 −4 lib/css2less.rb
  3. +128 −0 spec/css2less_spec.rb
View
29 bin/css2less
@@ -1,9 +1,36 @@
#!/usr/bin/env ruby
+require 'optparse'
require 'css2less'
+options = {}
+parser = OptionParser.new do |opts|
+ opts.banner = "Usage: css2less [options] filename"
+
+ opts.on("-c", "--colors", "Automatically extract colors from less") do |v|
+ options[:update_colors] = v
+ end
+
+ opts.on("-m", "--mixins", "Automatically extract vendor prefixed mixins") do |v|
+ options[:vendor_mixins] = v
+ end
+
+ opts.on_tail("-h", "--help", "Show this message") do
+ puts opts
+ exit
+ end
+
+end
+
+parser.parse!
+
+if ARGV.empty?
+ puts parser
+ exit
+end
+
css = File.read(ARGV[0])
-converter = Css2Less::Converter.new(css)
+converter = Css2Less::Converter.new(css, options)
converter.process_less
puts converter.get_less
View
155 lib/css2less.rb
@@ -15,18 +15,42 @@
#
# You should have received a copy of the GNU General Public License
# along with Foobar. If not, see <http://www.gnu.org/licenses/>.
+require 'set'
module Css2Less
+ # These are the official colors
+ CSS_COLORS = Set.new %w{aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgrey darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray grey green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgrey lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen}
+ VENDOR_PREFIXES_LIST = %w{-moz -o -ms -webkit}
+ VENDOR_PREFIXES = /^(-moz|-o|-ms|-webkit)-/
+
require 'enumerator'
+ # This is the CSS2Less converter class.
class Converter
- def initialize(css=nil)
+
+ # This is the constructor of the class
+ #
+ # The following options are supported:
+ #
+ # * Matching colors in the CSS document and replacing them
+ # update_colors => true
+ def initialize(css=nil, options = {})
if not css.nil?
@css = css
end
+
+ # Option merge, instead of rails reverse_merge
+ @options = {:update_colors => false, :vendor_mixins => false}.merge(options)
+
@tree = {}
@less = ''
+
+ # We want to store all color information
+ @colors = {}
+
+ # Storing all vendor prefix mixins here
+ @vendor_mixins = {}
end
def process_less
@@ -57,9 +81,109 @@ def cleanup
@less = ''
end
+ # Split set of rules into single item
+ def convert_rules(data)
+ data.split(';').map { |s| s.strip }.reject { |s| s.empty? }
+ end
+
+ def color?(value)
+ if CSS_COLORS.include?(value.strip) || /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.match(value.strip) != nil ||
+ /(rgba?)\(.*\)/.match(value)
+ true
+ else
+ false
+ end
+ end
+
+ # Check if the global index contains the color and replace the value
+ # accordingly
+ def convert_if_color(color)
+ if color?(color)
+ unless @colors.key?(color.strip)
+ @colors[color.strip] = "@color#{@colors.size}"
+ end
+ @colors[color.strip]
+ else
+ color
+ end
+ end
+
+ # Try to match a color of a set of rules
+ def match_color(style)
+ convert_rules(style).map { |r|
+ (key, value) = r.split(":").map { |e| e.strip }
+ if value.nil?
+ "#{key}"
+ else
+ "#{key}: #{value.split(/\s+/).map { |e| convert_if_color(e) }.join(" ")}"
+ end
+ }.join(";\n") << ";\n"
+ end
+
+ def match_vendor_prefix_mixin(style)
+ normal_rules = {}
+ prefixed_rules = {}
+
+ # First identify all those vendor prefixed rules that are similar
+ convert_rules(style).each { |e|
+ (key, value) = e.split(":").map { |e| e.strip }
+ if value.nil?
+ normal_rules[key] = nil
+ else
+ # If this is a vendor prefixed rule, collect all similar ones in a
+ # single entry
+ if key.match(VENDOR_PREFIXES)
+ rule_key = key.gsub(VENDOR_PREFIXES, "")
+ val = value.split(/\s+/).map { |e| e.strip }
+
+ if prefixed_rules.key?(rule_key) && prefixed_rules[rule_key] != val
+ # Abort, because we have different values for different vendor
+ # prefixed values, this can only mean intended different behavior
+ # for different browsers
+ return style
+ end
+
+ prefixed_rules[rule_key] = val
+ else
+ normal_rules[key] = value
+ end
+ end
+ }
+
+ # Now we have all information to proceed. First, we check if the mixin is
+ # already available globally. If not we announce it
+ prefixed_rules.each { |k,v|
+ unless @vendor_mixins.key?(k)
+ @vendor_mixins[k] = v.size
+ end
+
+ if normal_rules.key?(k)
+ normal_rules.delete(k)
+ normal_rules[".vp-#{k}(#{v.join("; ")})"] = nil
+ end
+ }
+
+ result = normal_rules.to_a.map { |e|
+ val = "#{e[0]}"
+ val << ": #{e[1]}" unless e[1].nil?
+ val}.join(";\n") << ";\n"
+ end
+
+ # This method is called for each selector that we want to add as a rule.
+ # Since we have plain CSS rules here, we should try to bring some order into
+ # the chaos.
def add_rule(tree, selectors, style)
return if style.nil? || style.empty?
+
+ # Stop recursion and add styles
if selectors.empty?
+
+ # Match and replace global colors
+ style = match_color(style) if @options[:update_colors]
+
+ # Match and replace global mixins for vendor specific behavior
+ style = match_vendor_prefix_mixin(style) if @options[:vendor_mixins]
+
(tree[:style] ||= ';') << style
else
first, rest = selectors.first, selectors[1..-1]
@@ -92,18 +216,41 @@ def generate_tree
end
end
+
+ def build_mixin_list(indent)
+ less = ""
+ @vendor_mixins.each { |k,v|
+ args = Array(0..v-1).map { |e| "@p#{e}" }
+ less << ".vp-#{k}(#{args.join("; ")}) {\n"
+ VENDOR_PREFIXES_LIST.each { |vp|
+ less << " " * (indent+4) << "#{vp}-#{k}: #{args.join(" ")};\n"
+ }
+ less << " " * (indent+4) << "#{k}: #{args.join(" ")};\n"
+ less << "}\n"
+ }
+ less << "\n"
+ end
+
def render_less(tree=nil, indent=0)
if tree.nil?
- tree = @tree
+ # This is the initial node, add all global vars / mixins here
+ @colors.each { |k,v|
+ @less << "#{v}: #{k};\n"
+ }
+ @less << "\n" if @colors.size > 0
+
+ @less << build_mixin_list(indent) if @options[:vendor_mixins]
+
+ tree = @tree
end
tree.each do |element, children|
if element == :style
- @less = @less + children.split(';').map { |s| s.strip }.reject { |s| s.empty? }.map { |s| s + ";" }.join("\n") + "\n"
+ @less = @less + convert_rules(children).map { |s| s + ";" }.join("\n") + "\n"
else
@less = @less + ' ' * indent + element + " {\n"
style = children.delete(:style)
if style
- @less = @less + style.split(';').map { |s| s.strip }.reject { |s| s.empty? }.map { |s| ' ' * (indent+4) + s + ";" }.join("\n") + "\n"
+ @less = @less + convert_rules(style).map { |s| ' ' * (indent+4) + s + ";" }.join("\n") + "\n"
end
render_less(children, indent + 4)
@less = @less + ' ' * indent + "}\n"
View
128 spec/css2less_spec.rb
@@ -129,4 +129,132 @@
converter.process_less
converter.get_less.should eq(less)
end
+
+ it "should convert basic css colors into global variables" do
+ css = <<EOF
+#hello {
+ color: blue;
+}
+
+#hello #buddy {
+ background: red;
+ color: #333;
+}
+
+p {
+ color: rgb(1,1,1);
+ border: 1px dotted #e4e9f0;
+}
+EOF
+ less = <<EOF
+@color0: blue;
+@color1: red;
+@color2: #333;
+@color3: rgb(1,1,1);
+@color4: #e4e9f0;
+
+#hello {
+ color: @color0;
+ #buddy {
+ background: @color1;
+ color: @color2;
+ }
+}
+p {
+ color: @color3;
+ border: 1px dotted @color4;
+}
+EOF
+ converter = Css2Less::Converter.new(css, {:update_colors => true})
+ converter.process_less
+ converter.get_less.should eq(less)
+ end
+
+ it "should generate appropriate vendor mixins" do
+ css = <<EOF
+.thumbnail-kenburn img {
+ left:10px;
+ margin-left:-10px;
+ position:relative;
+ -webkit-transition: all 0.8s ease-in-out;
+ -moz-transition: all 0.8s ease-in-out;
+ -o-transition: all 0.8s ease-in-out;
+ -ms-transition: all 0.8s ease-in-out;
+ transition: all 0.8s ease-in-out;
+}
+.thumbnail-kenburn:hover img {
+ -webkit-transform: scale(1.2) rotate(2deg);
+ -moz-transform: scale(1.2) rotate(2deg);
+ -o-transform: scale(1.2) rotate(2deg);
+ -ms-transform: scale(1.2) rotate(2deg);
+ transform: scale(1.2) rotate(2deg);
+}
+
+/*Welcome Block*/
+.service-block .span4 {
+ padding:20px 30px;
+ text-align:center;
+ color: red;
+ margin-bottom:20px;
+ border-radius:2px;
+ -webkit-transition:all 0.3s ease-in-out;
+ -moz-transition:all 0.3s ease-in-out;
+ -o-transition:all 0.3s ease-in-out;
+ transition:all 0.3s ease-in-out;
+}
+EOF
+
+ less = <<EOF
+@color0: red;
+
+.vp-transition(@p0; @p1; @p2) {
+ -moz-transition: @p0 @p1 @p2;
+ -o-transition: @p0 @p1 @p2;
+ -ms-transition: @p0 @p1 @p2;
+ -webkit-transition: @p0 @p1 @p2;
+ transition: @p0 @p1 @p2;
+}
+.vp-transform(@p0; @p1) {
+ -moz-transform: @p0 @p1;
+ -o-transform: @p0 @p1;
+ -ms-transform: @p0 @p1;
+ -webkit-transform: @p0 @p1;
+ transform: @p0 @p1;
+}
+
+.thumbnail-kenburn {
+ img {
+ left: 10px;
+ margin-left: -10px;
+ position: relative;
+ .vp-transition(all;
+ 0.8s;
+ ease-in-out);
+ }
+}
+.thumbnail-kenburn:hover {
+ img {
+ .vp-transform(scale(1.2);
+ rotate(2deg));
+ }
+}
+.service-block {
+ .span4 {
+ padding: 20px 30px;
+ text-align: center;
+ color: @color0;
+ margin-bottom: 20px;
+ border-radius: 2px;
+ .vp-transition(all;
+ 0.3s;
+ ease-in-out);
+ }
+}
+EOF
+
+ converter = Css2Less::Converter.new(css, {:update_colors => true, :vendor_mixins => true})
+ converter.process_less
+ converter.get_less.should eq(less)
+
+ end
end
Please sign in to comment.
Something went wrong with that request. Please try again.