Skip to content

Commit

Permalink
Add an @each directive for iterating over lists.
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Nov 10, 2010
1 parent 6c2dc4d commit deb1d10
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 2 deletions.
24 changes: 24 additions & 0 deletions lib/sass/engine.rb
Expand Up @@ -14,6 +14,7 @@
require 'sass/tree/if_node'
require 'sass/tree/while_node'
require 'sass/tree/for_node'
require 'sass/tree/each_node'
require 'sass/tree/debug_node'
require 'sass/tree/warn_node'
require 'sass/tree/import_node'
Expand Down Expand Up @@ -610,6 +611,8 @@ def parse_directive(parent, line, root)
parse_mixin_include(line, root)
elsif directive == "for"
parse_for(line, root, value)
elsif directive == "each"
parse_each(line, root, value)
elsif directive == "else"
parse_else(parent, line, value)
elsif directive == "while"
Expand Down Expand Up @@ -671,6 +674,27 @@ def parse_for(line, root, text)
Tree::ForNode.new(var, parsed_from, parsed_to, to_name == 'to')
end

def parse_each(line, root, text)
var, list_expr = text.scan(/^([^\s]+)\s+in\s+(.+)$/).first

if var.nil? # scan failed, try to figure out why for error message
if text !~ /^[^\s]+/
expected = "variable name"
elsif text !~ /^[^\s]+\s+from\s+.+/
expected = "'in <expr>'"
end
raise SyntaxError.new("Invalid for directive '@each #{text}': expected #{expected}.")
end
raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
if var.slice!(0) == ?!
offset = line.offset + line.text.index("!" + var) + 1
Script.var_warning(var, @line, offset, @options[:filename])
end

parsed_list = parse_script(list_expr, :offset => line.offset + line.text.index(list_expr))
Tree::EachNode.new(var, parsed_list)
end

def parse_else(parent, line, text)
previous = parent.children.last
raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
Expand Down
16 changes: 14 additions & 2 deletions lib/sass/scss/parser.rb
Expand Up @@ -98,8 +98,8 @@ def process_comment(text, node)
node << comment
end

DIRECTIVES = Set[:mixin, :include, :debug, :warn, :for, :while, :if, :extend, :import,
:media, :charset]
DIRECTIVES = Set[:mixin, :include, :debug, :warn, :for, :each, :while, :if,
:extend, :import, :media, :charset]

def directive
return unless tok(/@/)
Expand Down Expand Up @@ -169,6 +169,18 @@ def for_directive
block(node(Sass::Tree::ForNode.new(var, from, to, exclusive)), :directive)
end

def each_directive
tok!(/\$/)
var = tok! IDENT
ss

tok!(/in/)
list = sass_script(:parse)
ss

block(node(Sass::Tree::EachNode.new(var, list)), :directive)
end

def while_directive
expr = sass_script(:parse)
ss
Expand Down
54 changes: 54 additions & 0 deletions lib/sass/tree/each_node.rb
@@ -0,0 +1,54 @@
require 'sass/tree/node'

module Sass::Tree
# A dynamic node representing a Sass `@each` loop.
#
# @see Sass::Tree
class EachNode < Node
# @param var [String] The name of the loop variable
# @param list [Script::Node] The parse tree for the list
def initialize(var, list)
@var = var
@list = list
super()
end

protected

# @see Node#to_src
def to_src(tabs, opts, fmt)
"#{' ' * tabs}@each $#{dasherize(@var, opts)} in #{@list.to_sass(opts)}" +
children_to_src(tabs, opts, fmt)
end

# Runs the child nodes once for each value in the list.
#
# @param environment [Sass::Environment] The lexical environment containing
# variable and mixin values
# @return [Array<Tree::Node>] The resulting static nodes
# @see Sass::Tree
def _perform(environment)
list = @list.perform(environment)

children = []
environment = Sass::Environment.new(environment)
list.to_a.each do |v|
environment.set_local_var(@var, v)
children += perform_children(environment)
end
children
end

# Returns an error message if the given child node is invalid,
# and false otherwise.
#
# {ExtendNode}s are valid within {EachNode}s.
#
# @param child [Tree::Node] A potential child node.
# @return [Boolean, String] Whether or not the child node is valid,
# as well as the error message to display if it is invalid
def invalid_child?(child)
super unless child.is_a?(ExtendNode)
end
end
end
20 changes: 20 additions & 0 deletions test/sass/conversion_test.rb
Expand Up @@ -675,6 +675,26 @@ def test_if
SCSS
end

def test_each
assert_renders <<SASS, <<SCSS
a
@each $number in 1px 2px 3px 4px
b: $number
c
@each $str in foo, bar, baz, bang
d: $str
SASS
a {
@each $number in 1px 2px 3px 4px {
b: $number; } }
c {
@each $str in foo, bar, baz, bang {
d: $str; } }
SCSS
end

def test_import
assert_renders <<SASS, <<SCSS
@import foo
Expand Down
23 changes: 23 additions & 0 deletions test/sass/engine_test.rb
Expand Up @@ -1281,6 +1281,29 @@ def test_else
SASS
end

def test_each
assert_equal(<<CSS, render(<<SASS))
a {
b: 1px;
b: 2px;
b: 3px;
b: 4px;
c: foo;
c: bar;
c: baz;
c: bang;
d: blue; }
CSS
a
@each $number in 1px 2px 3px 4px
b: $number
@each $str in foo, bar, baz, bang
c: $str
@each $single in blue
d: $single
SASS
end

def test_variable_reassignment
assert_equal(<<CSS, render(<<SASS))
a {
Expand Down
27 changes: 27 additions & 0 deletions test/sass/scss/scss_test.rb
Expand Up @@ -235,6 +235,33 @@ def test_while_directive
SCSS
end

def test_each_directive
assert_equal <<CSS, render(<<SCSS)
a {
b: 1px;
b: 2px;
b: 3px;
b: 4px; }
c {
d: foo;
d: bar;
d: baz;
d: bang; }
CSS
a {
@each $number in 1px 2px 3px 4px {
b: $number;
}
}
c {
@each $str in foo, bar, baz, bang {
d: $str;
}
}
SCSS
end

def test_css_import_directive
assert_equal "@import url(foo.css);\n", render('@import "foo.css";')
assert_equal "@import url(foo.css);\n", render("@import 'foo.css';")
Expand Down

0 comments on commit deb1d10

Please sign in to comment.