require File.dirname(__FILE__) + '/../sass' require 'sass/tree/node' require 'sass/scss/css_parser' 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 stylesheet. # This stylesheet can be encoded using any encoding # that can be converted to Unicode. # If the stylesheet contains an `@charset` declaration, # that overrides the Ruby encoding # (see {file:SASS_REFERENCE.md#encodings the encoding documentation}) # @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. # @option options :indent [String] (" ") # The string to use for indenting each line. Defaults to two spaces. 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) check_encoding! build_tree.send("to_#{fmt}", @options).strip + "\n" rescue Sass::SyntaxError => err err.modify_backtrace(:filename => @options[:filename] || '(css)') raise err end # Returns the original encoding of the document, # or `nil` under Ruby 1.8. # # @return [Encoding, nil] # @raise [Encoding::UndefinedConversionError] if the source encoding # cannot be converted to UTF-8 # @raise [ArgumentError] if the document uses an unknown encoding with `@charset` def source_encoding check_encoding! @original_encoding end private def check_encoding! return if @checked_encoding @checked_encoding = true @template, @original_encoding = Sass::Util.check_sass_encoding(@template) do |msg, line| raise Sass::SyntaxError.new(msg, :line => line) end end # 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, @options[:filename]).parse parse_selectors root expand_commas root nest_seqs root parent_ref_rules root flatten_rules root bubble_subject root fold_commas root dump_selectors root root end # Parse all the selectors in the document and assign them to # {Sass::Tree::RuleNode#parsed_rules}. # # @param root [Tree::Node] The parent node def parse_selectors(root) root.children.each do |child| next parse_selectors(child) if child.is_a?(Tree::DirectiveNode) next unless child.is_a?(Tree::RuleNode) parser = Sass::SCSS::CssParser.new(child.rule.first, child.filename, child.line) child.parsed_rules = parser.parse_selector end 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| # child.parsed_rules.members.size > 1 iff the rule contains a comma unless child.is_a?(Tree::RuleNode) && child.parsed_rules.members.size > 1 expand_commas(child) if child.is_a?(Tree::DirectiveNode) next child end child.parsed_rules.members.map do |seq| node = Tree::RuleNode.new([]) node.parsed_rules = make_cseq(seq) node.children = child.children node end end root.children.flatten! end # Make rules use nesting 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 nest_seqs(root) current_rule = nil root.children.map! do |child| unless child.is_a?(Tree::RuleNode) nest_seqs(child) if child.is_a?(Tree::DirectiveNode) next child end seq = first_seq(child) seq.members.reject! {|sseq| sseq == "\n"} first, rest = seq.members.first, seq.members[1..-1] if current_rule.nil? || first_sseq(current_rule) != first current_rule = Tree::RuleNode.new([]) current_rule.parsed_rules = make_seq(first) end unless rest.empty? child.parsed_rules = make_seq(*rest) current_rule << child else current_rule.children += child.children end current_rule end root.children.compact! root.children.uniq! root.children.each {|v| nest_seqs(v)} end # Make rules use parent refs so that # # foo # color: green # foo.bar # color: blue # # becomes # # foo # color: green # &.bar # 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 sseq = first_sseq(child) next child unless sseq.is_a?(Sass::Selector::SimpleSequence) firsts, rest = [sseq.members.first], sseq.members[1..-1] firsts.push rest.shift if firsts.first.is_a?(Sass::Selector::Parent) last_simple_subject = rest.empty? && sseq.subject? if current_rule.nil? || first_sseq(current_rule).members != firsts || !!first_sseq(current_rule).subject? != !!last_simple_subject current_rule = Tree::RuleNode.new([]) current_rule.parsed_rules = make_sseq(last_simple_subject, *firsts) end unless rest.empty? rest.unshift Sass::Selector::Parent.new child.parsed_rules = make_sseq(sseq.subject?, *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 # 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 first_simple_sel(child).is_a?(Sass::Selector::Parent) rule.parsed_rules = child.parsed_rules.resolve_parent_refs(rule.parsed_rules) else rule.parsed_rules = make_seq(*(first_seq(rule).members + first_seq(child).members)) end rule.children = child.children end flatten_rules(rule) end def bubble_subject(root) root.children.each do |child| bubble_subject(child) if child.is_a?(Tree::RuleNode) || child.is_a?(Tree::DirectiveNode) next unless child.is_a?(Tree::RuleNode) && !child.children.empty? next unless child.children.all? do |c| next unless c.is_a?(Tree::RuleNode) first_simple_sel(c).is_a?(Sass::Selector::Parent) && first_sseq(c).subject? end first_sseq(child).subject = true child.children.each {|c| first_sseq(c).subject = false} end 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.parsed_rules.members << first_seq(child) next nil end fold_commas(child) prev_rule = child child end root.children.compact! end # Dump all the parsed {Sass::Tree::RuleNode} selectors to strings. # # @param root [Tree::Node] The parent node def dump_selectors(root) root.children.each do |child| next dump_selectors(child) if child.is_a?(Tree::DirectiveNode) next unless child.is_a?(Tree::RuleNode) child.rule = [child.parsed_rules.to_s] dump_selectors(child) end end # Create a {Sass::Selector::CommaSequence}. # # @param seqs [Array] # @return [Sass::Selector::CommaSequence] def make_cseq(*seqs) Sass::Selector::CommaSequence.new(seqs) end # Create a {Sass::Selector::CommaSequence} containing only a single # {Sass::Selector::Sequence}. # # @param sseqs [Array] # @return [Sass::Selector::CommaSequence] def make_seq(*sseqs) make_cseq(Sass::Selector::Sequence.new(sseqs)) end # Create a {Sass::Selector::CommaSequence} containing only a single # {Sass::Selector::Sequence} which in turn contains only a single # {Sass::Selector::SimpleSequence}. # # @param subject [Boolean] Whether this is a subject selector # @param sseqs [Array] # @return [Sass::Selector::CommaSequence] def make_sseq(subject, *sseqs) make_seq(Sass::Selector::SimpleSequence.new(sseqs, subject)) end # Return the first {Sass::Selector::Sequence} in a {Sass::Tree::RuleNode}. # # @param rule [Sass::Tree::RuleNode] # @return [Sass::Selector::Sequence] def first_seq(rule) rule.parsed_rules.members.first end # Return the first {Sass::Selector::SimpleSequence} in a # {Sass::Tree::RuleNode}. # # @param rule [Sass::Tree::RuleNode] # @return [Sass::Selector::SimpleSequence, String] def first_sseq(rule) first_seq(rule).members.first end # Return the first {Sass::Selector::Simple} in a {Sass::Tree::RuleNode}, # unless the rule begins with a combinator. # # @param rule [Sass::Tree::RuleNode] # @return [Sass::Selector::Simple?] def first_simple_sel(rule) sseq = first_sseq(rule) return unless sseq.is_a?(Sass::Selector::SimpleSequence) sseq.members.first end end end