sass 3.1.0.alpha.48 → 3.1.0.alpha.49

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,278 @@
1
+ # A visitor for converting a dynamic Sass tree into a static Sass tree.
2
+ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
3
+ protected
4
+
5
+ def initialize
6
+ @environment = Sass::Environment.new
7
+ end
8
+
9
+ # If an exception is raised, this add proper metadata to the backtrace.
10
+ def visit(node)
11
+ super(node.dup)
12
+ rescue Sass::SyntaxError => e
13
+ e.modify_backtrace(:filename => node.filename, :line => node.line)
14
+ raise e
15
+ end
16
+
17
+ # Keeps track of the current environment.
18
+ def visit_children(parent)
19
+ with_environment Sass::Environment.new(@environment) do
20
+ parent.children = super.flatten
21
+ parent.children.each {|c| parent.check_child! c}
22
+ parent
23
+ end
24
+ end
25
+
26
+ # Runs a block of code with the current environment replaced with the given one.
27
+ #
28
+ # @param env [Sass::Environment] The new environment for the duration of the block.
29
+ # @yield A block in which the environment is set to `env`.
30
+ # @return [Object] The return value of the block.
31
+ def with_environment(env)
32
+ old_env, @environment = @environment, env
33
+ yield
34
+ ensure
35
+ @environment = old_env
36
+ end
37
+
38
+ # Sets the options on the environment if this is the top-level root.
39
+ def visit_root(node)
40
+ @environment.options = node.options if @environment.options.nil? || @environment.options.empty?
41
+ yield
42
+ rescue Sass::SyntaxError => e
43
+ e.sass_template ||= node.template
44
+ raise e
45
+ end
46
+
47
+ # Removes this node from the tree if it's a silent comment.
48
+ def visit_comment(node)
49
+ node.silent ? [] : node
50
+ end
51
+
52
+ # Prints the expression to STDERR.
53
+ def visit_debug(node)
54
+ res = node.expr.perform(@environment)
55
+ res = res.value if res.is_a?(Sass::Script::String)
56
+ if node.filename
57
+ $stderr.puts "#{node.filename}:#{node.line} DEBUG: #{res}"
58
+ else
59
+ $stderr.puts "Line #{node.line} DEBUG: #{res}"
60
+ end
61
+ []
62
+ end
63
+
64
+ # Runs the child nodes once for each value in the list.
65
+ def visit_each(node)
66
+ list = node.list.perform(@environment)
67
+
68
+ with_environment Sass::Environment.new(@environment) do
69
+ list.to_a.map do |v|
70
+ @environment.set_local_var(node.var, v)
71
+ node.children.map {|c| visit(c)}
72
+ end.flatten
73
+ end
74
+ end
75
+
76
+ # Runs SassScript interpolation in the selector,
77
+ # and then parses the result into a {Sass::Selector::CommaSequence}.
78
+ def visit_extend(node)
79
+ parser = Sass::SCSS::CssParser.new(run_interp(node.selector), node.line)
80
+ node.resolved_selector = parser.parse_selector(node.filename)
81
+ node
82
+ end
83
+
84
+ # Runs the child nodes once for each time through the loop, varying the variable each time.
85
+ def visit_for(node)
86
+ from = node.from.perform(@environment)
87
+ to = node.to.perform(@environment)
88
+ from.assert_int!
89
+ to.assert_int!
90
+
91
+ to = to.coerce(from.numerator_units, from.denominator_units)
92
+ range = Range.new(from.to_i, to.to_i, node.exclusive)
93
+
94
+ with_environment Sass::Environment.new(@environment) do
95
+ range.map do |i|
96
+ @environment.set_local_var(node.var,
97
+ Sass::Script::Number.new(i, from.numerator_units, from.denominator_units))
98
+ node.children.map {|c| visit(c)}
99
+ end.flatten
100
+ end
101
+ end
102
+
103
+ # Runs the child nodes if the conditional expression is true;
104
+ # otherwise, tries the else nodes.
105
+ def visit_if(node)
106
+ if node.expr.nil? || node.expr.perform(@environment).to_bool
107
+ yield
108
+ node.children
109
+ elsif node.else
110
+ visit(node.else)
111
+ else
112
+ []
113
+ end
114
+ end
115
+
116
+ # Returns a static DirectiveNode if this is importing a CSS file,
117
+ # or parses and includes the imported Sass file.
118
+ def visit_import(node)
119
+ if path = node.css_import?
120
+ return Sass::Tree::DirectiveNode.new("@import url(#{path})")
121
+ end
122
+
123
+ @environment.push_frame(:filename => node.filename, :line => node.line)
124
+ root = node.imported_file.to_tree
125
+ node.children = root.children.map {|c| visit(c)}.flatten
126
+ node
127
+ rescue Sass::SyntaxError => e
128
+ e.modify_backtrace(:filename => node.imported_file.options[:filename])
129
+ e.add_backtrace(:filename => node.filename, :line => node.line)
130
+ raise e
131
+ ensure
132
+ @environment.pop_frame
133
+ end
134
+
135
+ # Loads a mixin into the environment.
136
+ def visit_mixindef(node)
137
+ @environment.set_mixin(node.name,
138
+ Sass::Mixin.new(node.name, node.args, @environment, node.children))
139
+ []
140
+ end
141
+
142
+ # Runs a mixin.
143
+ def visit_mixin(node)
144
+ handle_include_loop!(node) if @environment.mixins_in_use.include?(node.name)
145
+
146
+ original_env = @environment
147
+ original_env.push_frame(:filename => node.filename, :line => node.line)
148
+ original_env.prepare_frame(:mixin => node.name)
149
+ raise Sass::SyntaxError.new("Undefined mixin '#{node.name}'.") unless mixin = @environment.mixin(node.name)
150
+
151
+ passed_args = node.args.dup
152
+ passed_keywords = node.keywords.dup
153
+
154
+ raise Sass::SyntaxError.new(<<END.gsub("\n", "")) if mixin.args.size < passed_args.size
155
+ Mixin #{node.name} takes #{mixin.args.size} argument#{'s' if mixin.args.size != 1}
156
+ but #{node.args.size} #{node.args.size == 1 ? 'was' : 'were'} passed.
157
+ END
158
+
159
+ passed_keywords.each do |name, value|
160
+ # TODO: Make this fast
161
+ unless mixin.args.find {|(var, default)| var.underscored_name == name}
162
+ raise Sass::SyntaxError.new("Mixin #{node.name} doesn't have an argument named $#{name}")
163
+ end
164
+ end
165
+
166
+ environment = mixin.args.zip(passed_args).
167
+ inject(Sass::Environment.new(mixin.environment)) do |env, ((var, default), value)|
168
+ env.set_local_var(var.name,
169
+ if value
170
+ value.perform(@environment)
171
+ elsif kv = passed_keywords[var.underscored_name]
172
+ kv.perform(env)
173
+ elsif default
174
+ val = default.perform(env)
175
+ if default.context == :equals && val.is_a?(Sass::Script::String)
176
+ val = Sass::Script::String.new(val.value)
177
+ end
178
+ val
179
+ end)
180
+ raise Sass::SyntaxError.new("Mixin #{node.name} is missing parameter #{var.inspect}.") unless env.var(var.name)
181
+ env
182
+ end
183
+
184
+ with_environment(environment) {node.children = mixin.tree.map {|c| visit(c)}.flatten}
185
+ node
186
+ rescue Sass::SyntaxError => e
187
+ if original_env # Don't add backtrace info if this is an @include loop
188
+ e.modify_backtrace(:mixin => node.name, :line => node.line)
189
+ e.add_backtrace(:line => node.line)
190
+ end
191
+ raise e
192
+ ensure
193
+ original_env.pop_frame if original_env
194
+ end
195
+
196
+ # Runs any SassScript that may be embedded in a property.
197
+ def visit_prop(node)
198
+ node.resolved_name = run_interp(node.name)
199
+ val = node.value.perform(@environment)
200
+ node.resolved_value =
201
+ if node.value.context == :equals && val.is_a?(Sass::Script::String)
202
+ val.value
203
+ else
204
+ val.to_s
205
+ end
206
+ yield
207
+ end
208
+
209
+ # Runs SassScript interpolation in the selector,
210
+ # and then parses the result into a {Sass::Selector::CommaSequence}.
211
+ def visit_rule(node)
212
+ parser = Sass::SCSS::StaticParser.new(run_interp(node.rule), node.line)
213
+ node.parsed_rules = parser.parse_selector(node.filename)
214
+ yield
215
+ end
216
+
217
+ # Loads the new variable value into the environment.
218
+ def visit_variable(node)
219
+ return [] if node.guarded && !@environment.var(node.name).nil?
220
+ val = node.expr.perform(@environment)
221
+ val = Sass::Script::String.new(val.value) if node.expr.context == :equals && val.is_a?(Sass::Script::String)
222
+ @environment.set_var(node.name, val)
223
+ []
224
+ end
225
+
226
+ # Prints the expression to STDERR with a stylesheet trace.
227
+ def visit_warn(node)
228
+ @environment.push_frame(:filename => node.filename, :line => node.line)
229
+ res = node.expr.perform(@environment)
230
+ res = res.value if res.is_a?(Sass::Script::String)
231
+ msg = "WARNING: #{res}\n"
232
+ @environment.stack.reverse.each_with_index do |entry, i|
233
+ msg << " #{i == 0 ? "on" : "from"} line #{entry[:line]}" <<
234
+ " of #{entry[:filename] || "an unknown file"}"
235
+ msg << ", in `#{entry[:mixin]}'" if entry[:mixin]
236
+ msg << "\n"
237
+ end
238
+ Sass::Util.sass_warn msg
239
+ []
240
+ ensure
241
+ @environment.pop_frame
242
+ end
243
+
244
+ # Runs the child nodes until the continuation expression becomes false.
245
+ def visit_while(node)
246
+ children = []
247
+ with_environment Sass::Environment.new(@environment) do
248
+ children += node.children.map {|c| visit(c)} while node.expr.perform(@environment).to_bool
249
+ end
250
+ children.flatten
251
+ end
252
+
253
+ private
254
+
255
+ def run_interp(text)
256
+ text.map do |r|
257
+ next r if r.is_a?(String)
258
+ val = r.perform(@environment)
259
+ # Interpolated strings should never render with quotes
260
+ next val.value if val.is_a?(Sass::Script::String)
261
+ val.to_s
262
+ end.join.strip
263
+ end
264
+
265
+ def handle_include_loop!(node)
266
+ msg = "An @include loop has been found:"
267
+ mixins = @environment.stack.map {|s| s[:mixin]}.compact
268
+ if mixins.size == 2 && mixins[0] == mixins[1]
269
+ raise Sass::SyntaxError.new("#{msg} #{node.name} includes itself")
270
+ end
271
+
272
+ mixins << node.name
273
+ msg << "\n" << Sass::Util.enum_cons(mixins, 2).map do |m1, m2|
274
+ " #{m1} includes #{m2}"
275
+ end.join("\n")
276
+ raise Sass::SyntaxError.new(msg)
277
+ end
278
+ end
@@ -0,0 +1,200 @@
1
+ # A visitor for converting a Sass tree into CSS.
2
+ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
3
+ protected
4
+
5
+ def initialize
6
+ @tabs = 0
7
+ end
8
+
9
+ def visit(node)
10
+ super
11
+ rescue Sass::SyntaxError => e
12
+ e.modify_backtrace(:filename => node.filename, :line => node.line)
13
+ raise e
14
+ end
15
+
16
+ def with_tabs(tabs)
17
+ old_tabs, @tabs = @tabs, tabs
18
+ yield
19
+ ensure
20
+ @tabs = old_tabs
21
+ end
22
+
23
+ def visit_root(node)
24
+ result = String.new
25
+ node.children.each do |child|
26
+ next if child.invisible?
27
+ child_str = visit(child)
28
+ result << child_str + (node.style == :compressed ? '' : "\n")
29
+ end
30
+ result.rstrip!
31
+ return "" if result.empty?
32
+ result << "\n"
33
+ unless Sass::Util.ruby1_8? || result.ascii_only?
34
+ if node.children.first.is_a?(Sass::Tree::CharsetNode)
35
+ begin
36
+ encoding = node.children.first.name
37
+ # Default to big-endian encoding, because we have to decide somehow
38
+ encoding << 'BE' if encoding =~ /\Autf-(16|32)\Z/i
39
+ result = result.encode(Encoding.find(encoding))
40
+ rescue EncodingError
41
+ end
42
+ end
43
+
44
+ result = "@charset \"#{result.encoding.name}\";#{
45
+ node.style == :compressed ? '' : "\n"
46
+ }".encode(result.encoding) + result
47
+ end
48
+ result
49
+ rescue Sass::SyntaxError => e
50
+ e.sass_template ||= node.template
51
+ raise e
52
+ end
53
+
54
+ def visit_charset(node)
55
+ "@charset \"#{node.name}\";"
56
+ end
57
+
58
+ def visit_comment(node)
59
+ return if node.invisible?
60
+ spaces = (' ' * [@tabs - node.value[/^ */].size, 0].max)
61
+
62
+ content = node.value.gsub(/^/, spaces)
63
+ content.gsub!(/\n +(\* *(?!\/))?/, ' ') if node.style == :compact
64
+ content
65
+ end
66
+
67
+ def visit_directive(node)
68
+ return node.value + ";" unless node.has_children
69
+ return node.value + " {}" if node.children.empty?
70
+ result = if node.style == :compressed
71
+ "#{node.value}{"
72
+ else
73
+ "#{' ' * @tabs}#{node.value} {" + (node.style == :compact ? ' ' : "\n")
74
+ end
75
+ was_prop = false
76
+ first = true
77
+ node.children.each do |child|
78
+ next if child.invisible?
79
+ if node.style == :compact
80
+ if child.is_a?(Sass::Tree::PropNode)
81
+ with_tabs(first || was_prop ? 0 : @tabs + 1) {result << visit(child) << ' '}
82
+ else
83
+ result[-1] = "\n" if was_prop
84
+ rendered = with_tabs(@tabs + 1) {visit(child).dup}
85
+ rendered = rendered.lstrip if first
86
+ result << rendered.rstrip + "\n"
87
+ end
88
+ was_prop = child.is_a?(Sass::Tree::PropNode)
89
+ first = false
90
+ elsif node.style == :compressed
91
+ result << (was_prop ? ";" : "") << with_tabs(0) {visit(child)}
92
+ was_prop = child.is_a?(Sass::Tree::PropNode)
93
+ else
94
+ result << with_tabs(@tabs + 1) {visit(child)} + "\n"
95
+ end
96
+ end
97
+ result.rstrip + if node.style == :compressed
98
+ "}"
99
+ else
100
+ (node.style == :expanded ? "\n" : " ") + "}\n"
101
+ end
102
+ end
103
+
104
+ def visit_media(node)
105
+ str = with_tabs(@tabs + node.tabs) {visit_directive(node)}
106
+ str.gsub!(/\n\Z/, '') unless node.style == :compressed || node.group_end
107
+ str
108
+ end
109
+
110
+ def visit_prop(node)
111
+ tab_str = ' ' * (@tabs + node.tabs)
112
+ if node.style == :compressed
113
+ "#{tab_str}#{node.resolved_name}:#{node.resolved_value}"
114
+ else
115
+ "#{tab_str}#{node.resolved_name}: #{node.resolved_value};"
116
+ end
117
+ end
118
+
119
+ def visit_rule(node)
120
+ with_tabs(@tabs + node.tabs) do
121
+ rule_separator = node.style == :compressed ? ',' : ', '
122
+ line_separator =
123
+ case node.style
124
+ when :nested, :expanded; "\n"
125
+ when :compressed; ""
126
+ else; " "
127
+ end
128
+ rule_indent = ' ' * @tabs
129
+ per_rule_indent, total_indent = [:nested, :expanded].include?(node.style) ? [rule_indent, ''] : ['', rule_indent]
130
+
131
+ total_rule = total_indent + node.resolved_rules.members.
132
+ map {|seq| seq.to_a.join.gsub(/([^,])\n/m, node.style == :compressed ? '\1 ' : "\\1\n")}.
133
+ join(rule_separator).split("\n").map do |line|
134
+ per_rule_indent + line.strip
135
+ end.join(line_separator)
136
+
137
+ to_return = ''
138
+ old_spaces = ' ' * @tabs
139
+ spaces = ' ' * (@tabs + 1)
140
+ if node.style != :compressed
141
+ if node.options[:debug_info]
142
+ to_return << visit(debug_info_rule(node.debug_info, node.options)) << "\n"
143
+ elsif node.options[:line_comments]
144
+ to_return << "#{old_spaces}/* line #{node.line}"
145
+
146
+ if node.filename
147
+ relative_filename = if node.options[:css_filename]
148
+ begin
149
+ Pathname.new(node.filename).relative_path_from(
150
+ Pathname.new(File.dirname(node.options[:css_filename]))).to_s
151
+ rescue ArgumentError
152
+ nil
153
+ end
154
+ end
155
+ relative_filename ||= node.filename
156
+ to_return << ", #{relative_filename}"
157
+ end
158
+
159
+ to_return << " */\n"
160
+ end
161
+ end
162
+
163
+ if node.style == :compact
164
+ properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(' ')}
165
+ to_return << "#{total_rule} { #{properties} }#{"\n" if node.group_end}"
166
+ elsif node.style == :compressed
167
+ properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(';')}
168
+ to_return << "#{total_rule}{#{properties}}"
169
+ else
170
+ properties = with_tabs(@tabs + 1) {node.children.map {|a| visit(a)}.join("\n")}
171
+ end_props = (node.style == :expanded ? "\n" + old_spaces : ' ')
172
+ to_return << "#{total_rule} {\n#{properties}#{end_props}}#{"\n" if node.group_end}"
173
+ end
174
+
175
+ to_return
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ def debug_info_rule(debug_info, options)
182
+ node = Sass::Tree::DirectiveNode.new("@media -sass-debug-info")
183
+ debug_info.map {|k, v| [k.to_s, v.to_s]}.sort.each do |k, v|
184
+ rule = Sass::Tree::RuleNode.new([""])
185
+ rule.resolved_rules = Sass::Selector::CommaSequence.new(
186
+ [Sass::Selector::Sequence.new(
187
+ [Sass::Selector::SimpleSequence.new(
188
+ [Sass::Selector::Element.new(k.to_s.gsub(/[^\w-]/, "\\\\\\0"), nil)])
189
+ ])
190
+ ])
191
+ prop = Sass::Tree::PropNode.new([""], "", :new)
192
+ prop.resolved_name = "font-family"
193
+ prop.resolved_value = Sass::SCSS::RX.escape_ident(v.to_s)
194
+ rule << prop
195
+ node << rule
196
+ end
197
+ node.options = options.merge(:debug_info => false, :line_comments => false, :style => :compressed)
198
+ node
199
+ end
200
+ end