PageRenderTime 49ms CodeModel.GetById 16ms app.highlight 29ms RepoModel.GetById 1ms app.codeStats 0ms

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

http://github.com/scalate/scalate
Ruby | 294 lines | 131 code | 23 blank | 140 comment | 21 complexity | 97e9754b726f04f7424a9d6e69975225 MD5 | raw file
  1require File.dirname(__FILE__) + '/../sass'
  2require 'sass/tree/node'
  3require 'sass/scss/css_parser'
  4require 'strscan'
  5
  6module Sass
  7  # This class converts CSS documents into Sass or SCSS templates.
  8  # It works by parsing the CSS document into a {Sass::Tree} structure,
  9  # and then applying various transformations to the structure
 10  # to produce more concise and idiomatic Sass/SCSS.
 11  #
 12  # Example usage:
 13  #
 14  #     Sass::CSS.new("p { color: blue }").render(:sass) #=> "p\n  color: blue"
 15  #     Sass::CSS.new("p { color: blue }").render(:scss) #=> "p {\n  color: blue; }"
 16  class CSS
 17    # @param template [String] The CSS stylesheet.
 18    #   This stylesheet can be encoded using any encoding
 19    #   that can be converted to Unicode.
 20    #   If the stylesheet contains an `@charset` declaration,
 21    #   that overrides the Ruby encoding
 22    #   (see {file:SASS_REFERENCE.md#encodings the encoding documentation})
 23    # @option options :old [Boolean] (false)
 24    #     Whether or not to output old property syntax
 25    #     (`:color blue` as opposed to `color: blue`).
 26    #     This is only meaningful when generating Sass code,
 27    #     rather than SCSS.
 28    def initialize(template, options = {})
 29      if template.is_a? IO
 30        template = template.read
 31      end
 32
 33      @options = options.dup
 34      # Backwards compatibility
 35      @options[:old] = true if @options[:alternate] == false
 36      @template = template
 37    end
 38
 39    # Converts the CSS template into Sass or SCSS code.
 40    #
 41    # @param fmt [Symbol] `:sass` or `:scss`, designating the format to return.
 42    # @return [String] The resulting Sass or SCSS code
 43    # @raise [Sass::SyntaxError] if there's an error parsing the CSS template
 44    def render(fmt = :sass)
 45      check_encoding!
 46      build_tree.send("to_#{fmt}", @options).strip + "\n"
 47    rescue Sass::SyntaxError => err
 48      err.modify_backtrace(:filename => @options[:filename] || '(css)')
 49      raise err
 50    end
 51
 52    # Returns the original encoding of the document,
 53    # or `nil` under Ruby 1.8.
 54    #
 55    # @return [Encoding, nil]
 56    # @raise [Encoding::UndefinedConversionError] if the source encoding
 57    #   cannot be converted to UTF-8
 58    # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
 59    def source_encoding
 60      check_encoding!
 61      @original_encoding
 62    end
 63
 64    private
 65
 66    def check_encoding!
 67      return if @checked_encoding
 68      @checked_encoding = true
 69      @template, @original_encoding = Haml::Util.check_sass_encoding(@template) do |msg, line|
 70        raise Sass::SyntaxError.new(msg, :line => line)
 71      end
 72    end
 73
 74    # Parses the CSS template and applies various transformations
 75    #
 76    # @return [Tree::Node] The root node of the parsed tree
 77    def build_tree
 78      root = Sass::SCSS::CssParser.new(@template).parse
 79      expand_commas      root
 80      parent_ref_rules   root
 81      remove_parent_refs root
 82      flatten_rules      root
 83      fold_commas        root
 84      root
 85    end
 86
 87    # Transform
 88    #
 89    #     foo, bar, baz
 90    #       color: blue
 91    #
 92    # into
 93    #
 94    #     foo
 95    #       color: blue
 96    #     bar
 97    #       color: blue
 98    #     baz
 99    #       color: blue
100    #
101    # @param root [Tree::Node] The parent node
102    def expand_commas(root)
103      root.children.map! do |child|
104        unless child.is_a?(Tree::RuleNode) && child.rule.first.include?(',')
105          expand_commas(child) if child.is_a?(Tree::DirectiveNode)
106          next child
107        end
108        child.rule.first.split(',').map do |rule|
109          node = Tree::RuleNode.new([rule.strip])
110          node.children = child.children
111          node
112        end
113      end
114      root.children.flatten!
115    end
116
117    # Make rules use parent refs so that
118    #
119    #     foo
120    #       color: green
121    #     foo.bar
122    #       color: blue
123    #
124    # becomes
125    #
126    #     foo
127    #       color: green
128    #       &.bar
129    #         color: blue
130    #
131    # This has the side effect of nesting rules,
132    # so that
133    #
134    #     foo
135    #       color: green
136    #     foo bar
137    #       color: red
138    #     foo baz
139    #       color: blue
140    #
141    # becomes
142    #
143    #     foo
144    #       color: green
145    #       & bar
146    #         color: red
147    #       & baz
148    #         color: blue
149    #
150    # @param root [Tree::Node] The parent node
151    def parent_ref_rules(root)
152      current_rule = nil
153      root.children.map! do |child|
154        unless child.is_a?(Tree::RuleNode)
155          parent_ref_rules(child) if child.is_a?(Tree::DirectiveNode)
156          next child
157        end
158
159        first, rest = child.rule.first.scan(/\A(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?\Z/m).first
160
161        if current_rule.nil? || current_rule.rule.first != first
162          current_rule = Tree::RuleNode.new([first])
163        end
164
165        if rest
166          child.rule = ["&" + rest]
167          current_rule << child
168        else
169          current_rule.children += child.children
170        end
171
172        current_rule
173      end
174      root.children.compact!
175      root.children.uniq!
176
177      root.children.each { |v| parent_ref_rules(v) }
178    end
179
180    # Remove useless parent refs so that
181    #
182    #     foo
183    #       & bar
184    #         color: blue
185    #
186    # becomes
187    #
188    #     foo
189    #       bar
190    #         color: blue
191    #
192    # @param root [Tree::Node] The parent node
193    def remove_parent_refs(root)
194      root.children.each do |child|
195        case child
196        when Tree::RuleNode
197          child.rule.first.gsub! /^& +/, ''
198          remove_parent_refs child
199        when Tree::DirectiveNode
200          remove_parent_refs child
201        end
202      end
203    end
204
205    # Flatten rules so that
206    #
207    #     foo
208    #       bar
209    #         color: red
210    #
211    # becomes
212    #
213    #     foo bar
214    #       color: red
215    #
216    # and
217    #
218    #     foo
219    #       &.bar
220    #         color: blue
221    #
222    # becomes
223    #
224    #     foo.bar
225    #       color: blue
226    #
227    # @param root [Tree::Node] The parent node
228    def flatten_rules(root)
229      root.children.each do |child|
230        case child
231        when Tree::RuleNode
232          flatten_rule(child)
233        when Tree::DirectiveNode
234          flatten_rules(child)
235        end
236      end
237    end
238
239    # Flattens a single rule
240    #
241    # @param rule [Tree::RuleNode] The candidate for flattening
242    # @see #flatten_rules
243    def flatten_rule(rule)
244      while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
245        child = rule.children.first
246
247        if child.rule.first[0] == ?&
248          rule.rule = [child.rule.first.gsub(/^&/, rule.rule.first)]
249        else
250          rule.rule = ["#{rule.rule.first} #{child.rule.first}"]
251        end
252
253        rule.children = child.children
254      end
255
256      flatten_rules(rule)
257    end
258
259    # Transform
260    #
261    #     foo
262    #       bar
263    #         color: blue
264    #       baz
265    #         color: blue
266    #
267    # into
268    #
269    #     foo
270    #       bar, baz
271    #         color: blue
272    #
273    # @param rule [Tree::RuleNode] The candidate for flattening
274    def fold_commas(root)
275      prev_rule = nil
276      root.children.map! do |child|
277        unless child.is_a?(Tree::RuleNode)
278          fold_commas(child) if child.is_a?(Tree::DirectiveNode)
279          next child
280        end
281
282        if prev_rule && prev_rule.children == child.children
283          prev_rule.rule.first << ", #{child.rule.first}"
284          next nil
285        end
286
287        fold_commas(child)
288        prev_rule = child
289        child
290      end
291      root.children.compact!
292    end
293  end
294end