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.



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

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
87
88
# 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 = Sass::Util.array_minus(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?
        description = unknown_args.length > 1 ? 'the following arguments:' : 'an argument named'
        raise Sass::SyntaxError.new("#{desc} doesn't have #{description} #{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.



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

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.



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

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.



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

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)



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

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)



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

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.



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

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

#visit_directive(node) (protected)



354
355
356
357
# File 'lib/sass/tree/visitors/perform.rb', line 354

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.



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

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.



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

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.



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

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.



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

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.



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

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.



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

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)



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

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.



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
278
279
# File 'lib/sass/tree/visitors/perform.rb', line 247

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.



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

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.



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

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.



304
305
306
# File 'lib/sass/tree/visitors/perform.rb', line 304

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.



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

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.



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

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)



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

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.



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

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.



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

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.



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

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.



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

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