Skip to content
Browse files

[Sass] Parse selector/property interpolation outside of Sass::Tree.

  • Loading branch information...
1 parent 7be2c9f commit d7bbab7a246f12a32739c6141bfce59cd9d81a6d @nex3 nex3 committed Jan 5, 2010
Showing with 116 additions and 102 deletions.
  1. +1 −3 TODO
  2. +14 −14 lib/sass/css.rb
  3. +30 −20 lib/sass/engine.rb
  4. +9 −6 lib/sass/scss/parser.rb
  5. +7 −16 lib/sass/tree/node.rb
  6. +42 −17 lib/sass/tree/prop_node.rb
  7. +12 −7 lib/sass/tree/rule_node.rb
  8. +1 −19 test/sass/engine_test.rb
View
4 TODO
@@ -1,4 +1,4 @@
-# -*- mode: org -*-
+y# -*- mode: org -*-
#+STARTUP: nofold
* Documentation
@@ -33,8 +33,6 @@
CSS superset
Classes are mixins
Can refer to specific property values? Syntax?
- Pre-parse everything possible: never call Node#interpolate
- Do all parsing in to_tree
Pull in Compass watcher stuff
Internationalization
Particularly word constituents in Regexps
View
28 lib/sass/css.rb
@@ -25,7 +25,7 @@ def to_sass(tabs = 0, opts = {})
class RuleNode
# @see Node#to_sass
def to_sass(tabs, opts = {})
- name = rule
+ name = rule.first
name = "\\" + name if name[0] == ?:
str = "\n#{' ' * tabs}#{name}#{children.any? { |c| c.is_a? PropNode } ? "\n" : ''}"
@@ -40,7 +40,7 @@ def to_sass(tabs, opts = {})
class PropNode
# @see Node#to_sass
def to_sass(tabs, opts = {})
- "#{' ' * tabs}#{opts[:old] ? ':' : ''}#{name}#{opts[:old] ? '' : ':'} #{value}\n"
+ "#{' ' * tabs}#{opts[:old] ? ':' : ''}#{name.first}#{opts[:old] ? '' : ':'} #{value.first}\n"
end
end
@@ -126,9 +126,9 @@ def build_tree
# @param root [Tree::Node] The parent node
def expand_commas(root)
root.children.map! do |child|
- next child unless Tree::RuleNode === child && child.rule.include?(',')
- child.rule.split(',').map do |rule|
- node = Tree::RuleNode.new(rule.strip)
+ next child unless Tree::RuleNode === child && child.rule.first.include?(',')
+ child.rule.first.split(',').map do |rule|
+ node = Tree::RuleNode.new([rule.strip])
node.children = child.children
node
end
@@ -174,15 +174,15 @@ def parent_ref_rules(root)
current_rule = nil
root.children.select { |c| Tree::RuleNode === c }.each do |child|
root.children.delete child
- first, rest = child.rule.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
+ first, rest = child.rule.first.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
- if current_rule.nil? || current_rule.rule != first
- current_rule = Tree::RuleNode.new(first)
+ if current_rule.nil? || current_rule.rule.first != first
+ current_rule = Tree::RuleNode.new([first])
root << current_rule
end
if rest
- child.rule = "&" + rest
+ child.rule = ["&" + rest]
current_rule << child
else
current_rule.children += child.children
@@ -208,7 +208,7 @@ def parent_ref_rules(root)
def remove_parent_refs(root)
root.children.each do |child|
if child.is_a?(Tree::RuleNode)
- child.rule.gsub! /^& +/, ''
+ child.rule.first.gsub! /^& +/, ''
remove_parent_refs child
end
end
@@ -249,10 +249,10 @@ def flatten_rule(rule)
while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
child = rule.children.first
- if child.rule[0] == ?&
- rule.rule = child.rule.gsub(/^&/, rule.rule)
+ if child.rule.first[0] == ?&
+ rule.rule = [child.rule.first.gsub(/^&/, rule.rule.first)]
else
- rule.rule = "#{rule.rule} #{child.rule}"
+ rule.rule = ["#{rule.rule.first} #{child.rule.first}"]
end
rule.children = child.children
@@ -282,7 +282,7 @@ def fold_commas(root)
next child unless child.is_a?(Tree::RuleNode)
if prev_rule && prev_rule.children == child.children
- prev_rule.rule << ", #{child.rule}"
+ prev_rule.rule.first << ", #{child.rule.first}"
next nil
end
View
50 lib/sass/engine.rb
@@ -304,19 +304,10 @@ def validate_and_append_child(parent, child, line, root)
def check_for_no_children(node)
return unless node.is_a?(Tree::RuleNode) && node.children.empty?
- warning = (node.rule.include?("\n")) ? <<LONG : <<SHORT
-
-WARNING on line #{node.line}#{" of #{node.filename}" if node.filename}:
-Selector
- #{node.rule.gsub("\n", "\n ")}
-doesn't have any properties and will not be rendered.
-LONG
-
+ warn(<<WARNING.strip)
WARNING on line #{node.line}#{" of #{node.filename}" if node.filename}:
-Selector #{node.rule.inspect} doesn't have any properties and will not be rendered.
-SHORT
-
- warn(warning.strip)
+This selector doesn't have any properties and will not be rendered.
+WARNING
end
def parse_line(parent, line, root)
@@ -329,7 +320,7 @@ def parse_line(parent, line, root)
# which begin with ::,
# as well as pseudo-classes
# if we're using the new property syntax
- Tree::RuleNode.new(line.text)
+ Tree::RuleNode.new(parse_interp(line.text))
else
parse_property(line, PROPERTY_OLD)
end
@@ -340,20 +331,20 @@ def parse_line(parent, line, root)
when DIRECTIVE_CHAR
parse_directive(parent, line, root)
when ESCAPE_CHAR
- Tree::RuleNode.new(line.text[1..-1])
+ Tree::RuleNode.new(parse_interp(line.text[1..-1]))
when MIXIN_DEFINITION_CHAR
parse_mixin_definition(line)
when MIXIN_INCLUDE_CHAR
if line.text[1].nil? || line.text[1] == ?\s
- Tree::RuleNode.new(line.text)
+ Tree::RuleNode.new(parse_interp(line.text))
else
parse_mixin_include(line, root)
end
else
if line.text =~ PROPERTY_NEW_MATCHER
parse_property(line, PROPERTY_NEW)
else
- Tree::RuleNode.new(line.text)
+ Tree::RuleNode.new(parse_interp(line.text))
end
end
end
@@ -365,11 +356,13 @@ def parse_property(line, property_regx)
:line => @line) if name.nil? || value.nil?
expr = if (eq.strip[0] == SCRIPT_CHAR)
- parse_script(value, :offset => line.offset + line.text.index(value))
+ [parse_script(value, :offset => line.offset + line.text.index(value))]
else
- value
+ parse_interp(value)
end
- Tree::PropNode.new(name, expr, property_regx == PROPERTY_OLD ? :old : :new)
+ Tree::PropNode.new(
+ parse_interp(name), expr,
+ property_regx == PROPERTY_OLD ? :old : :new)
end
def parse_variable(line)
@@ -388,7 +381,7 @@ def parse_comment(line)
format_comment_text(line[2..-1].strip),
line[1] == SASS_COMMENT_CHAR)
else
- Tree::RuleNode.new(line)
+ Tree::RuleNode.new(parse_interp(line))
end
end
@@ -497,5 +490,22 @@ def format_comment_text(text)
content.last.gsub!(%r{ ?\*/ *$}, '')
"/* " + content.join("\n *") + " */"
end
+
+ def parse_interp(text)
+ res = []
+ rest = Haml::Shared.handle_interpolation text do |scan|
+ escapes = scan[2].size
+ res << scan.matched[0...-2 - escapes]
+ if escapes % 2 == 1
+ res << "\\" * (escapes - 1) << '#{'
+ else
+ res << "\\" * [0, escapes - 1].max
+ res << Script::Parser.new(
+ scan, @line, scan.pos - scan.matched_size, @filename).
+ parse_interpolated
+ end
+ end
+ res << rest
+ end
end
end
View
15 lib/sass/scss/parser.rb
@@ -13,6 +13,7 @@ class Parser
def initialize(str)
@scanner = StringScanner.new(str)
@line = 1
+ @strs = []
end
# Parses an SCSS document.
@@ -202,7 +203,7 @@ def ruleset
end
end
- block(node(Sass::Tree::RuleNode.new(rules.strip)))
+ block(node(Sass::Tree::RuleNode.new([rules.strip])))
end
def block(node)
@@ -401,7 +402,7 @@ def declaration
ss
require_block ||= tok?(/\{/)
- node = node(Sass::Tree::PropNode.new(name, value, :new))
+ node = node(Sass::Tree::PropNode.new([name], [value], :new))
if require_block && expression && !space
@use_property_exception = true
@@ -460,11 +461,11 @@ def hexcolor
end
def str
- @str = ""
+ @strs.push ""
yield
- @str
+ @strs.last
ensure
- @str = nil
+ @strs.pop
end
def node(node)
@@ -543,7 +544,9 @@ def tok(rx)
if res
@line += res.count("\n")
@expected = nil
- @str << res if @str && rx != COMMENT && rx != SINGLE_LINE_COMMENT
+ if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT
+ @strs.each {|s| s << res}
+ end
end
res
View
23 lib/sass/tree/node.rb
@@ -262,27 +262,18 @@ def perform_children(environment)
children.map {|c| c.perform(environment)}.flatten
end
- # Replaces SassScript in a chunk of text (via `#{}`)
+ # Replaces SassScript in a chunk of text
# with the resulting value.
#
- # @param text [String] The text to interpolate
+ # @param text [Array<String, Sass::Script::Node>] The text to interpolate
# @param environment [Sass::Environment] The lexical environment containing
# variable and mixin values
# @return [String] The interpolated text
- def interpolate(text, environment)
- res = ''
- rest = Haml::Shared.handle_interpolation text do |scan|
- escapes = scan[2].size
- res << scan.matched[0...-2 - escapes]
- if escapes % 2 == 1
- res << "\\" * (escapes - 1) << '#{'
- else
- res << "\\" * [0, escapes - 1].max
- res << Script::Parser.new(scan, line, scan.pos - scan.matched_size, filename).
- parse_interpolated.perform(environment).to_s
- end
- end
- res + rest
+ def run_interp(text, environment)
+ text.map do |r|
+ next r if r.is_a?(String)
+ r.perform(environment).to_s
+ end.join
end
# @see Haml::Shared.balance
View
59 lib/sass/tree/prop_node.rb
@@ -3,17 +3,36 @@ module Sass::Tree
#
# @see Sass::Tree
class PropNode < Node
- # The name of the property.
+ # The name of the property,
+ # interspersed with {Sass::Script::Node}s
+ # representing `#{}`-interpolation.
+ # Any adjacent strings will be merged together.
#
- # @return [String]
+ # @return [Array<String, Sass::Script::Node>]
attr_accessor :name
+ # The name of the property
+ # after any interpolated SassScript has been resolved.
+ # Only set once \{Tree::Node#perform} has been called.
+ #
+ # @return [String]
+ attr_accessor :resolved_name
+
# The value of the property,
- # either a plain string or a SassScript parse tree.
+ # interspersed with {Sass::Script::Node}s
+ # representing `#{}`-interpolation.
+ # Any adjacent strings will be merged together.
#
- # @return [String, Script::Node]
+ # @return [Array<String, Script::Node>]
attr_accessor :value
+ # The value of the property
+ # after any interpolated SassScript has been resolved.
+ # Only set once \{Tree::Node#perform} has been called.
+ #
+ # @return [String]
+ attr_accessor :resolved_value
+
# How deep this property is indented
# relative to a normal property.
# This is only greater than 0 in the case that:
@@ -26,8 +45,8 @@ class PropNode < Node
# @return [Fixnum]
attr_accessor :tabs
- # @param name [String] See \{#name}
- # @param value [String] See \{#value}
+ # @param name [Array<String, Sass::Script::Node>] See \{#name}
+ # @param value [Array<String, Sass::Script::Node>] See \{#value}
# @param prop_syntax [Symbol] `:new` if this property uses `a: b`-style syntax,
# `:old` if it uses `:a b`-style syntax
def initialize(name, value, prop_syntax)
@@ -51,9 +70,11 @@ def ==(other)
# This only applies for old-style properties with no value,
# so returns the empty string if this is new-style.
#
+ # This should only be called once \{#perform} has been called.
+ #
# @return [String] The message
def pseudo_class_selector_message
- return "" if @prop_syntax == :new || !value.empty?
+ return "" if @prop_syntax == :new || !resolved_value.empty?
"\nIf #{declaration.dump} should be a selector, use \"\\#{declaration}\" instead."
end
@@ -64,8 +85,8 @@ def pseudo_class_selector_message
# @param tabs [Fixnum] The level of indentation for the CSS
# @return [String] The resulting CSS
def _to_s(tabs)
- to_return = ' ' * (tabs - 1 + self.tabs) + name + ":" +
- (style == :compressed ? '' : ' ') + value + (style == :compressed ? "" : ";")
+ to_return = ' ' * (tabs - 1 + self.tabs) + resolved_name + ":" +
+ (style == :compressed ? '' : ' ') + resolved_value + (style == :compressed ? "" : ";")
end
# Converts nested properties into flat properties.
@@ -76,7 +97,7 @@ def _to_s(tabs)
def _cssize(parent)
node = super
result = node.children.dup
- if !node.value.empty? || node.children.empty?
+ if !node.resolved_value.empty? || node.children.empty?
node.send(:check!)
result.unshift(node)
end
@@ -89,8 +110,8 @@ def _cssize(parent)
# @param parent [PropNode, nil] The parent node of this node,
# or nil if the parent isn't a {PropNode}
def cssize!(parent)
- self.name = "#{parent.name}-#{name}" if parent
- self.tabs = parent.tabs + (parent.value.empty? ? 0 : 1) if parent && style == :nested
+ self.resolved_name = "#{parent.resolved_name}-#{resolved_name}" if parent
+ self.tabs = parent.tabs + (parent.resolved_value.empty? ? 0 : 1) if parent && style == :nested
super
end
@@ -100,8 +121,8 @@ def cssize!(parent)
# @param environment [Sass::Environment] The lexical environment containing
# variable and mixin values
def perform!(environment)
- @name = interpolate(@name, environment)
- @value = @value.is_a?(String) ? interpolate(@value, environment) : @value.perform(environment).to_s
+ @resolved_name = run_interp(@name, environment)
+ @resolved_value = run_interp(@value, environment)
super
end
@@ -124,16 +145,20 @@ def check!
raise Sass::SyntaxError.new("Illegal property syntax: can't use new syntax when :property_syntax => :old is set.")
elsif @options[:property_syntax] == :new && @prop_syntax == :old
raise Sass::SyntaxError.new("Illegal property syntax: can't use old syntax when :property_syntax => :new is set.")
- elsif value[-1] == ?;
+ elsif resolved_value[-1] == ?;
raise Sass::SyntaxError.new("Invalid property: #{declaration.dump} (no \";\" required at end-of-line).")
- elsif value.empty?
+ elsif resolved_value.empty?
raise Sass::SyntaxError.new("Invalid property: #{declaration.dump} (no value)." +
pseudo_class_selector_message)
end
end
def declaration
- (@prop_syntax == :new ? "#{name}: #{value}" : ":#{name} #{value}").strip
+ if @prop_syntax == :new
+ "#{resolved_name}: #{resolved_value}"
+ else
+ ":#{resolved_name} #{resolved_value}"
+ end.strip
end
end
end
View
19 lib/sass/tree/rule_node.rb
@@ -8,9 +8,12 @@ class RuleNode < Node
# The character used to include the parent selector
PARENT = '&'
- # The (completely unparsed) CSS selector for this rule.
+ # The CSS selector for this rule,
+ # interspersed with {Sass::Script::Node}s
+ # representing `#{}`-interpolation.
+ # Any adjacent strings will be merged together.
#
- # @return [String]
+ # @return [Array<String, Sass::Script::Node>]
attr_accessor :rule
# The CSS selectors for this rule,
@@ -32,7 +35,7 @@ class RuleNode < Node
# [[:parent, ".foo"], ["bar"], ["baz"],
# ["\nbip"], [:parent, ".bop"], ["bup"]]
#
- # @return [Array<Array<String|Symbol>>]
+ # @return [Array<Array<String, Symbol>>]
attr_accessor :parsed_rules
# The CSS selectors for this rule,
@@ -71,7 +74,8 @@ class RuleNode < Node
# @return [Boolean]
attr_accessor :group_end
- # @param rule [String] The first CSS rule. See \{#rule}
+ # @param rule [Array<String, Sass::Script::Node>]
+ # The CSS rule. See \{#rule}
def initialize(rule)
@rule = rule
@tabs = 0
@@ -91,12 +95,13 @@ def ==(other)
#
# @param node [RuleNode] The other node
def add_rules(node)
- @rule << "\n" << node.rule
+ @rule += ["\n"] + node.rule
end
# @return [Boolean] Whether or not this rule is continued on the next line
def continued?
- @rule[-1] == ?,
+ last = @rule.last
+ last.is_a?(String) && last[-1] == ?,
end
protected
@@ -165,7 +170,7 @@ def _to_s(tabs)
# @param environment [Sass::Environment] The lexical environment containing
# variable and mixin values
def perform!(environment)
- @parsed_rules = parse_selector(interpolate(@rule, environment))
+ @parsed_rules = parse_selector(run_interp(@rule, environment))
super
end
View
20 test/sass/engine_test.rb
@@ -976,25 +976,7 @@ def test_quoted_ampersand
def test_empty_selector_warning
assert_warning(<<END) {render("foo bar")}
WARNING on line 1 of test_empty_selector_warning_inline.sass:
-Selector "foo bar" doesn't have any properties and will not be rendered.
-END
-
- assert_warning(<<END) {render(<<SASS)}
-WARNING on line 3 of test_empty_selector_warning_inline.sass:
-Selector
- foo, bar, baz,
- bang, bip, bop
-doesn't have any properties and will not be rendered.
-END
-
-
-foo, bar, baz,
-bang, bip, bop
-SASS
-
- assert_warning(<<END) {render("foo bar", :filename => nil)}
-WARNING on line 1:
-Selector "foo bar" doesn't have any properties and will not be rendered.
+This selector doesn't have any properties and will not be rendered.
END
end

0 comments on commit d7bbab7

Please sign in to comment.
Something went wrong with that request. Please try again.