diff --git a/Rakefile b/Rakefile index 6b7d7aa9..22bfdc56 100644 --- a/Rakefile +++ b/Rakefile @@ -269,6 +269,7 @@ OPTS list.exclude('lib/haml/helpers/xss_mods.rb') list.exclude('lib/sass/plugin/merb.rb') list.exclude('lib/sass/plugin/rails.rb') + list.exclude('lib/sass/less.rb') end.to_a t.options << '--incremental' if Rake.application.top_level_tasks.include?('redoc') t.options += FileList.new(scope('yard/*.rb')).to_a.map {|f| ['-e', f]}.flatten diff --git a/lib/haml/exec.rb b/lib/haml/exec.rb index d452aff6..2e4102c4 100644 --- a/lib/haml/exec.rb +++ b/lib/haml/exec.rb @@ -595,12 +595,12 @@ Options: END opts.on('-F', '--from FORMAT', - 'The format to convert from. Can be css, scss, sass, or sass2.', + 'The format to convert from. Can be css, scss, sass, less, or sass2.', 'sass2 is the same as sass, but updates more old syntax to new.', 'By default, this is inferred from the input filename.', 'If there is none, defaults to css.') do |name| @options[:from] = name.downcase.to_sym - unless [:css, :scss, :sass, :sass2].include?(@options[:from]) + unless [:css, :scss, :sass, :less, :sass2].include?(@options[:from]) raise "Unknown format for sass-convert --from: #{name}" end end @@ -715,6 +715,7 @@ END case input.path when /\.scss$/; :scss when /\.sass$/; :sass + when /\.less$/; :less when /\.css$/; :css end elsif @options[:in_place] @@ -743,6 +744,9 @@ END if @options[:from] == :css require 'sass/css' ::Sass::CSS.new(input.read, @options[:for_tree]).render(@options[:to]) + elsif @options[:from] == :less + require 'sass/less' + Less::Engine.new(input.read).to_tree.to_sass_tree.send("to_#{@options[:to]}", @options[:for_tree]) else if input.is_a?(File) ::Sass::Files.tree_for(input.path, @options[:for_engine]) diff --git a/lib/sass/less.rb b/lib/sass/less.rb new file mode 100755 index 00000000..0bb396f6 --- /dev/null +++ b/lib/sass/less.rb @@ -0,0 +1,205 @@ +#!/usr/bin/env ruby + +require 'less' + +module Less + module StyleSheet + module Mixin4 + def build(env) + selectors.build(env, :mixin).each do |path| + el = path.inject(env.root) do |current, node| + current.descend(node.selector, node) or raise MixinNameError, "#{selectors.text_value} in #{env}" + end + if el.is_a?(Node::Mixin::Def) + env << Node::Mixin::Call.new(el, [], env) + else + sel = path.map {|e| e.sass_selector_str}.join(' ').gsub(' :', ':') + env << Node::Mixin::Extend.new(sel) + end + end + end + end + + module Accessor1 + def build(env) + warn <~]/; "#{@selector} " + else @selector + end + rule = Sass::Tree::RuleNode.new([sel, @name]) + rules.each {|r| rule << r.to_sass_tree} + return rule + end + + def sass_selector_str + case @selector + when /[+>~]/; "#{@selector} #{@name}" + else @selector + @name + end + end + end + + module Mixin + class Extend + include Entity + + def initialize(name) + @name = name + end + + def to_sass_tree + Sass::Tree::ExtendNode.new([@name]) + end + end + + class Call + def to_sass_tree + Sass::Tree::MixinNode.new(@mixin.name.gsub(/^\./, ''), @params.map {|v| v.to_sass_tree}) + end + end + + class Def + def to_sass_tree + mixin = Sass::Tree::MixinDefNode.new(name, @params.map do |v| + [Sass::Script::Variable.new(v), v.value.to_sass_tree] + end) + rules.each {|r| mixin << r.to_sass_tree} + mixin + end + end + end + + class Property + def to_sass_tree + Sass::Tree::PropNode.new([self], @value.to_sass_tree, :new) + end + end + + class Expression + def to_sass_tree + if first.is_a?(Array) + val = map {|e| _to_sass_tree(e)}.inject(nil) do |e, i| + next i unless e + Sass::Script::Operation.new(e, i, :comma) + end + else + val = _to_sass_tree(self) + end + val.options = {} + val + end + + private + + LESS_TO_SASS_OPERATORS = {"-" => :minus, "+" => :plus, "*" => :times, "/" => :div, "=" => :single_eq} + def _to_sass_tree(arr) + return Sass::Script::UnaryOperation.new(_to_sass_tree(arr[1..-1]), :minus) if arr[0] == "-" + + first, rest = _sass_split(arr) + return first if rest.empty? + if rest[0].is_a?(Operator) + return Sass::Script::Operation.new(first, _to_sass_tree(rest[1..-1]), + LESS_TO_SASS_OPERATORS[rest[0]]) + end + + Sass::Script::Operation.new(first, _to_sass_tree(rest), :concat) + end + + def _sass_split(arr) + return arr[0].to_sass_tree, arr[1..-1] unless arr[0] == "(" + parens = 1 + i = arr[1..-1].each_with_index do |e, i| + parens += 1 if e == "(" + parens -= 1 if e == ")" + break i if parens == 0 + end + + return _to_sass_tree(arr[1...i+1]), arr[i+2..-1] + end + end + + class Color + def to_sass_tree + Sass::Script::Color.new(:red => r, :green => g, :blue => b, :alpha => a) + end + end + + class Number + def to_sass_tree + Sass::Script::Number.new(self, [self.unit]) + end + end + + class Variable + def to_sass_tree + if @declaration + Sass::Tree::VariableNode.new(self, @value.to_sass_tree, false) + else + Sass::Script::Variable.new(self) + end + end + end + + class Function + def to_sass_tree + Sass::Script::Funcall.new(self, @args.map {|a| a.to_sass_tree}) + end + end + + class Keyword + def to_sass_tree + Sass::Script::String.new(self) + end + end + + class Anonymous + def to_sass_tree + Sass::Script::String.new(self) + end + end + + class Quoted + def to_sass_tree + Sass::Script::String.new(self, true) + end + end + + class FontFamily + def to_sass_tree + @family.map {|f| f.to_sass_tree}.inject(nil) do |e, f| + next f unless e + Sass::Script::Operation.new(e, f, :comma) + end + end + end + end +end