require File.dirname(__FILE__) + '/../sass' require 'sass/tree/node' require 'sass/scss/css_parser' require 'strscan' module Sass # This class converts CSS documents into Sass or SCSS templates. # It works by parsing the CSS document into a {Sass::Tree} structure, # and then applying various transformations to the structure # to produce more concise and idiomatic Sass/SCSS. # # Example usage: # # Sass::CSS.new("p { color: blue }").render(:sass) #=> "p\n color: blue" # Sass::CSS.new("p { color: blue }").render(:scss) #=> "p {\n color: blue; }" class CSS # @param template [String] The CSS code # @option options :old [Boolean] (false) # Whether or not to output old property syntax # (`:color blue` as opposed to `color: blue`). # This is only meaningful when generating Sass code, # rather than SCSS. def initialize(template, options = {}) if template.is_a? IO template = template.read end @options = options.dup # Backwards compatibility @options[:old] = true if @options[:alternate] == false @template = template end # Converts the CSS template into Sass or SCSS code. # # @param fmt [Symbol] `:sass` or `:scss`, designating the format to return. # @return [String] The resulting Sass or SCSS code # @raise [Sass::SyntaxError] if there's an error parsing the CSS template def render(fmt = :sass) Haml::Util.check_encoding(@template) do |msg, line| raise Sass::SyntaxError.new(msg, :line => line) end build_tree.send("to_#{fmt}", @options).strip + "\n" rescue Sass::SyntaxError => err err.modify_backtrace(:filename => @options[:filename] || '(css)') raise err end private # Parses the CSS template and applies various transformations # # @return [Tree::Node] The root node of the parsed tree def build_tree root = Sass::SCSS::CssParser.new(@template).parse expand_commas root parent_ref_rules root remove_parent_refs root flatten_rules root fold_commas root root end # Transform # # foo, bar, baz # color: blue # # into # # foo # color: blue # bar # color: blue # baz # color: blue # # @param root [Tree::Node] The parent node def expand_commas(root) root.children.map! do |child| unless child.is_a?(Tree::RuleNode) && child.rule.first.include?(',') expand_commas(child) if child.is_a?(Tree::DirectiveNode) next child end child.rule.first.split(',').map do |rule| node = Tree::RuleNode.new([rule.strip]) node.children = child.children node end end root.children.flatten! end # Make rules use parent refs so that # # foo # color: green # foo.bar # color: blue # # becomes # # foo # color: green # &.bar # color: blue # # This has the side effect of nesting rules, # so that # # foo # color: green # foo bar # color: red # foo baz # color: blue # # becomes # # foo # color: green # & bar # color: red # & baz # color: blue # # @param root [Tree::Node] The parent node def parent_ref_rules(root) current_rule = nil root.children.map! do |child| unless child.is_a?(Tree::RuleNode) parent_ref_rules(child) if child.is_a?(Tree::DirectiveNode) next child end first, rest = child.rule.first.scan(/\A(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?\Z/m).first if current_rule.nil? || current_rule.rule.first != first current_rule = Tree::RuleNode.new([first]) end if rest child.rule = ["&" + rest] current_rule << child else current_rule.children += child.children end current_rule end root.children.compact! root.children.uniq! root.children.each { |v| parent_ref_rules(v) } end # Remove useless parent refs so that # # foo # & bar # color: blue # # becomes # # foo # bar # color: blue # # @param root [Tree::Node] The parent node def remove_parent_refs(root) root.children.each do |child| case child when Tree::RuleNode child.rule.first.gsub! /^& +/, '' remove_parent_refs child when Tree::DirectiveNode remove_parent_refs child end end end # Flatten rules so that # # foo # bar # color: red # # becomes # # foo bar # color: red # # and # # foo # &.bar # color: blue # # becomes # # foo.bar # color: blue # # @param root [Tree::Node] The parent node def flatten_rules(root) root.children.each do |child| case child when Tree::RuleNode flatten_rule(child) when Tree::DirectiveNode flatten_rules(child) end end end # Flattens a single rule # # @param rule [Tree::RuleNode] The candidate for flattening # @see #flatten_rules def flatten_rule(rule) while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode) child = rule.children.first if child.rule.first[0] == ?& rule.rule = [child.rule.first.gsub(/^&/, rule.rule.first)] else rule.rule = ["#{rule.rule.first} #{child.rule.first}"] end rule.children = child.children end flatten_rules(rule) end # Transform # # foo # bar # color: blue # baz # color: blue # # into # # foo # bar, baz # color: blue # # @param rule [Tree::RuleNode] The candidate for flattening def fold_commas(root) prev_rule = nil root.children.map! do |child| unless child.is_a?(Tree::RuleNode) fold_commas(child) if child.is_a?(Tree::DirectiveNode) next child end if prev_rule && prev_rule.children == child.children prev_rule.rule.first << ", #{child.rule.first}" next nil end fold_commas(child) prev_rule = child child end root.children.compact! end end end