123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- 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.
- 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
- 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)
- if current_rule.nil? || first_sseq(current_rule).members != firsts
- current_rule = Tree::RuleNode.new([])
- current_rule.parsed_rules = make_sseq(*firsts)
- end
- unless rest.empty?
- rest.unshift Sass::Selector::Parent.new
- child.parsed_rules = make_sseq(*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_sseq(rule), *first_seq(child).members)
- 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.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<Sass::Selector::Sequence>]
- # @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<Sass::Selector::Sequence, String>]
- # @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 sseqs [Array<Sass::Selector::Sequence, String>]
- # @return [Sass::Selector::CommaSequence]
- def make_sseq(*sseqs)
- make_seq(Sass::Selector::SimpleSequence.new(sseqs))
- 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
|