sass 3.3.0.alpha.16 → 3.3.0.alpha.50

Sign up to get free protection for your applications and to get access to all the features.
@@ -42,7 +42,7 @@ module Sass
42
42
  def interp_ident(ident = IDENT); s = tok(ident) and [s]; end
43
43
  def use_css_import?; true; end
44
44
 
45
- def special_directive(name)
45
+ def special_directive(name, start_pos)
46
46
  return unless %w[media import charset -moz-document].include?(name)
47
47
  super
48
48
  end
@@ -0,0 +1,159 @@
1
+ require 'pathname'
2
+
3
+ module Sass::Source
4
+ class Map
5
+ # A mapping from one source range to another. Indicates that `input` was
6
+ # compiled to `output`.
7
+ #
8
+ # @!attribute input
9
+ # @return [Sass::Source::Range] The source range in the input document.
10
+ #
11
+ # @!attribute output
12
+ # @return [Sass::Source::Range] The source range in the output document.
13
+ class Mapping < Struct.new(:input, :output)
14
+ # @return [String] A string representation of the mapping.
15
+ def inspect
16
+ "#{input.inspect} => #{output.inspect}"
17
+ end
18
+ end
19
+
20
+ # The mapping data ordered by the location in the target.
21
+ #
22
+ # @return [Array<Mapping>]
23
+ attr_reader :data
24
+
25
+ def initialize
26
+ @data = []
27
+ end
28
+
29
+ # Adds a new mapping from one source range to another. Multiple invocations
30
+ # of this method should have each `output` range come after all previous ranges.
31
+ #
32
+ # @param input [Sass::Source::Range]
33
+ # The source range in the input document.
34
+ # @param output [Sass::Source::Range]
35
+ # The source range in the output document.
36
+ def add(input, output)
37
+ @data.push(Mapping.new(input, output))
38
+ end
39
+
40
+ # Shifts all output source ranges forward one or more lines.
41
+ #
42
+ # @param delta [Fixnum] The number of lines to shift the ranges forward.
43
+ def shift_output_lines(delta)
44
+ return if delta == 0
45
+ @data.each do |m|
46
+ m.output.start_pos.line += delta
47
+ m.output.end_pos.line += delta
48
+ end
49
+ end
50
+
51
+ # Shifts any output source ranges that lie on the first line forward one or
52
+ # more characters on that line.
53
+ #
54
+ # @param delta [Fixnum] The number of characters to shift the ranges
55
+ # forward.
56
+ def shift_output_offsets(delta)
57
+ return if delta == 0
58
+ @data.each do |m|
59
+ break if m.output.start_pos.line > 1
60
+ m.output.start_pos.offset += delta
61
+ m.output.end_pos.offset += delta if m.output.end_pos.line > 1
62
+ end
63
+ end
64
+
65
+ # Returns the standard JSON representation of the source map.
66
+ #
67
+ # @param target_filename [String] The filename of the output file; that is,
68
+ # the target of the mapping. This should be relative to the working
69
+ # directory.
70
+ # @return [String] The JSON string.
71
+ def to_json(target_filename)
72
+ result = "{\n"
73
+ write_json_field(result, "version", "3", true)
74
+
75
+ source_pathname_to_id = {}
76
+ id_to_source_pathname = {}
77
+ next_source_id = 0
78
+ line_data = []
79
+ segment_data_for_line = []
80
+
81
+ # These track data necessary for the delta coding.
82
+ previous_target_line = nil
83
+ previous_target_offset = 1
84
+ previous_source_line = 1
85
+ previous_source_offset = 1
86
+ previous_source_id = 0
87
+
88
+ target_pathname = Pathname.pwd.join(Pathname.new(target_filename)).cleanpath
89
+ @data.each do |m|
90
+ source_pathname = Pathname.pwd.join(Pathname.new(m.input.file)).cleanpath
91
+ source_pathname = source_pathname.relative_path_from(target_pathname.dirname)
92
+ current_source_id = source_pathname_to_id[source_pathname]
93
+ unless current_source_id
94
+ current_source_id = next_source_id
95
+ next_source_id += 1
96
+
97
+ source_pathname_to_id[source_pathname] = current_source_id
98
+ id_to_source_pathname[current_source_id] = source_pathname
99
+ end
100
+
101
+ [
102
+ [m.input.start_pos, m.output.start_pos],
103
+ [m.input.end_pos, m.output.end_pos]
104
+ ].each do |source_pos, target_pos|
105
+ if previous_target_line != target_pos.line
106
+ line_data.push(segment_data_for_line.join(",")) unless segment_data_for_line.empty?
107
+ (target_pos.line - 1 - (previous_target_line || 0)).times {line_data.push("")}
108
+ previous_target_line = target_pos.line
109
+ previous_target_offset = 1
110
+ segment_data_for_line = []
111
+ end
112
+
113
+ # `segment` is a data chunk for a single position mapping.
114
+ segment = ""
115
+
116
+ # Field 1: zero-based starting offset.
117
+ segment << Sass::Util.encode_vlq(target_pos.offset - previous_target_offset)
118
+ previous_target_offset = target_pos.offset
119
+
120
+ # Field 2: zero-based index into the "sources" list.
121
+ segment << Sass::Util.encode_vlq(current_source_id - previous_source_id)
122
+ previous_source_id = current_source_id
123
+
124
+ # Field 3: zero-based starting line in the original source.
125
+ segment << Sass::Util.encode_vlq(source_pos.line - previous_source_line)
126
+ previous_source_line = source_pos.line
127
+
128
+ # Field 4: zero-based starting offset in the original source.
129
+ segment << Sass::Util.encode_vlq(source_pos.offset - previous_source_offset)
130
+ previous_source_offset = source_pos.offset
131
+
132
+ segment_data_for_line.push(segment)
133
+
134
+ previous_target_line = target_pos.line
135
+ end
136
+ end
137
+ line_data.push(segment_data_for_line.join(","))
138
+ write_json_field(result, "mappings", line_data.join(";"))
139
+
140
+ source_names = []
141
+ (0...next_source_id).each {|id| source_names.push(id_to_source_pathname[id].to_s)}
142
+ write_json_field(result, "sources", source_names)
143
+ write_json_field(result, "file", target_pathname.basename.to_s)
144
+
145
+ result << "\n}"
146
+ result
147
+ end
148
+
149
+ private
150
+
151
+ def write_json_field(out, name, value, is_first = false)
152
+ out << (is_first ? "" : ",\n") <<
153
+ "\"" <<
154
+ Sass::Util.json_escape_string(name) <<
155
+ "\": " <<
156
+ Sass::Util.json_value_of(value)
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,26 @@
1
+ module Sass::Source
2
+ class Position
3
+ # The one-based line of the document associated with the position.
4
+ #
5
+ # @return [Fixnum]
6
+ attr_accessor :line
7
+
8
+ # The one-based offset in the line of the document associated with the
9
+ # position.
10
+ #
11
+ # @return [Fixnum]
12
+ attr_accessor :offset
13
+
14
+ # @param line [Fixnum] The source line
15
+ # @param offset [Fixnum] The source offset
16
+ def initialize(line, offset)
17
+ @line = line
18
+ @offset = offset
19
+ end
20
+
21
+ # @return [String] A string representation of the source position.
22
+ def inspect
23
+ "#{line.inspect}:#{offset.inspect}"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module Sass::Source
2
+ class Range
3
+ # The starting position of the range in the document (inclusive).
4
+ #
5
+ # @return [Sass::Source::Position]
6
+ attr_accessor :start_pos
7
+
8
+ # The ending position of the range in the document (exclusive).
9
+ #
10
+ # @return [Sass::Source::Position]
11
+ attr_accessor :end_pos
12
+
13
+ # The file in which this source range appears. This can be nil if the file
14
+ # is unknown or not yet generated.
15
+ #
16
+ # @return [String]
17
+ attr_accessor :file
18
+
19
+ # @param start_pos [Sass::Source::Position] See \{#start_pos}
20
+ # @param end_pos [Sass::Source::Position] See \{#end_pos}
21
+ # @param file [String] See \{#file}
22
+ def initialize(start_pos, end_pos, file)
23
+ raise 'hell' if end_pos.is_a?(Fixnum)
24
+ @start_pos = start_pos
25
+ @end_pos = end_pos
26
+ @file = file
27
+ end
28
+
29
+ # @return [String] A string representation of the source range.
30
+ def inspect
31
+ "(#{start_pos.inspect} to #{end_pos.inspect}#{" in #{@file}" if @file})"
32
+ end
33
+ end
34
+ end
@@ -30,7 +30,7 @@ module Sass::Tree
30
30
 
31
31
  # @param uri [String, Sass::Script::Node] See \{#uri}
32
32
  # @param query [Array<String, Sass::Script::Node>] See \{#query}
33
- def initialize(uri, query = nil)
33
+ def initialize(uri, query = [])
34
34
  @uri = uri
35
35
  @query = query
36
36
  super('')
@@ -46,6 +46,11 @@ module Sass
46
46
  # @return [Fixnum]
47
47
  attr_accessor :line
48
48
 
49
+ # The source range in the document on which this node appeared.
50
+ #
51
+ # @return [Sass::Source::Range]
52
+ attr_accessor :source_range
53
+
49
54
  # The name of the document on which this node appeared.
50
55
  #
51
56
  # @return [String]
@@ -127,10 +132,21 @@ module Sass
127
132
 
128
133
  # Computes the CSS corresponding to this static CSS tree.
129
134
  #
130
- # @return [String, nil] The resulting CSS
135
+ # @return [String] The resulting CSS
136
+ # @see Sass::Tree
137
+ def css
138
+ Sass::Tree::Visitors::ToCss.new.visit(self)
139
+ end
140
+
141
+ # Computes the CSS corresponding to this static CSS tree, along with
142
+ # the respective source map.
143
+ #
144
+ # @return [(String, Sass::Source::Map)] The resulting CSS and the source map
131
145
  # @see Sass::Tree
132
- def to_s
133
- Sass::Tree::Visitors::ToCss.visit(self)
146
+ def css_with_sourcemap
147
+ visitor = Sass::Tree::Visitors::ToCss.new(:build_source_mapping)
148
+ result = visitor.visit(self)
149
+ return result, visitor.source_mapping
134
150
  end
135
151
 
136
152
  # Returns a representation of the node for debugging purposes.
@@ -1,5 +1,5 @@
1
1
  module Sass::Tree
2
- # A static node reprenting a CSS property.
2
+ # A static node representing a CSS property.
3
3
  #
4
4
  # @see Sass::Tree
5
5
  class PropNode < Node
@@ -42,6 +42,16 @@ module Sass::Tree
42
42
  # @return [Fixnum]
43
43
  attr_accessor :tabs
44
44
 
45
+ # The source range in which the property name appears.
46
+ #
47
+ # @return [Sass::Source::Range]
48
+ attr_accessor :name_source_range
49
+
50
+ # The source range in which the property value appears.
51
+ #
52
+ # @return [Sass::Source::Range]
53
+ attr_accessor :value_source_range
54
+
45
55
  # @param name [Array<String, Sass::Script::Node>] See \{#name}
46
56
  # @param value [Sass::Script::Node] See \{#value}
47
57
  # @param prop_syntax [Symbol] `:new` if this property uses `a: b`-style syntax,
@@ -13,15 +13,31 @@ module Sass
13
13
  @template = template
14
14
  end
15
15
 
16
- # Runs the dynamic Sass code *and* computes the CSS for the tree.
17
- # @see #to_s
16
+ # Runs the dynamic Sass code and computes the CSS for the tree.
17
+ #
18
+ # @return [String] The compiled CSS.
18
19
  def render
20
+ css_tree.css
21
+ end
22
+
23
+ # Runs the dynamic Sass code and computes the CSS for the tree, along with
24
+ # the sourcemap.
25
+ #
26
+ # @return [(String, Sass::Source::Map)] The compiled CSS, as well as
27
+ # the source map. @see #render
28
+ def render_with_sourcemap
29
+ css_tree.css_with_sourcemap
30
+ end
31
+
32
+ private
33
+
34
+ def css_tree
19
35
  Visitors::CheckNesting.visit(self)
20
36
  result = Visitors::Perform.visit(self)
21
37
  Visitors::CheckNesting.visit(result) # Check again to validate mixins
22
38
  result, extends = Visitors::Cssize.visit(result)
23
39
  Visitors::Extend.visit(result, extends)
24
- result.to_s
40
+ result
25
41
  end
26
42
  end
27
43
  end
@@ -213,7 +213,9 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
213
213
  # or parses and includes the imported Sass file.
214
214
  def visit_import(node)
215
215
  if path = node.css_import?
216
- return Sass::Tree::CssImportNode.resolved("url(#{path})")
216
+ resolved_node = Sass::Tree::CssImportNode.resolved("url(#{path})")
217
+ resolved_node.source_range = node.source_range
218
+ return resolved_node
217
219
  end
218
220
  file = node.imported_file
219
221
  handle_import_loop!(node) if @stack.any? {|e| e[:filename] == file.options[:filename]}
@@ -295,6 +297,7 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
295
297
  node.resolved_name = run_interp(node.name)
296
298
  val = node.value.perform(@environment)
297
299
  node.resolved_value = val.to_s
300
+ node.value_source_range = val.source_range if val.source_range
298
301
  yield
299
302
  end
300
303
 
@@ -323,6 +326,11 @@ class Sass::Tree::Visitors::Perform < Sass::Tree::Visitors::Base
323
326
  var = @environment.var(node.name)
324
327
  return [] if node.guarded && var && !var.null?
325
328
  val = node.expr.perform(@environment)
329
+ if node.expr.source_range
330
+ val.source_range = node.expr.source_range
331
+ else
332
+ val.source_range = node.source_range
333
+ end
326
334
  @environment.set_var(node.name, val)
327
335
  []
328
336
  end
@@ -1,11 +1,25 @@
1
1
  # A visitor for converting a Sass tree into CSS.
2
2
  class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
3
- protected
3
+ # The source mapping for the generated CSS file. This is only set if
4
+ # `build_source_mapping` is passed to the constructor and \{#render} has been
5
+ # run.
6
+ attr_reader :source_mapping
4
7
 
5
- def initialize
8
+ # @param build_source_mapping [Boolean] Whether to build a
9
+ # \{Sass::Source::Map} while creating the CSS output. The mapping will
10
+ # be available from \{#source\_mapping} after the visitor has completed.
11
+ def initialize(build_source_mapping = false)
6
12
  @tabs = 0
13
+ @line = 1
14
+ @offset = 1
15
+ @result = ""
16
+ @source_mapping = Sass::Source::Map.new if build_source_mapping
7
17
  end
8
18
 
19
+ # Runs the visitor on `node`.
20
+ #
21
+ # @param node [Sass::Tree::Node] The root node of the tree to convert to CSS>
22
+ # @return [String] The CSS output.
9
23
  def visit(node)
10
24
  super
11
25
  rescue Sass::SyntaxError => e
@@ -13,6 +27,8 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
13
27
  raise e
14
28
  end
15
29
 
30
+ protected
31
+
16
32
  def with_tabs(tabs)
17
33
  old_tabs, @tabs = @tabs, tabs
18
34
  yield
@@ -20,40 +36,116 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
20
36
  @tabs = old_tabs
21
37
  end
22
38
 
39
+ # Associate all output produced in a block with a given node. Used for source
40
+ # mapping.
41
+ def for_node(node, attr_prefix = nil)
42
+ return yield unless @source_mapping
43
+ start_pos = Sass::Source::Position.new(@line, @offset)
44
+ yield
45
+
46
+ range_attr = attr_prefix ? :"#{attr_prefix}_source_range" : :source_range
47
+ return if node.invisible? || !node.send(range_attr)
48
+ source_range = node.send(range_attr)
49
+ target_end_pos = Sass::Source::Position.new(@line, @offset)
50
+ target_range = Sass::Source::Range.new(start_pos, target_end_pos, nil)
51
+ @source_mapping.add(source_range, target_range)
52
+ end
53
+
54
+ # Move the output cursor back `chars` characters.
55
+ def erase!(chars)
56
+ return if chars == 0
57
+ str = @result.slice!(-chars..-1)
58
+ newlines = str.count("\n")
59
+ if newlines > 0
60
+ @line -= newlines
61
+ @offset = @result[@result.rindex("\n") || 0..-1].size
62
+ else
63
+ @offset -= chars
64
+ end
65
+ end
66
+
67
+ # Avoid allocating lots of new strings for `#output`. This is important
68
+ # because `#output` is called all the time.
69
+ NEWLINE = "\n"
70
+
71
+ # Add `s` to the output string and update the line and offset information
72
+ # accordingly.
73
+ def output(s)
74
+ if @lstrip
75
+ s = s.gsub(/\A\s+/, "")
76
+ @lstrip = false
77
+ end
78
+
79
+ newlines = s.count(NEWLINE)
80
+ if newlines > 0
81
+ @line += newlines
82
+ @offset = s[s.rindex(NEWLINE)..-1].size
83
+ else
84
+ @offset += s.size
85
+ end
86
+
87
+ @result << s
88
+ end
89
+
90
+ # Strip all trailing whitespace from the output string.
91
+ def rstrip!
92
+ erase! @result.length - 1 - (@result.rindex(/[^\s]/) || -1)
93
+ end
94
+
95
+ # lstrip the first output in the given block.
96
+ def lstrip
97
+ old_lstrip = @lstrip
98
+ @lstrip = true
99
+ yield
100
+ ensure
101
+ @lstrip = @lstrip && old_lstrip
102
+ end
103
+
104
+ # Prepend `prefix` to the output string.
105
+ def prepend!(prefix)
106
+ @result.insert 0, prefix
107
+ return unless @source_mapping
108
+
109
+ line_delta = prefix.count("\n")
110
+ offset_delta = prefix.gsub(/.*\n/, '').size
111
+ @source_mapping.shift_output_offsets(offset_delta)
112
+ @source_mapping.shift_output_lines(line_delta)
113
+ end
114
+
23
115
  def visit_root(node)
24
- result = String.new
25
116
  node.children.each do |child|
26
117
  next if child.invisible?
27
- child_str = visit(child)
28
- result << child_str + (node.style == :compressed ? '' : "\n")
118
+ visit(child)
119
+ output "\n" unless node.style == :compressed
29
120
  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
121
+ rstrip!
122
+ return "" if @result.empty?
123
+
124
+ output "\n"
125
+ return @result if Sass::Util.ruby1_8? || @result.ascii_only?
43
126
 
44
- result = "@charset \"#{result.encoding.name}\";#{
45
- node.style == :compressed ? '' : "\n"
46
- }".encode(result.encoding) + result
127
+ if node.children.first.is_a?(Sass::Tree::CharsetNode)
128
+ begin
129
+ encoding = node.children.first.name
130
+ # Default to big-endian encoding, because we have to decide somehow
131
+ encoding << 'BE' if encoding =~ /\Autf-(16|32)\Z/i
132
+ @result = @result.encode(Encoding.find(encoding))
133
+ rescue EncodingError
134
+ end
47
135
  end
48
- result
136
+
137
+ prepend! "@charset \"#{@result.encoding.name}\";#{
138
+ node.style == :compressed ? '' : "\n"
139
+ }".encode(@result.encoding)
140
+ @result
49
141
  rescue Sass::SyntaxError => e
50
142
  e.sass_template ||= node.template
51
143
  raise e
52
144
  end
53
145
 
54
146
  def visit_charset(node)
55
- "@charset \"#{node.name}\";"
56
- end
147
+ for_node(node) {output("@charset \"#{node.name}\";")}
148
+ end
57
149
 
58
150
  def visit_comment(node)
59
151
  return if node.invisible?
@@ -62,55 +154,74 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
62
154
  content = node.resolved_value.gsub(/^/, spaces)
63
155
  content.gsub!(%r{^(\s*)//(.*)$}) {|md| "#{$1}/*#{$2} */"} if node.type == :silent
64
156
  content.gsub!(/\n +(\* *(?!\/))?/, ' ') if (node.style == :compact || node.style == :compressed) && node.type != :loud
65
- content
157
+ for_node(node) {output(content)}
66
158
  end
67
159
 
68
160
  def visit_directive(node)
69
161
  was_in_directive = @in_directive
70
162
  tab_str = ' ' * @tabs
71
- return tab_str + node.resolved_value + ";" unless node.has_children
72
- return tab_str + node.resolved_value + " {}" if node.children.empty?
163
+ if !node.has_children || node.children.empty?
164
+ output(tab_str)
165
+ for_node(node) {output(node.resolved_value)}
166
+ output(!node.has_children ? ";" : " {}")
167
+ return
168
+ end
169
+
73
170
  @in_directive = @in_directive || !node.is_a?(Sass::Tree::MediaNode)
74
- result = if node.style == :compressed
75
- "#{node.resolved_value}{"
76
- else
77
- "#{tab_str}#{node.resolved_value} {" + (node.style == :compact ? ' ' : "\n")
78
- end
171
+ output(tab_str) if node.style != :compressed
172
+ for_node(node) {output(node.resolved_value)}
173
+ output(node.style == :compressed ? "{" : " {")
174
+ output(node.style == :compact ? ' ' : "\n") if node.style != :compressed
175
+
79
176
  was_prop = false
80
177
  first = true
81
178
  node.children.each do |child|
82
179
  next if child.invisible?
83
180
  if node.style == :compact
84
181
  if child.is_a?(Sass::Tree::PropNode)
85
- with_tabs(first || was_prop ? 0 : @tabs + 1) {result << visit(child) << ' '}
182
+ with_tabs(first || was_prop ? 0 : @tabs + 1) do
183
+ visit(child)
184
+ output(' ')
185
+ end
86
186
  else
87
- result[-1] = "\n" if was_prop
88
- rendered = with_tabs(@tabs + 1) {visit(child).dup}
89
- rendered = rendered.lstrip if first
90
- result << rendered.rstrip + "\n"
187
+ if was_prop
188
+ erase! 1
189
+ output "\n"
190
+ end
191
+
192
+ if first
193
+ lstrip {with_tabs(@tabs + 1) {visit(child)}}
194
+ else
195
+ with_tabs(@tabs + 1) {visit(child)}
196
+ end
197
+
198
+ rstrip!
199
+ output "\n"
91
200
  end
92
201
  was_prop = child.is_a?(Sass::Tree::PropNode)
93
202
  first = false
94
203
  elsif node.style == :compressed
95
- result << (was_prop ? ";" : "") << with_tabs(0) {visit(child)}
204
+ output(was_prop ? ";" : "")
205
+ with_tabs(0) {visit(child)}
96
206
  was_prop = child.is_a?(Sass::Tree::PropNode)
97
207
  else
98
- result << with_tabs(@tabs + 1) {visit(child)} + "\n"
208
+ with_tabs(@tabs + 1) {visit(child)}
209
+ output "\n"
99
210
  end
100
211
  end
101
- result.rstrip + if node.style == :compressed
102
- "}"
103
- else
104
- (node.style == :expanded ? "\n" : " ") + "}\n"
105
- end
212
+ rstrip!
213
+ output(if node.style == :compressed
214
+ "}"
215
+ else
216
+ (node.style == :expanded ? "\n" : " ") + "}\n"
217
+ end)
106
218
  ensure
107
219
  @in_directive = was_in_directive
108
220
  end
109
221
 
110
222
  def visit_media(node)
111
- str = with_tabs(@tabs + node.tabs) {visit_directive(node)}
112
- str.gsub!(/\n\Z/, '') unless node.style == :compressed || node.group_end
113
- str
223
+ with_tabs(@tabs + node.tabs) {visit_directive(node)}
224
+ erase! 1 unless node.style == :compressed || node.group_end || @result[-1] != ?\n
114
225
  end
115
226
 
116
227
  def visit_supports(node)
@@ -124,10 +235,15 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
124
235
  def visit_prop(node)
125
236
  return if node.resolved_value.empty?
126
237
  tab_str = ' ' * (@tabs + node.tabs)
238
+ output(tab_str)
239
+ for_node(node, :name) {output(node.resolved_name)}
127
240
  if node.style == :compressed
128
- "#{tab_str}#{node.resolved_name}:#{node.resolved_value}"
241
+ output(":");
242
+ for_node(node, :value) {output(node.resolved_value)}
129
243
  else
130
- "#{tab_str}#{node.resolved_name}: #{node.resolved_value};"
244
+ output(": ")
245
+ for_node(node, :value) {output(node.resolved_value)}
246
+ output(";")
131
247
  end
132
248
  end
133
249
 
@@ -158,18 +274,18 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
158
274
  joined_rules.gsub!(/\s*\n\s*/, "#{line_separator}#{per_rule_indent}")
159
275
  total_rule = total_indent << joined_rules
160
276
 
161
- to_return = ''
162
277
  old_spaces = ' ' * @tabs
163
278
  spaces = ' ' * (@tabs + 1)
164
279
  if node.style != :compressed
165
280
  if node.options[:debug_info] && !@in_directive
166
- to_return << visit(debug_info_rule(node.debug_info, node.options)) << "\n"
281
+ visit(debug_info_rule(node.debug_info, node.options))
282
+ output "\n"
167
283
  elsif node.options[:trace_selectors]
168
- to_return << "#{old_spaces}/* "
169
- to_return << node.stack_trace.join("\n #{old_spaces}")
170
- to_return << " */\n"
284
+ output("#{old_spaces}/* ")
285
+ output(node.stack_trace.join("\n #{old_spaces}"))
286
+ output(" */\n")
171
287
  elsif node.options[:line_comments]
172
- to_return << "#{old_spaces}/* line #{node.line}"
288
+ output("#{old_spaces}/* line #{node.line}")
173
289
 
174
290
  if node.filename
175
291
  relative_filename = if node.options[:css_filename]
@@ -181,26 +297,37 @@ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
181
297
  end
182
298
  end
183
299
  relative_filename ||= node.filename
184
- to_return << ", #{relative_filename}"
300
+ output(", #{relative_filename}")
185
301
  end
186
302
 
187
- to_return << " */\n"
303
+ output(" */\n")
188
304
  end
189
305
  end
190
306
 
307
+ end_props, trailer, tabs = '', '', 0
191
308
  if node.style == :compact
192
- properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(' ')}
193
- to_return << "#{total_rule} { #{properties} }#{"\n" if node.group_end}"
309
+ separator, end_props, bracket = ' ', ' ', ' { '
310
+ trailer = "\n" if node.group_end
194
311
  elsif node.style == :compressed
195
- properties = with_tabs(0) {node.children.map {|a| visit(a)}.join(';')}
196
- to_return << "#{total_rule}{#{properties}}"
312
+ separator, bracket = ';', '{'
197
313
  else
198
- properties = with_tabs(@tabs + 1) {node.children.map {|a| visit(a)}.join("\n")}
314
+ tabs = @tabs + 1
315
+ separator, bracket = "\n", " {\n"
316
+ trailer = "\n" if node.group_end
199
317
  end_props = (node.style == :expanded ? "\n" + old_spaces : ' ')
200
- to_return << "#{total_rule} {\n#{properties}#{end_props}}#{"\n" if node.group_end}"
318
+ end
319
+ output(total_rule)
320
+ output(bracket)
321
+
322
+ with_tabs(tabs) do
323
+ node.children.each_with_index do |child, i|
324
+ output(separator) if i > 0
325
+ visit(child)
326
+ end
201
327
  end
202
328
 
203
- to_return
329
+ output(end_props)
330
+ output("}" + trailer)
204
331
  end
205
332
  end
206
333