# A visitor for converting a Sass tree into CSS. class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base protected def initialize @tabs = 0 end def visit(node) super rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.filename, :line => node.line) raise e end def with_tabs(tabs) old_tabs, @tabs = @tabs, tabs yield ensure @tabs = old_tabs end def visit_root(node) result = String.new node.children.each do |child| next if child.invisible? child_str = visit(child) result << child_str + (node.style == :compressed ? '' : "\n") end result.rstrip! return "" if result.empty? result << "\n" unless Sass::Util.ruby1_8? || result.ascii_only? if node.children.first.is_a?(Sass::Tree::CharsetNode) begin encoding = node.children.first.name # Default to big-endian encoding, because we have to decide somehow encoding << 'BE' if encoding =~ /\Autf-(16|32)\Z/i result = result.encode(Encoding.find(encoding)) rescue EncodingError end end result = "@charset \"#{result.encoding.name}\";#{ node.style == :compressed ? '' : "\n" }".encode(result.encoding) + result end result rescue Sass::SyntaxError => e e.sass_template ||= node.template raise e end def visit_charset(node) "@charset \"#{node.name}\";" end def visit_comment(node) return if node.invisible? spaces = (' ' * [@tabs - node.value[/^ */].size, 0].max) content = node.value.gsub(/^/, spaces) content.gsub!(/\n +(\* *(?!\/))?/, ' ') if node.style == :compact content end def visit_directive(node) return node.value + ";" unless node.has_children return node.value + " {}" if node.children.empty? result = if node.style == :compressed "#{node.value}{" else "#{' ' * @tabs}#{node.value} {" + (node.style == :compact ? ' ' : "\n") end was_prop = false first = true node.children.each do |child| next if child.invisible? if node.style == :compact if child.is_a?(Sass::Tree::PropNode) with_tabs(first || was_prop ? 0 : @tabs + 1) {result << visit(child) << ' '} else result[-1] = "\n" if was_prop rendered = with_tabs(@tabs + 1) {visit(child).dup} rendered = rendered.lstrip if first result << rendered.rstrip + "\n" end was_prop = child.is_a?(Sass::Tree::PropNode) first = false elsif node.style == :compressed result << (was_prop ? ";" : "") << with_tabs(0) {visit(child)} was_prop = child.is_a?(Sass::Tree::PropNode) else result << with_tabs(@tabs + 1) {visit(child)} + "\n" end end result.rstrip + if node.style == :compressed "}" else (node.style == :expanded ? "\n" : " ") + "}\n" end end def visit_media(node) str = with_tabs(@tabs + node.tabs) {visit_directive(node)} str.gsub!(/\n\Z/, '') unless node.style == :compressed || node.group_end str end def visit_prop(node) tab_str = ' ' * (@tabs + node.tabs) if node.style == :compressed "#{tab_str}#{node.resolved_name}:#{node.resolved_value}" else "#{tab_str}#{node.resolved_name}: #{node.resolved_value};" end end def visit_rule(node) with_tabs(@tabs + node.tabs) do rule_separator = node.style == :compressed ? ',' : ', ' line_separator = case node.style when :nested, :expanded; "\n" when :compressed; "" else; " " end rule_indent = ' ' * @tabs per_rule_indent, total_indent = [:nested, :expanded].include?(node.style) ? [rule_indent, ''] : ['', rule_indent] total_rule = total_indent + node.resolved_rules.members. map {|seq| seq.to_a.join.gsub(/([^,])\n/m, node.style == :compressed ? '\1 ' : "\\1\n")}. join(rule_separator).split("\n").map do |line| per_rule_indent + line.strip end.join(line_separator) to_return = '' old_spaces = ' ' * @tabs spaces = ' ' * (@tabs + 1) if node.style != :compressed if node.options[:debug_info] to_return << visit(debug_info_rule(node.debug_info, node.options)) << "\n" elsif node.options[:line_comments] to_return << "#{old_spaces}/* line #{node.line}" if node.filename relative_filename = if node.options[:css_filename] begin Pathname.new(node.filename).relative_path_from( Pathname.new(File.dirname(node.options[:css_filename]))).to_s rescue ArgumentError nil end end relative_filename ||= node.filename to_return << ", #{relative_filename}" end to_return << " */\n" end end if node.style == :compact properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(' ')} to_return << "#{total_rule} { #{properties} }#{"\n" if node.group_end}" elsif node.style == :compressed properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(';')} to_return << "#{total_rule}{#{properties}}" else properties = with_tabs(@tabs + 1) {node.children.map {|a| visit(a)}.join("\n")} end_props = (node.style == :expanded ? "\n" + old_spaces : ' ') to_return << "#{total_rule} {\n#{properties}#{end_props}}#{"\n" if node.group_end}" end to_return end end private def debug_info_rule(debug_info, options) node = Sass::Tree::DirectiveNode.new("@media -sass-debug-info") debug_info.map {|k, v| [k.to_s, v.to_s]}.sort.each do |k, v| rule = Sass::Tree::RuleNode.new([""]) rule.resolved_rules = Sass::Selector::CommaSequence.new( [Sass::Selector::Sequence.new( [Sass::Selector::SimpleSequence.new( [Sass::Selector::Element.new(k.to_s.gsub(/[^\w-]/, "\\\\\\0"), nil)]) ]) ]) prop = Sass::Tree::PropNode.new([""], "", :new) prop.resolved_name = "font-family" prop.resolved_value = Sass::SCSS::RX.escape_ident(v.to_s) rule << prop node << rule end node.options = options.merge(:debug_info => false, :line_comments => false, :style => :compressed) node end end