Class: Sass::Tree::Visitors::Perform

Inherits:
Base
  • Object
show all
Defined in:
lib/sass/tree/visitors/perform.rb

Overview

A visitor for converting a dynamic Sass tree into a static Sass tree.

Constant Summary

Constants inherited from Base

Base::NODE_NAME_RE

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Base

#node_name

Constructor Details

#initialize(env) ⇒ Perform (protected)

Returns a new instance of Perform.



90
91
92
93
94
# File 'lib/sass/tree/visitors/perform.rb', line 90

def initialize(env)
  @environment = env
  # Stack trace information, including mixin includes and imports.
  @stack = []
end

Class Method Details

.perform_arguments(callable, args, keywords, splat)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/sass/tree/visitors/perform.rb', line 11

def self.perform_arguments(callable, args, keywords, splat)
  desc = "#{callable.type.capitalize} #{callable.name}"
  downcase_desc = "#{callable.type} #{callable.name}"

  begin
    unless keywords.empty?
      unknown_args = keywords.keys - callable.args.map {|var| var.first.underscored_name}
      if callable.splat && unknown_args.include?(callable.splat.underscored_name)
        raise Sass::SyntaxError.new("Argument $#{callable.splat.name} of #{downcase_desc} cannot be used as a named argument.")
      elsif unknown_args.any?
        raise Sass::SyntaxError.new("#{desc} doesn't have #{unknown_args.length > 1 ? 'the following arguments:' : 'an argument named'} #{unknown_args.map{|name| "$#{name}"}.join ', '}.")
      end
    end
  rescue Sass::SyntaxError => keyword_exception
  end

  # If there's no splat, raise the keyword exception immediately. The actual
  # raising happens in the ensure clause at the end of this function.
  return if keyword_exception && !callable.splat

  if args.size > callable.args.size && !callable.splat
    takes = callable.args.size
    passed = args.size
    raise Sass::SyntaxError.new(
      "#{desc} takes #{takes} argument#{'s' unless takes == 1} " +
      "but #{passed} #{passed == 1 ? 'was' : 'were'} passed.")
  end

  splat_sep = :comma
  if splat
    args += splat.to_a
    splat_sep = splat.separator if splat.is_a?(Sass::Script::List)
    # If the splat argument exists, there won't be any keywords passed in
    # manually, so we can safely overwrite rather than merge here.
    keywords = splat.keywords if splat.is_a?(Sass::Script::ArgList)
  end

  keywords = keywords.dup
  env = Sass::Environment.new(callable.environment)
  callable.args.zip(args[0...callable.args.length]) do |(var, default), value|
    if value && keywords.include?(var.underscored_name)
      raise Sass::SyntaxError.new("#{desc} was passed argument $#{var.name} both by position and by name.")
    end

    value ||= keywords.delete(var.underscored_name)
    value ||= default && default.perform(env)
    raise Sass::SyntaxError.new("#{desc} is missing argument #{var.inspect}.") unless value
    env.set_local_var(var.name, value)
  end

  if callable.splat
    rest = args[callable.args.length..-1]
    arg_list = Sass::Script::ArgList.new(rest, keywords.dup, splat_sep)
    arg_list.options = env.options
    env.set_local_var(callable.splat.name, arg_list)
  end

  yield env
rescue Exception => e
ensure
  # If there's a keyword exception, we don't want to throw it immediately,
  # because the invalid keywords may be part of a glob argument that should be
  # passed on to another function. So we only raise it if we reach the end of
  # this function *and* the keywords attached to the argument list glob object
  # haven't been accessed.
  #
  # The keyword exception takes precedence over any Sass errors, but not over
  # non-Sass exceptions.
  if keyword_exception &&
      !(arg_list && arg_list.keywords_accessed) &&
      (e.nil? || e.is_a?(Sass::SyntaxError))
    raise keyword_exception
  elsif e
    raise e
  end
end

.visit(root, environment = Sass::Environment.new) ⇒ Tree::Node

Returns The resulting tree of static nodes.

Parameters:

  • root (Tree::Node)

    The root node of the tree to visit.

  • environment (Sass::Environment) (defaults to: Sass::Environment.new)

    The lexical environment.

Returns:

  • (Tree::Node)

    The resulting tree of static nodes.



6
7
8
# File 'lib/sass/tree/visitors/perform.rb', line 6

def self.visit(root, environment = Sass::Environment.new)
  new(environment).send(:visit, root)
end

Instance Method Details

#visit(node) (protected)

If an exception is raised, this adds proper metadata to the backtrace.



97
98
99
100
101
102
# File 'lib/sass/tree/visitors/perform.rb', line 97

def visit(node)
  super(node.dup)
rescue Sass::SyntaxError => e
  e.modify_backtrace(:filename => node.filename, :line => node.line)
  raise e
end

#visit_children(parent) (protected)

Keeps track of the current environment.



105
106
107
108
109
110
# File 'lib/sass/tree/visitors/perform.rb', line 105

def visit_children(parent)
  with_environment Sass::Environment.new(@environment, parent.options) do
    parent.children = super.flatten
    parent
  end
end

#visit_comment(node) (protected)

Removes this node from the tree if it's a silent comment.



133
134
135
136
137
138
# File 'lib/sass/tree/visitors/perform.rb', line 133

def visit_comment(node)
  return [] if node.invisible?
  node.resolved_value = run_interp_no_strip(node.value)
  node.resolved_value.gsub!(/\\([\\#])/, '\1')
  node
end

#visit_content(node) (protected)



279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/sass/tree/visitors/perform.rb', line 279

def visit_content(node)
  return [] unless content = @environment.content
  @stack.push(:filename => node.filename, :line => node.line, :name => '@content')
  trace_node = Sass::Tree::TraceNode.from_node('@content', node)
  with_environment(@environment.caller) {trace_node.children = content.map {|c| visit(c.dup)}.flatten}
  trace_node
rescue Sass::SyntaxError => e
  e.modify_backtrace(:mixin => '@content', :line => node.line)
  e.add_backtrace(:line => node.line)
  raise e
ensure
  @stack.pop if content
end

#visit_cssimport(node) (protected)



369
370
371
372
373
374
375
376
# File 'lib/sass/tree/visitors/perform.rb', line 369

def visit_cssimport(node)
  node.resolved_uri = run_interp([node.uri])
  if node.query
    parser = Sass::SCSS::StaticParser.new(run_interp(node.query), node.filename, node.line)
    node.resolved_query ||= parser.parse_media_query_list
  end
  yield
end

#visit_debug(node) (protected)

Prints the expression to STDERR.



141
142
143
144
145
146
147
148
149
150
# File 'lib/sass/tree/visitors/perform.rb', line 141

def visit_debug(node)
  res = node.expr.perform(@environment)
  res = res.value if res.is_a?(Sass::Script::String)
  if node.filename
    $stderr.puts "#{node.filename}:#{node.line} DEBUG: #{res}"
  else
    $stderr.puts "Line #{node.line} DEBUG: #{res}"
  end
  []
end

#visit_directive(node) (protected)



352
353
354
355
# File 'lib/sass/tree/visitors/perform.rb', line 352

def visit_directive(node)
  node.resolved_value = run_interp(node.value)
  yield
end

#visit_each(node) (protected)

Runs the child nodes once for each value in the list.



153
154
155
156
157
158
159
160
161
162
# File 'lib/sass/tree/visitors/perform.rb', line 153

def visit_each(node)
  list = node.list.perform(@environment)

  with_environment Sass::Environment.new(@environment) do
    list.to_a.map do |v|
      @environment.set_local_var(node.var, v)
      node.children.map {|c| visit(c)}
    end.flatten
  end
end

#visit_extend(node) (protected)

Runs SassScript interpolation in the selector, and then parses the result into a Selector::CommaSequence.



166
167
168
169
170
# File 'lib/sass/tree/visitors/perform.rb', line 166

def visit_extend(node)
  parser = Sass::SCSS::StaticParser.new(run_interp(node.selector), node.filename, node.line)
  node.resolved_selector = parser.parse_selector
  node
end

#visit_for(node) (protected)

Runs the child nodes once for each time through the loop, varying the variable each time.



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/sass/tree/visitors/perform.rb', line 173

def visit_for(node)
  from = node.from.perform(@environment)
  to = node.to.perform(@environment)
  from.assert_int!
  to.assert_int!

  to = to.coerce(from.numerator_units, from.denominator_units)
  range = Range.new(from.to_i, to.to_i, node.exclusive)

  with_environment Sass::Environment.new(@environment) do
    range.map do |i|
      @environment.set_local_var(node.var,
        Sass::Script::Number.new(i, from.numerator_units, from.denominator_units))
      node.children.map {|c| visit(c)}
    end.flatten
  end
end

#visit_function(node) (protected)

Loads the function into the environment.



192
193
194
195
196
197
# File 'lib/sass/tree/visitors/perform.rb', line 192

def visit_function(node)
  env = Sass::Environment.new(@environment, node.options)
  @environment.set_local_function(node.name,
    Sass::Callable.new(node.name, node.args, node.splat, env, node.children, !:has_content, "function"))
  []
end

#visit_if(node) (protected)

Runs the child nodes if the conditional expression is true; otherwise, tries the else nodes.



201
202
203
204
205
206
207
208
209
210
# File 'lib/sass/tree/visitors/perform.rb', line 201

def visit_if(node)
  if node.expr.nil? || node.expr.perform(@environment).to_bool
    yield
    node.children
  elsif node.else
    visit(node.else)
  else
    []
  end
end

#visit_import(node) (protected)

Returns a static DirectiveNode if this is importing a CSS file, or parses and includes the imported Sass file.



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/sass/tree/visitors/perform.rb', line 214

def visit_import(node)
  if path = node.css_import?
    return Sass::Tree::CssImportNode.resolved("url(#{path})")
  end
  file = node.imported_file
  handle_import_loop!(node) if @stack.any? {|e| e[:filename] == file.options[:filename]}

  begin
    @stack.push(:filename => node.filename, :line => node.line)
    root = file.to_tree
    Sass::Tree::Visitors::CheckNesting.visit(root)
    node.children = root.children.map {|c| visit(c)}.flatten
    node
  rescue Sass::SyntaxError => e
    e.modify_backtrace(:filename => node.imported_file.options[:filename])
    e.add_backtrace(:filename => node.filename, :line => node.line)
    raise e
  end
ensure
  @stack.pop unless path
end

#visit_media(node) (protected)



357
358
359
360
361
# File 'lib/sass/tree/visitors/perform.rb', line 357

def visit_media(node)
  parser = Sass::SCSS::StaticParser.new(run_interp(node.query), node.filename, node.line)
  node.resolved_query ||= parser.parse_media_query_list
  yield
end

#visit_mixin(node) (protected)

Runs a mixin.



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/sass/tree/visitors/perform.rb', line 245

def visit_mixin(node)
  include_loop = true
  handle_include_loop!(node) if @stack.any? {|e| e[:name] == node.name}
  include_loop = false

  @stack.push(:filename => node.filename, :line => node.line, :name => node.name)
  raise Sass::SyntaxError.new("Undefined mixin '#{node.name}'.") unless mixin = @environment.mixin(node.name)

  if node.children.any? && !mixin.has_content
    raise Sass::SyntaxError.new(%Q{Mixin "#{node.name}" does not accept a content block.})
  end

  args = node.args.map {|a| a.perform(@environment)}
  keywords = Sass::Util.map_hash(node.keywords) {|k, v| [k, v.perform(@environment)]}
  splat = node.splat.perform(@environment) if node.splat

  self.class.perform_arguments(mixin, args, keywords, splat) do |env|
    env.caller = Sass::Environment.new(@environment)
    env.content = node.children if node.has_children

    trace_node = Sass::Tree::TraceNode.from_node(node.name, node)
    with_environment(env) {trace_node.children = mixin.tree.map {|c| visit(c)}.flatten}
    trace_node
  end
rescue Sass::SyntaxError => e
  unless include_loop
    e.modify_backtrace(:mixin => node.name, :line => node.line)
    e.add_backtrace(:line => node.line)
  end
  raise e
ensure
  @stack.pop unless include_loop
end

#visit_mixindef(node) (protected)

Loads a mixin into the environment.



237
238
239
240
241
242
# File 'lib/sass/tree/visitors/perform.rb', line 237

def visit_mixindef(node)
  env = Sass::Environment.new(@environment, node.options)
  @environment.set_local_mixin(node.name,
    Sass::Callable.new(node.name, node.args, node.splat, env, node.children, node.has_content, "mixin"))
  []
end

#visit_prop(node) (protected)

Runs any SassScript that may be embedded in a property.



294
295
296
297
298
299
# File 'lib/sass/tree/visitors/perform.rb', line 294

def visit_prop(node)
  node.resolved_name = run_interp(node.name)
  val = node.value.perform(@environment)
  node.resolved_value = val.to_s
  yield
end

#visit_return(node) (protected)

Returns the value of the expression.



302
303
304
# File 'lib/sass/tree/visitors/perform.rb', line 302

def visit_return(node)
  throw :_sass_return, node.expr.perform(@environment)
end

#visit_root(node) (protected)

Sets the options on the environment if this is the top-level root.



125
126
127
128
129
130
# File 'lib/sass/tree/visitors/perform.rb', line 125

def visit_root(node)
  yield
rescue Sass::SyntaxError => e
  e.sass_template ||= node.template
  raise e
end

#visit_rule(node) (protected)

Runs SassScript interpolation in the selector, and then parses the result into a Selector::CommaSequence.



308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/sass/tree/visitors/perform.rb', line 308

def visit_rule(node)
  rule = node.rule
  rule = rule.map {|e| e.is_a?(String) && e != ' ' ? e.strip : e} if node.style == :compressed
  parser = Sass::SCSS::StaticParser.new(run_interp(node.rule), node.filename, node.line)
  node.parsed_rules ||= parser.parse_selector
  if node.options[:trace_selectors]
    @stack.push(:filename => node.filename, :line => node.line)
    node.stack_trace = stack_trace
    @stack.pop
  end
  yield
end

#visit_supports(node) (protected)



363
364
365
366
367
# File 'lib/sass/tree/visitors/perform.rb', line 363

def visit_supports(node)
  node.condition = node.condition.deep_copy
  node.condition.perform(@environment)
  yield
end

#visit_variable(node) (protected)

Loads the new variable value into the environment.



322
323
324
325
326
327
328
# File 'lib/sass/tree/visitors/perform.rb', line 322

def visit_variable(node)
  var = @environment.var(node.name)
  return [] if node.guarded && var && !var.null?
  val = node.expr.perform(@environment)
  @environment.set_var(node.name, val)
  []
end

#visit_warn(node) (protected)

Prints the expression to STDERR with a stylesheet trace.



331
332
333
334
335
336
337
338
339
340
341
# File 'lib/sass/tree/visitors/perform.rb', line 331

def visit_warn(node)
  @stack.push(:filename => node.filename, :line => node.line)
  res = node.expr.perform(@environment)
  res = res.value if res.is_a?(Sass::Script::String)
  msg = "WARNING: #{res}\n         "
  msg << stack_trace.join("\n         ") << "\n"
  Sass::Util.sass_warn msg
  []
ensure
  @stack.pop
end

#visit_while(node) (protected)

Runs the child nodes until the continuation expression becomes false.



344
345
346
347
348
349
350
# File 'lib/sass/tree/visitors/perform.rb', line 344

def visit_while(node)
  children = []
  with_environment Sass::Environment.new(@environment) do
    children += node.children.map {|c| visit(c)} while node.expr.perform(@environment).to_bool
  end
  children.flatten
end

#with_environment(env) { ... } ⇒ Object (protected)

Runs a block of code with the current environment replaced with the given one.

Parameters:

Yields:

  • A block in which the environment is set to env.

Returns:

  • (Object)

    The return value of the block.



117
118
119
120
121
122
# File 'lib/sass/tree/visitors/perform.rb', line 117

def with_environment(env)
  old_env, @environment = @environment, env
  yield
ensure
  @environment = old_env
end