PageRenderTime 89ms CodeModel.GetById 14ms app.highlight 52ms RepoModel.GetById 20ms app.codeStats 0ms

/scalate-jruby/src/main/resources/haml-3.0.25/lib/sass/less.rb

http://github.com/scalate/scalate
Ruby | 382 lines | 313 code | 37 blank | 32 comment | 35 complexity | 0d9d6606124e4ab58348871ad91633a4 MD5 | raw file
  1#!/usr/bin/env ruby
  2
  3require 'less'
  4
  5module Less
  6  # This is the class that Treetop defines for parsing Less files.
  7  # Since not everything gets parsed into the AST but is instead resolved at parse-time,
  8  # we need to override some of it so that it can be converted into Sass.
  9  module StyleSheet
 10    # Selector mixins that don't have arguments.
 11    # This depends only on the syntax at the call site;
 12    # if it doesn't use parens, it hits this production,
 13    # regardless of whether the mixin being called has arguments or not.
 14    module Mixin4
 15      def build_with_sass(env)
 16        selectors.build(env, :mixin).each do |path|
 17          el = path.inject(env.root) do |current, node|
 18            current.descend(node.selector, node) or raise MixinNameError, "#{selectors.text_value} in #{env}"
 19          end
 20          if el.is_a?(Node::Mixin::Def)
 21            # Calling a mixin with arguments, which gets compiled to a Sass mixin
 22            env << Node::Mixin::Call.new(el, [], env)
 23          else
 24            # Calling a mixin without arguments, which gets compiled to @extend
 25            sel = selector_str(path)
 26            base = selector_str(selector_base(path))
 27            if base == sel
 28              env << Node::SassNode.new(Sass::Tree::ExtendNode.new([sel]))
 29            else
 30              Haml::Util.haml_warn <<WARNING
 31WARNING: Sass doesn't support mixing in selector sequences.
 32Replacing "#{sel}" with "@extend #{base}"
 33WARNING
 34              env << Node::SassNode.new(Sass::Tree::CommentNode.new("// #{sel};", true))
 35              env << Node::SassNode.new(Sass::Tree::ExtendNode.new([base]))
 36            end
 37          end
 38        end
 39      end
 40      alias_method :build_without_sass, :build
 41      alias_method :build, :build_with_sass
 42
 43      def selector_base(path)
 44        el, i = Haml::Util.enum_with_index(path).to_a.reverse.find {|e, i| e.selector !~ /^:{1,2}$/} ||
 45          [path.first, 0]
 46        sel = (el.selector =~ /^:{0,2}$/ ? el.selector : "")
 47        [Node::Element.new(el.name, sel)] + path[i+1..-1]
 48      end
 49
 50      def selector_str(path)
 51        path.map {|e| e.sass_selector_str}.join(' ').gsub(' :', ':')
 52      end
 53    end
 54
 55    # Property and variable declarations.
 56    # We want to keep track of the line number
 57    # so we don't space out the variables too much in the generated Sass.
 58    module Declaration3
 59      def build_with_sass(env)
 60        build_without_sass(env)
 61        env.rules.last.src_line = input.line_of(interval.first)
 62      end
 63      alias_method :build_without_sass, :build
 64      alias_method :build, :build_with_sass
 65    end
 66
 67    # Comma-separated selectors.
 68    # Less breaks these into completely separate nodes.
 69    # Since we don't want this duplication in the Sass,
 70    # we modify the production to keep track of the original group
 71    # so we can reconstruct it later on.
 72    module Selectors2
 73      def build_with_sass(env, method)
 74        arr = build_without_sass(env, method)
 75        return arr if method == :mixin
 76        rarr = arr.map {|e| e.top(env)}
 77        rarr.each {|e| e.group = rarr}
 78        arr
 79      end
 80      alias_method :build_without_sass, :build
 81      alias_method :build, :build_with_sass
 82    end
 83
 84    # Attribute accessors.
 85    # Sass just flat-out doesn't support these,
 86    # so we print a warning to that effect and compile them to comments.
 87    module Accessor1
 88      def build(env)
 89        Haml::Util.haml_warn <<WARNING
 90WARNING: Sass doesn't support attribute accessors.
 91Ignoring #{text_value}
 92WARNING
 93        Node::Anonymous.new("/* #{text_value} */")
 94      end
 95    end
 96
 97    # @import statements.
 98    # Less handles these during parse-time,
 99    # so we want to wrap them up as a node in the tree.
100    # We also include the nodes, though,
101    # since we want to have access to the mixins
102    # so we can tell if they take arguments or not.
103    # The included nodes are hidden so they don't appear in the output.
104    module Import1
105      def build_with_sass(env)
106        line = input.line_of(interval.first)
107        import = Sass::Tree::ImportNode.new(url.value.gsub(/\.less$/, ''))
108        import.line = input.line_of(interval.first)
109        env << Node::SassNode.new(import)
110        old_rules = env.rules.dup
111        build_without_sass env
112        (env.rules - old_rules).each {|r| r.hide_in_sass = true}
113      rescue ImportError => e
114        raise Sass::SyntaxError.new("File to import #{url.text_value} not found or unreadable", :line => line)
115      end
116      alias_method :build_without_sass, :build
117      alias_method :build, :build_with_sass
118    end
119
120    # The IE-specific `alpha(opacity=@var)`.
121    # Less manually resolves the variable here at parse-time.
122    # We want to keep the variable around,
123    # so we compile this to a function.
124    # Less doesn't actually have an `=` operator,
125    # but that's okay since it's just getting compiled to Sass anyway.
126    module Entity::Alpha1
127      def build(env)
128        Node::Function.new("alpha",
129          [Node::Expression.new([
130                Node::Keyword.new("opacity"),
131                Node::Operator.new("="),
132                variable.build])])
133      end
134    end
135  end
136
137  # The Less AST classes for the document,
138  # including both stylesheet-level nodes and expression-level nodes.
139  # The main purpose of overriding these is to add `#to_sass_tree` functions
140  # for converting to Sass.
141  module Node
142    module Entity
143      attr_accessor :hide_in_sass
144      attr_accessor :src_line
145    end
146
147    class Element
148      attr_accessor :group
149
150      def top(env)
151        return self if parent.equal?(env)
152        return parent.top(env)
153      end
154
155      def to_sass_tree
156        if root?
157          root = Sass::Tree::RootNode.new("")
158          rules.each {|r| root << r.to_sass_tree}
159          return root
160        end
161        return if hide_in_sass
162        return if !self.equal?(group.first)
163
164        last_el = nil
165        sel = group.map do |el|
166          comma_sel = []
167          loop do
168            comma_sel << el.sass_selector_str
169            break unless el.rules.size == 1 && el.rules.first.is_a?(Element)
170            el = el.rules.first
171          end
172          last_el = el
173          comma_sel = comma_sel.join(' ').gsub(' :', ':')
174          comma_sel.gsub!(/^:/, '&:') unless parent.root?
175          comma_sel
176        end.join(', ')
177
178        rule = Sass::Tree::RuleNode.new([sel])
179        last_el.rules.each {|r| rule << r.to_sass_tree}
180        return rule
181      end
182
183      def sass_selector_str
184        case @selector
185        when /[+>~]/; "#{@selector} #{@name}"
186        else @selector + @name
187        end
188      end
189    end
190
191    module Mixin
192      class Call
193        def to_sass_tree
194          return if hide_in_sass
195          Sass::Tree::MixinNode.new(@mixin.name.gsub(/^\./, ''), @params.map {|v| v.to_sass_tree})
196        end
197      end
198
199      class Def
200        def to_sass_tree
201          return if hide_in_sass
202          mixin = Sass::Tree::MixinDefNode.new(name, @params.map do |v|
203              v.value.flatten!
204              [Sass::Script::Variable.new(v), v.value.to_sass_tree]
205            end)
206          rules.each {|r| mixin << r.to_sass_tree}
207          mixin
208        end
209      end
210    end
211
212    class SassNode
213      include Entity
214
215      def initialize(node)
216        @node = node
217      end
218
219      def to_sass_tree
220        return if hide_in_sass
221        @node
222      end
223    end
224
225    class Property
226      def to_sass_tree
227        return if hide_in_sass
228        Sass::Tree::PropNode.new([self], @value.to_sass_tree, :new)
229      end
230    end
231
232    class Expression
233      def to_sass_tree
234        if first.is_a?(Array)
235          val = map {|e| _to_sass_tree(e)}.inject(nil) do |e, i|
236            next i unless e
237            Sass::Script::Operation.new(e, i, :comma)
238          end
239        else
240          val = _to_sass_tree(self)
241        end
242        val.options = {}
243        val
244      end
245
246      private
247
248      LESS_TO_SASS_OPERATORS = {"-" => :minus, "+" => :plus, "*" => :times, "/" => :div, "=" => :single_eq}
249
250      def _to_sass_tree(arr)
251        e, rest = _to_sass_tree_plus_minus_eq(arr)
252        until rest.empty?
253          e2, rest = _to_sass_tree_plus_minus_eq(rest)
254          e = Sass::Script::Operation.new(e, e2, :concat)
255        end
256        return e
257      end
258
259      def _to_sass_tree_plus_minus_eq(arr)
260        e, rest = _to_sass_tree_times_div(arr)
261        while rest[0] && rest[0].is_a?(Operator) && %w[+ - =].include?(rest[0])
262          op = LESS_TO_SASS_OPERATORS[rest[0]]
263          e2, rest = _to_sass_tree_times_div(rest[1..-1])
264          e = Sass::Script::Operation.new(e, e2, op)
265        end
266        return e, rest
267      end
268
269      def _to_sass_tree_times_div(arr)
270        e, rest = _to_sass_tree_unary(arr)
271        while rest[0] && rest[0].is_a?(Operator) && %w[* /].include?(rest[0])
272          op = LESS_TO_SASS_OPERATORS[rest[0]]
273          e2, rest = _to_sass_tree_unary(rest[1..-1])
274          e = Sass::Script::Operation.new(e, e2, op)
275        end
276        return e, rest
277      end
278
279      def _to_sass_tree_unary(arr)
280        if arr[0] == "-"
281          first, rest = _sass_split(arr[1..-1])
282          return Sass::Script::UnaryOperation.new(first, :minus), rest
283        else
284          return _sass_split(arr[0..-1])
285        end
286      end
287
288      def _sass_split(arr)
289        return arr[0].to_sass_tree, arr[1..-1] unless arr[0] == "("
290        parens = 1
291        i = arr[1..-1].each_with_index do |e, i|
292          parens += 1 if e == "("
293          parens -= 1 if e == ")"
294          break i if parens == 0
295        end
296
297        return _to_sass_tree(arr[1...i+1]), arr[i+2..-1]
298      end
299    end
300
301    class Color
302      def to_sass_tree
303        Sass::Script::Color.new(:red => r, :green => g, :blue => b, :alpha => a)
304      end
305    end
306
307    class Number
308      def to_sass_tree
309        Sass::Script::Number.new(self, [self.unit])
310      end
311    end
312
313    class Variable
314      def to_sass_tree
315        if @declaration
316          return if hide_in_sass
317          node = Sass::Tree::VariableNode.new(self, @value.to_sass_tree, false)
318          node.line = self.src_line
319          node
320        else
321          Sass::Script::Variable.new(self)
322        end
323      end
324    end
325
326    class Function
327      def to_sass_tree
328        Sass::Script::Funcall.new(self, @args.map {|a| a.to_sass_tree})
329      end
330    end
331
332    class Keyword
333      def to_sass_tree
334        Sass::Script::String.new(self)
335      end
336    end
337
338    class Anonymous
339      def to_sass_tree
340        Sass::Script::String.new(self)
341      end
342    end
343
344    class Quoted
345      def to_sass_tree
346        Sass::Script::String.new(self, true)
347      end
348    end
349
350    class FontFamily
351      def to_sass_tree
352        @family.map {|f| f.to_sass_tree}.inject(nil) do |e, f|
353          next f unless e
354          Sass::Script::Operation.new(e, f, :comma)
355        end
356      end
357    end
358  end
359
360  # The entry point to Less.
361  # By default Less doesn't preserve the filename of the file being parsed,
362  # which is unpleasant for error reporting.
363  # Our monkeypatch keeps it around.
364  class Engine
365    def initialize_with_sass(obj, opts = {})
366      initialize_without_sass(obj, opts)
367      @filename = obj.path if obj.is_a?(File)
368    end
369    alias_method :initialize_without_sass, :initialize
370    alias_method :initialize, :initialize_with_sass
371
372    def parse_with_sass
373      parse_without_sass
374    rescue Sass::SyntaxError => e
375      e.modify_backtrace(:filename => @filename)
376      raise e
377    end
378    alias_method :parse_without_sass, :parse
379    alias_method :parse, :parse_with_sass
380    alias_method :to_tree, :parse
381  end
382end