1
0
Fork 0
mirror of https://github.com/haml/haml.git synced 2022-11-09 12:33:31 -05:00

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

This commit is contained in:
Nathan Weizenbaum 2010-01-05 20:49:05 -08:00
parent 7be2c9fe96
commit d7bbab7a24
8 changed files with 117 additions and 103 deletions

4
TODO
View file

@ -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 file

@ -25,7 +25,7 @@ module Sass
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 @@ module Sass
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 @@ module Sass
# @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 @@ module Sass
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 @@ module Sass
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 @@ module Sass
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 @@ module Sass
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 file

@ -304,19 +304,10 @@ END
def check_for_no_children(node)
return unless node.is_a?(Tree::RuleNode) && node.children.empty?
warning = (node.rule.include?("\n")) ? <<LONG : <<SHORT
warn(<<WARNING.strip)
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
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 @@ SHORT
# 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,12 +331,12 @@ SHORT
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
@ -353,7 +344,7 @@ SHORT
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 @@ SHORT
: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 @@ SHORT
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 @@ SHORT
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 file

@ -13,6 +13,7 @@ module Sass
def initialize(str)
@scanner = StringScanner.new(str)
@line = 1
@strs = []
end
# Parses an SCSS document.
@ -202,7 +203,7 @@ module Sass
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 @@ module Sass
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 @@ MESSAGE
end
def str
@str = ""
@strs.push ""
yield
@str
@strs.last
ensure
@str = nil
@strs.pop
end
def node(node)
@ -543,7 +544,9 @@ MESSAGE
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 file

@ -262,27 +262,18 @@ module Sass
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 file

@ -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 value of the property,
# either a plain string or a SassScript parse tree.
# The name of the property
# after any interpolated SassScript has been resolved.
# Only set once \{Tree::Node#perform} has been called.
#
# @return [String, Script::Node]
# @return [String]
attr_accessor :resolved_name
# The value of the property,
# interspersed with {Sass::Script::Node}s
# representing `#{}`-interpolation.
# Any adjacent strings will be merged together.
#
# @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 @@ module Sass::Tree
# @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 @@ module Sass::Tree
# 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 @@ module Sass::Tree
# @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 @@ module Sass::Tree
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 @@ module Sass::Tree
# @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 @@ module Sass::Tree
# @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 @@ module Sass::Tree
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 file

@ -8,9 +8,12 @@ module Sass::Tree
# 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 @@ module Sass::Tree
# [[: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 @@ module Sass::Tree
# @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 @@ module Sass::Tree
#
# @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 @@ module Sass::Tree
# @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 file

@ -976,25 +976,7 @@ SASS
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