123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- #!/usr/bin/env ruby
- require 'less'
- module Less
- # This is the class that Treetop defines for parsing Less files.
- # Since not everything gets parsed into the AST but is instead resolved at parse-time,
- # we need to override some of it so that it can be converted into Sass.
- module StyleSheet
- # Selector mixins that don't have arguments.
- # This depends only on the syntax at the call site;
- # if it doesn't use parens, it hits this production,
- # regardless of whether the mixin being called has arguments or not.
- module Mixin4
- def build_with_sass(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)
- # Calling a mixin with arguments, which gets compiled to a Sass mixin
- env << Node::Mixin::Call.new(el, [], env)
- else
- # Calling a mixin without arguments, which gets compiled to @extend
- sel = selector_str(path)
- base = selector_str(selector_base(path))
- if base == sel
- env << Node::SassNode.new(Sass::Tree::ExtendNode.new([sel]))
- else
- Sass::Util.sass_warn <<WARNING
- WARNING: Sass doesn't support mixing in selector sequences.
- Replacing "#{sel}" with "@extend #{base}"
- WARNING
- env << Node::SassNode.new(Sass::Tree::CommentNode.new(["// #{sel};"], true, false))
- env << Node::SassNode.new(Sass::Tree::ExtendNode.new([base]))
- end
- end
- end
- end
- alias_method :build_without_sass, :build
- alias_method :build, :build_with_sass
- def selector_base(path)
- el, i = Sass::Util.enum_with_index(path).to_a.reverse.find {|e, i| e.selector !~ /^:{1,2}$/} ||
- [path.first, 0]
- sel = (el.selector =~ /^:{0,2}$/ ? el.selector : "")
- [Node::Element.new(el.name, sel)] + path[i+1..-1]
- end
- def selector_str(path)
- path.map {|e| e.sass_selector_str}.join(' ').gsub(' :', ':')
- end
- end
- # Property and variable declarations.
- # We want to keep track of the line number
- # so we don't space out the variables too much in the generated Sass.
- module Declaration3
- def build_with_sass(env)
- build_without_sass(env)
- env.rules.last.src_line = input.line_of(interval.first)
- end
- alias_method :build_without_sass, :build
- alias_method :build, :build_with_sass
- end
- # Comma-separated selectors.
- # Less breaks these into completely separate nodes.
- # Since we don't want this duplication in the Sass,
- # we modify the production to keep track of the original group
- # so we can reconstruct it later on.
- module Selectors2
- def build_with_sass(env, method)
- arr = build_without_sass(env, method)
- return arr if method == :mixin
- rarr = arr.map {|e| e.top(env)}
- rarr.each {|e| e.group = rarr}
- arr
- end
- alias_method :build_without_sass, :build
- alias_method :build, :build_with_sass
- end
- # Attribute accessors.
- # Sass just flat-out doesn't support these,
- # so we print a warning to that effect and compile them to comments.
- module Accessor1
- def build(env)
- Sass::Util.sass_warn <<WARNING
- WARNING: Sass doesn't support attribute accessors.
- Ignoring #{text_value}
- WARNING
- Node::Anonymous.new("/* #{text_value} */")
- end
- end
- # @import statements.
- # Less handles these during parse-time,
- # so we want to wrap them up as a node in the tree.
- # We also include the nodes, though,
- # since we want to have access to the mixins
- # so we can tell if they take arguments or not.
- # The included nodes are hidden so they don't appear in the output.
- module Import1
- def build_with_sass(env)
- line = input.line_of(interval.first)
- import = Sass::Tree::ImportNode.new(url.value.gsub(/\.less$/, ''))
- import.line = input.line_of(interval.first)
- env << Node::SassNode.new(import)
- old_rules = env.rules.dup
- build_without_sass env
- (env.rules - old_rules).each {|r| r.hide_in_sass = true}
- rescue ImportError => e
- raise Sass::SyntaxError.new("File to import #{url.text_value} not found or unreadable", :line => line)
- end
- alias_method :build_without_sass, :build
- alias_method :build, :build_with_sass
- end
- # The IE-specific `alpha(opacity=@var)`.
- # Less manually resolves the variable here at parse-time.
- # We want to keep the variable around,
- # so we compile this to a function.
- # Less doesn't actually have an `=` operator,
- # but that's okay since it's just getting compiled to Sass anyway.
- module Entity::Alpha1
- def build(env)
- Node::Function.new("alpha",
- [Node::Expression.new([
- Node::Keyword.new("opacity"),
- Node::Operator.new("="),
- variable.build])])
- end
- end
- end
- # The Less AST classes for the document,
- # including both stylesheet-level nodes and expression-level nodes.
- # The main purpose of overriding these is to add `#to_sass_tree` functions
- # for converting to Sass.
- module Node
- module Entity
- attr_accessor :hide_in_sass
- attr_accessor :src_line
- end
- class Element
- attr_accessor :group
- def top(env)
- return self if parent.equal?(env)
- return parent.top(env)
- end
- def to_sass_tree
- if root?
- root = Sass::Tree::RootNode.new("")
- rules.each {|r| root << r.to_sass_tree}
- return root
- end
- return if hide_in_sass
- return if !self.equal?(group.first)
- last_el = nil
- sel = group.map do |el|
- comma_sel = []
- loop do
- comma_sel << el.sass_selector_str
- break unless el.rules.size == 1 && el.rules.first.is_a?(Element)
- el = el.rules.first
- end
- last_el = el
- comma_sel = comma_sel.join(' ').gsub(' :', ':')
- comma_sel.gsub!(/^:/, '&:') unless parent.root?
- comma_sel
- end.join(', ')
- rule = Sass::Tree::RuleNode.new([sel])
- last_el.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 Call
- def to_sass_tree
- return if hide_in_sass
- Sass::Tree::MixinNode.new(@mixin.name.gsub(/^\./, ''), @params.map {|v| v.to_sass_tree}, {})
- end
- end
- class Def
- def to_sass_tree
- return if hide_in_sass
- mixin = Sass::Tree::MixinDefNode.new(name, @params.map do |v|
- v.value.flatten!
- [Sass::Script::Variable.new(v), v.value.to_sass_tree]
- end)
- rules.each {|r| mixin << r.to_sass_tree}
- mixin
- end
- end
- end
- class SassNode
- include Entity
- def initialize(node)
- @node = node
- end
- def to_sass_tree
- return if hide_in_sass
- @node
- end
- end
- class Property
- def to_sass_tree
- return if hide_in_sass
- 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)
- e, rest = _to_sass_tree_plus_minus_eq(arr)
- until rest.empty?
- e2, rest = _to_sass_tree_plus_minus_eq(rest)
- e = Sass::Script::Operation.new(e, e2, :space)
- end
- return e
- end
- def _to_sass_tree_plus_minus_eq(arr)
- e, rest = _to_sass_tree_times_div(arr)
- while rest[0] && rest[0].is_a?(Operator) && %w[+ - =].include?(rest[0])
- op = LESS_TO_SASS_OPERATORS[rest[0]]
- e2, rest = _to_sass_tree_times_div(rest[1..-1])
- e = Sass::Script::Operation.new(e, e2, op)
- end
- return e, rest
- end
- def _to_sass_tree_times_div(arr)
- e, rest = _to_sass_tree_unary(arr)
- while rest[0] && rest[0].is_a?(Operator) && %w[* /].include?(rest[0])
- op = LESS_TO_SASS_OPERATORS[rest[0]]
- e2, rest = _to_sass_tree_unary(rest[1..-1])
- e = Sass::Script::Operation.new(e, e2, op)
- end
- return e, rest
- end
- def _to_sass_tree_unary(arr)
- if arr[0] == "-"
- first, rest = _sass_split(arr[1..-1])
- return Sass::Script::UnaryOperation.new(first, :minus), rest
- else
- return _sass_split(arr[0..-1])
- end
- 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
- return if hide_in_sass
- node = Sass::Tree::VariableNode.new(self, @value.to_sass_tree, false)
- node.line = self.src_line
- node
- 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
- # The entry point to Less.
- # By default Less doesn't preserve the filename of the file being parsed,
- # which is unpleasant for error reporting.
- # Our monkeypatch keeps it around.
- class Engine
- def initialize_with_sass(obj, opts = {})
- initialize_without_sass(obj, opts)
- @filename = obj.path if obj.is_a?(File)
- end
- alias_method :initialize_without_sass, :initialize
- alias_method :initialize, :initialize_with_sass
- def parse_with_sass
- parse_without_sass
- rescue Sass::SyntaxError => e
- e.modify_backtrace(:filename => @filename)
- raise e
- end
- alias_method :parse_without_sass, :parse
- alias_method :parse, :parse_with_sass
- alias_method :to_tree, :parse
- end
- end
|