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:
parent
7be2c9fe96
commit
d7bbab7a24
8 changed files with 117 additions and 103 deletions
4
TODO
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue