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.
@@ -17,10 +17,10 @@ module Sass
17
17
  # : The Ruby object corresponding to the value of the token.
18
18
  #
19
19
  # `line`: \[`Fixnum`\]
20
- # : The line of the source file on which the token appears.
20
+ # : The line of the source file on which the token appears (1-based).
21
21
  #
22
22
  # `offset`: \[`Fixnum`\]
23
- # : The number of bytes into the line the SassScript token appeared.
23
+ # : The number of characters into the line the SassScript token appeared (1-based).
24
24
  #
25
25
  # `pos`: \[`Fixnum`\]
26
26
  # : The scanner position at which the SassScript token appeared.
@@ -32,7 +32,7 @@ module Sass
32
32
  attr_reader :line
33
33
 
34
34
  # The number of bytes into the current line
35
- # of the lexer's current position.
35
+ # of the lexer's current position (1-based).
36
36
  #
37
37
  # @return [Fixnum]
38
38
  attr_reader :offset
@@ -125,10 +125,10 @@ module Sass
125
125
  }
126
126
 
127
127
  # @param str [String, StringScanner] The source text to lex
128
- # @param line [Fixnum] The line on which the SassScript appears.
129
- # Used for error reporting
130
- # @param offset [Fixnum] The number of characters in on which the SassScript appears.
131
- # Used for error reporting
128
+ # @param line [Fixnum] The 1-based line on which the SassScript appears.
129
+ # Used for error reporting and sourcemap building
130
+ # @param offset [Fixnum] The 1-based character (not byte) offset in the line on which the SassScript appears.
131
+ # Used for error reporting and sourcemap building
132
132
  # @param options [{Symbol => Object}] An options hash;
133
133
  # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
134
134
  def initialize(str, line, offset, options)
@@ -172,7 +172,11 @@ module Sass
172
172
  # Rewinds the underlying StringScanner
173
173
  # to before the token returned by \{#peek}.
174
174
  def unpeek!
175
- @scanner.pos = @tok.pos if @tok
175
+ if @tok
176
+ @scanner.pos = @tok.pos
177
+ @line = @tok.line
178
+ @offset = @tok.offset
179
+ end
176
180
  end
177
181
 
178
182
  # @return [Boolean] Whether or not there's more source text to lex.
@@ -220,8 +224,7 @@ module Sass
220
224
  size ||= @scanner.matched_size
221
225
 
222
226
  val.line = @line if val.is_a?(Script::Node)
223
- Token.new(type, val, @line,
224
- current_position - size, @scanner.pos - size)
227
+ Token.new(type, val, @line, @offset - size, @scanner.pos - size)
225
228
  end
226
229
 
227
230
  def whitespace
@@ -258,9 +261,11 @@ module Sass
258
261
  end
259
262
 
260
263
  def string(re, open)
264
+ start_pos = source_position
261
265
  return unless scan(STRING_REGULAR_EXPRESSIONS[[re, open]])
262
266
  if @scanner[2] == '#{' #'
263
267
  @scanner.pos -= 2 # Don't actually consume the #{
268
+ @offset -= 2
264
269
  @interpolation_stack << re
265
270
  end
266
271
  str =
@@ -269,34 +274,47 @@ module Sass
269
274
  else
270
275
  Script::String.new(@scanner[1].gsub(/\\(['"]|\#\{)/, '\1'), :string)
271
276
  end
277
+ str.source_range = range(start_pos)
272
278
  [:string, str]
273
279
  end
274
280
 
275
281
  def number
282
+ start_pos = source_position
276
283
  return unless scan(REGULAR_EXPRESSIONS[:number])
277
284
  value = @scanner[2] ? @scanner[2].to_f : @scanner[3].to_i
278
285
  value = -value if @scanner[1]
279
- [:number, Script::Number.new(value, Array(@scanner[4]))]
286
+ script_number = Script::Number.new(value, Array(@scanner[4]))
287
+ script_number.source_range = range(start_pos)
288
+ [:number, script_number]
280
289
  end
281
290
 
282
291
  def color
292
+ start_pos = source_position
283
293
  return unless s = scan(REGULAR_EXPRESSIONS[:color])
284
294
  raise Sass::SyntaxError.new(<<MESSAGE.rstrip) unless s.size == 4 || s.size == 7
285
295
  Colors must have either three or six digits: '#{s}'
286
296
  MESSAGE
287
297
  value = s.scan(/^#(..?)(..?)(..?)$/).first.
288
298
  map {|num| num.ljust(2, num).to_i(16)}
289
- [:color, Script::Color.new(value)]
299
+ script_color = Script::Color.new(value)
300
+ script_color.source_range = range(start_pos)
301
+ [:color, script_color]
290
302
  end
291
303
 
292
304
  def bool
305
+ start_pos = source_position
293
306
  return unless s = scan(REGULAR_EXPRESSIONS[:bool])
294
- [:bool, Script::Bool.new(s == 'true')]
307
+ script_bool = Script::Bool.new(s == 'true')
308
+ script_bool.source_range = range(start_pos)
309
+ [:bool, script_bool]
295
310
  end
296
311
 
297
312
  def null
313
+ start_pos = source_position
298
314
  return unless scan(REGULAR_EXPRESSIONS[:null])
299
- [:null, Script::Null.new]
315
+ script_null = Script::Null.new
316
+ script_null.source_range = range(start_pos)
317
+ [:null, script_null]
300
318
  end
301
319
 
302
320
  def special_fun
@@ -306,7 +324,7 @@ MESSAGE
306
324
  old_line = @line
307
325
  old_offset = @offset
308
326
  @line += c
309
- @offset = (c == 0 ? @offset + str2.size : str2[/\n(.*)/, 1].size)
327
+ @offset = c == 0 ? @offset + str2.size : str2[/\n([^\n]*)/, 1].size + 1
310
328
  [:special_fun,
311
329
  Sass::Util.merge_adjacent_strings(
312
330
  [str1] + Sass::Engine.parse_interp(str2, old_line, old_offset, @options)),
@@ -338,12 +356,16 @@ MESSAGE
338
356
  return unless str = @scanner.scan(re)
339
357
  c = str.count("\n")
340
358
  @line += c
341
- @offset = (c == 0 ? @offset + str.size : str[/\n(.*)/, 1].size)
359
+ @offset = (c == 0 ? @offset + str.size : str[/\n([^\n]*)/, 1].size + 1)
342
360
  str
343
361
  end
344
362
 
345
- def current_position
346
- @offset + 1
363
+ def range(start_pos, end_pos = source_position)
364
+ Sass::Source::Range.new(start_pos, end_pos, @options[:filename])
365
+ end
366
+
367
+ def source_position
368
+ Sass::Source::Position.new(@line, @offset)
347
369
  end
348
370
  end
349
371
  end
@@ -13,6 +13,16 @@ module Sass::Script
13
13
  # @return [Fixnum]
14
14
  attr_accessor :line
15
15
 
16
+ # The source range in the document on which this node appeared.
17
+ #
18
+ # @return [Sass::Source::Range]
19
+ attr_accessor :source_range
20
+
21
+ # The file name of the document on which this node appeared.
22
+ #
23
+ # @return [String]
24
+ attr_accessor :filename
25
+
16
26
  # Sets the options hash for this node,
17
27
  # as well as for all child nodes.
18
28
  # See {file:SASS_REFERENCE.md#sass_options the Sass options documentation}.
@@ -12,11 +12,18 @@ module Sass
12
12
  @lexer.line
13
13
  end
14
14
 
15
+ # The column number of the parser's current position.
16
+ #
17
+ # @return [Fixnum]
18
+ def offset
19
+ @lexer.offset
20
+ end
21
+
15
22
  # @param str [String, StringScanner] The source text to parse
16
23
  # @param line [Fixnum] The line on which the SassScript appears.
17
- # Used for error reporting
18
- # @param offset [Fixnum] The number of characters in on which the SassScript appears.
19
- # Used for error reporting
24
+ # Used for error reporting and sourcemap building
25
+ # @param offset [Fixnum] The character (not byte) offset in the line on which the SassScript appears.
26
+ # Used for error reporting and sourcemap building
20
27
  # @param options [{Symbol => Object}] An options hash;
21
28
  # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
22
29
  def initialize(str, line, offset, options = {})
@@ -32,9 +39,11 @@ module Sass
32
39
  # @return [Script::Node] The root node of the parse tree
33
40
  # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
34
41
  def parse_interpolated
42
+ start_pos = source_position
35
43
  expr = assert_expr :expr
36
44
  assert_tok :end_interpolation
37
45
  expr.options = @options
46
+ expr.source_range = range(start_pos)
38
47
  expr
39
48
  rescue Sass::SyntaxError => e
40
49
  e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
@@ -46,9 +55,11 @@ module Sass
46
55
  # @return [Script::Node] The root node of the parse tree
47
56
  # @raise [Sass::SyntaxError] if the expression isn't valid SassScript
48
57
  def parse
58
+ start_pos = source_position
49
59
  expr = assert_expr :expr
50
60
  assert_done
51
61
  expr.options = @options
62
+ expr.source_range = range(start_pos)
52
63
  expr
53
64
  rescue Sass::SyntaxError => e
54
65
  e.modify_backtrace :line => @lexer.line, :filename => @options[:filename]
@@ -213,9 +224,10 @@ module Sass
213
224
  return other_interp
214
225
  end
215
226
 
216
- line = @lexer.line
227
+ start_pos = source_position
217
228
  e = Operation.new(e, assert_expr(#{sub.inspect}), tok.type)
218
- e.line = line
229
+ e.line = start_pos.line
230
+ e.source_range = range(start_pos)
219
231
  end
220
232
  e
221
233
  end
@@ -238,12 +250,24 @@ RUBY
238
250
 
239
251
  private
240
252
 
253
+ def source_position
254
+ Sass::Source::Position.new(line, offset)
255
+ end
256
+
257
+ def token_start_position(token)
258
+ Sass::Source::Position.new(token.line, token.offset)
259
+ end
260
+
261
+ def range(start_pos, end_pos=source_position)
262
+ Sass::Source::Range.new(start_pos, end_pos, @options[:filename])
263
+ end
264
+
241
265
  # @private
242
266
  def lexer_class; Lexer; end
243
267
 
244
268
  def expr
245
269
  interp = try_ops_after_interp([:comma], :expr) and return interp
246
- line = @lexer.line
270
+ start_pos = source_position
247
271
  return unless e = interpolation
248
272
  arr = [e]
249
273
  while tok = try_tok(:comma)
@@ -253,7 +277,7 @@ RUBY
253
277
  end
254
278
  arr << assert_expr(:interpolation)
255
279
  end
256
- arr.size == 1 ? arr.first : node(List.new(arr, :comma), line)
280
+ arr.size == 1 ? arr.first : node(List.new(arr, :comma), start_pos)
257
281
  end
258
282
 
259
283
  production :equals, :interpolation, :single_eq
@@ -276,8 +300,10 @@ RUBY
276
300
  wa = @lexer.whitespace?
277
301
  str = Script::String.new(Lexer::OPERATORS_REVERSE[op.type])
278
302
  str.line = @lexer.line
303
+ start_pos = source_position
279
304
  interp = Script::Interpolation.new(prev, str, assert_expr(name), !:wb, wa, :originally_text)
280
305
  interp.line = @lexer.line
306
+ interp.source_range = range(start_pos)
281
307
  return interp
282
308
  end
283
309
 
@@ -295,13 +321,13 @@ RUBY
295
321
  end
296
322
 
297
323
  def space
298
- line = @lexer.line
324
+ start_pos = source_position
299
325
  return unless e = or_expr
300
326
  arr = [e]
301
327
  while e = or_expr
302
328
  arr << e
303
329
  end
304
- arr.size == 1 ? arr.first : node(List.new(arr, :space), line)
330
+ arr.size == 1 ? arr.first : node(List.new(arr, :space), start_pos)
305
331
  end
306
332
 
307
333
  production :or_expr, :and_expr, :or
@@ -322,16 +348,16 @@ RUBY
322
348
 
323
349
  name = @lexer.next
324
350
  if color = Color::COLOR_NAMES[name.value.downcase]
325
- return node(Color.new(color))
351
+ return node(Color.new(color), token_start_position(name), source_position)
326
352
  end
327
- node(Script::String.new(name.value, :identifier))
353
+ node(Script::String.new(name.value, :identifier), token_start_position(name), source_position)
328
354
  end
329
355
 
330
356
  def funcall
331
357
  return raw unless tok = try_tok(:funcall)
332
358
  args, keywords, splat = fn_arglist || [[], {}]
333
359
  assert_tok(:rparen)
334
- node(Script::Funcall.new(tok.value, args, keywords, splat))
360
+ node(Script::Funcall.new(tok.value, args, keywords, splat), token_start_position(tok), source_position)
335
361
  end
336
362
 
337
363
  def defn_arglist!(must_have_parens)
@@ -347,9 +373,10 @@ RUBY
347
373
  must_have_default = false
348
374
  loop do
349
375
  line = @lexer.line
350
- offset = @lexer.offset + 1
376
+ offset = @lexer.offset
351
377
  c = assert_tok(:const)
352
378
  var = Script::Variable.new(c.value)
379
+ var.source_range = range(c.offset)
353
380
  if try_tok(:colon)
354
381
  val = assert_expr(:space)
355
382
  must_have_default = true
@@ -421,27 +448,30 @@ RUBY
421
448
  return variable unless try_tok(:lparen)
422
449
  was_in_parens = @in_parens
423
450
  @in_parens = true
424
- line = @lexer.line
451
+ start_pos = source_position
425
452
  e = expr
426
453
  assert_tok(:rparen)
427
- return e || node(List.new([], :space), line)
454
+ return e || node(List.new([], :space), start_pos)
428
455
  ensure
429
456
  @in_parens = was_in_parens
430
457
  end
431
458
 
432
459
  def variable
460
+ start_pos = source_position
433
461
  return string unless c = try_tok(:const)
434
- node(Variable.new(*c.value))
462
+ node(Variable.new(*c.value), start_pos, source_position)
435
463
  end
436
464
 
437
465
  def string
438
466
  return number unless first = try_tok(:string)
439
467
  return first.value unless try_tok(:begin_interpolation)
468
+ start_pos = source_position
440
469
  line = @lexer.line
441
470
  mid = parse_interpolated
442
471
  last = assert_expr(:string)
443
472
  interp = StringInterpolation.new(first.value, mid, last)
444
473
  interp.line = line
474
+ interp.source_range = range(start_pos)
445
475
  interp
446
476
  end
447
477
 
@@ -486,8 +516,10 @@ RUBY
486
516
  @lexer.expected!(EXPR_NAMES[:default])
487
517
  end
488
518
 
489
- def node(node, line = @lexer.line)
490
- node.line = line
519
+ def node(node, start_pos = source_position, end_pos = nil)
520
+ node.line = start_pos.line
521
+ node.filename = @options[:filename]
522
+ node.source_range = range(start_pos, end_pos) if end_pos
491
523
  node
492
524
  end
493
525
  end
@@ -5,16 +5,24 @@ module Sass
5
5
  # The parser for SCSS.
6
6
  # It parses a string of code into a tree of {Sass::Tree::Node}s.
7
7
  class Parser
8
+
9
+ # Expose for the SASS parser.
10
+ attr_accessor :offset
11
+
8
12
  # @param str [String, StringScanner] The source document to parse.
9
13
  # Note that `Parser` *won't* raise a nice error message if this isn't properly parsed;
10
14
  # for that, you should use the higher-level {Sass::Engine} or {Sass::CSS}.
11
15
  # @param filename [String] The name of the file being parsed. Used for warnings.
12
- # @param line [Fixnum] The line on which the source string appeared,
16
+ # @param line [Fixnum] The 1-based line on which the source string appeared,
13
17
  # if it's part of another document.
14
- def initialize(str, filename, line = 1)
18
+ # @param offset [Fixnum] The 1-based character (not byte) offset in the line on
19
+ # which the source string starts. Used for error reporting and sourcemap
20
+ # building.
21
+ def initialize(str, filename, line = 1, offset = 1)
15
22
  @template = str
16
23
  @filename = filename
17
24
  @line = line
25
+ @offset = offset
18
26
  @strs = []
19
27
  end
20
28
 
@@ -56,6 +64,14 @@ module Sass
56
64
 
57
65
  include Sass::SCSS::RX
58
66
 
67
+ def source_position
68
+ Sass::Source::Position.new(@line, @offset)
69
+ end
70
+
71
+ def range(start_pos, end_pos=source_position)
72
+ Sass::Source::Range.new(start_pos, end_pos, @filename)
73
+ end
74
+
59
75
  def init_scanner!
60
76
  @scanner =
61
77
  if @template.is_a?(StringScanner)
@@ -66,7 +82,7 @@ module Sass
66
82
  end
67
83
 
68
84
  def stylesheet
69
- node = node(Sass::Tree::RootNode.new(@scanner.string))
85
+ node = node(Sass::Tree::RootNode.new(@scanner.string), source_position)
70
86
  block_contents(node, :stylesheet) {s(node)}
71
87
  end
72
88
 
@@ -128,13 +144,14 @@ module Sass
128
144
  PREFIXED_DIRECTIVES = Set[:supports]
129
145
 
130
146
  def directive
147
+ start_pos = source_position
131
148
  return unless tok(/@/)
132
149
  name = tok!(IDENT)
133
150
  ss
134
151
 
135
- if dir = special_directive(name)
152
+ if dir = special_directive(name, start_pos)
136
153
  return dir
137
- elsif dir = prefixed_directive(name)
154
+ elsif dir = prefixed_directive(name, start_pos)
138
155
  return dir
139
156
  end
140
157
 
@@ -143,11 +160,11 @@ module Sass
143
160
  # Some take no arguments at all.
144
161
  val = expr || selector
145
162
  val = val ? ["@#{name} "] + Sass::Util.strip_string_array(val) : ["@#{name}"]
146
- directive_body(val)
163
+ directive_body(val, start_pos)
147
164
  end
148
165
 
149
- def directive_body(value)
150
- node = node(Sass::Tree::DirectiveNode.new(value))
166
+ def directive_body(value, start_pos)
167
+ node = Sass::Tree::DirectiveNode.new(value)
151
168
 
152
169
  if tok(/\{/)
153
170
  node.has_children = true
@@ -155,31 +172,31 @@ module Sass
155
172
  tok!(/\}/)
156
173
  end
157
174
 
158
- node
175
+ node(node, start_pos)
159
176
  end
160
177
 
161
- def special_directive(name)
178
+ def special_directive(name, start_pos)
162
179
  sym = name.gsub('-', '_').to_sym
163
- DIRECTIVES.include?(sym) && send("#{sym}_directive")
180
+ DIRECTIVES.include?(sym) && send("#{sym}_directive", start_pos)
164
181
  end
165
182
 
166
- def prefixed_directive(name)
183
+ def prefixed_directive(name, start_pos)
167
184
  sym = name.gsub(/^-[a-z0-9]+-/i, '').gsub('-', '_').to_sym
168
- PREFIXED_DIRECTIVES.include?(sym) && send("#{sym}_directive", name)
185
+ PREFIXED_DIRECTIVES.include?(sym) && send("#{sym}_directive", name, start_pos)
169
186
  end
170
187
 
171
- def mixin_directive
188
+ def mixin_directive(start_pos)
172
189
  name = tok! IDENT
173
190
  args, splat = sass_script(:parse_mixin_definition_arglist)
174
191
  ss
175
- block(node(Sass::Tree::MixinDefNode.new(name, args, splat)), :directive)
192
+ block(node(Sass::Tree::MixinDefNode.new(name, args, splat), start_pos), :directive)
176
193
  end
177
194
 
178
- def include_directive
195
+ def include_directive(start_pos)
179
196
  name = tok! IDENT
180
197
  args, keywords, splat = sass_script(:parse_mixin_include_arglist)
181
198
  ss
182
- include_node = node(Sass::Tree::MixinNode.new(name, args, keywords, splat))
199
+ include_node = node(Sass::Tree::MixinNode.new(name, args, keywords, splat), start_pos)
183
200
  if tok?(/\{/)
184
201
  include_node.has_children = true
185
202
  block(include_node, :directive)
@@ -188,31 +205,31 @@ module Sass
188
205
  end
189
206
  end
190
207
 
191
- def content_directive
208
+ def content_directive(start_pos)
192
209
  ss
193
- node(Sass::Tree::ContentNode.new)
210
+ node(Sass::Tree::ContentNode.new, start_pos)
194
211
  end
195
212
 
196
- def function_directive
213
+ def function_directive(start_pos)
197
214
  name = tok! IDENT
198
215
  args, splat = sass_script(:parse_function_definition_arglist)
199
216
  ss
200
- block(node(Sass::Tree::FunctionNode.new(name, args, splat)), :function)
217
+ block(node(Sass::Tree::FunctionNode.new(name, args, splat), start_pos), :function)
201
218
  end
202
219
 
203
- def return_directive
204
- node(Sass::Tree::ReturnNode.new(sass_script(:parse)))
220
+ def return_directive(start_pos)
221
+ node(Sass::Tree::ReturnNode.new(sass_script(:parse)), start_pos)
205
222
  end
206
223
 
207
- def debug_directive
208
- node(Sass::Tree::DebugNode.new(sass_script(:parse)))
224
+ def debug_directive(start_pos)
225
+ node(Sass::Tree::DebugNode.new(sass_script(:parse)), start_pos)
209
226
  end
210
227
 
211
- def warn_directive
212
- node(Sass::Tree::WarnNode.new(sass_script(:parse)))
228
+ def warn_directive(start_pos)
229
+ node(Sass::Tree::WarnNode.new(sass_script(:parse)), start_pos)
213
230
  end
214
231
 
215
- def for_directive
232
+ def for_directive(start_pos)
216
233
  tok!(/\$/)
217
234
  var = tok! IDENT
218
235
  ss
@@ -226,10 +243,10 @@ module Sass
226
243
  to = sass_script(:parse)
227
244
  ss
228
245
 
229
- block(node(Sass::Tree::ForNode.new(var, from, to, exclusive)), :directive)
246
+ block(node(Sass::Tree::ForNode.new(var, from, to, exclusive), start_pos), :directive)
230
247
  end
231
248
 
232
- def each_directive
249
+ def each_directive(start_pos)
233
250
  tok!(/\$/)
234
251
  var = tok! IDENT
235
252
  ss
@@ -238,19 +255,19 @@ module Sass
238
255
  list = sass_script(:parse)
239
256
  ss
240
257
 
241
- block(node(Sass::Tree::EachNode.new(var, list)), :directive)
258
+ block(node(Sass::Tree::EachNode.new(var, list), start_pos), :directive)
242
259
  end
243
260
 
244
- def while_directive
261
+ def while_directive(start_pos)
245
262
  expr = sass_script(:parse)
246
263
  ss
247
- block(node(Sass::Tree::WhileNode.new(expr)), :directive)
264
+ block(node(Sass::Tree::WhileNode.new(expr), start_pos), :directive)
248
265
  end
249
266
 
250
- def if_directive
267
+ def if_directive(start_pos)
251
268
  expr = sass_script(:parse)
252
269
  ss
253
- node = block(node(Sass::Tree::IfNode.new(expr)), :directive)
270
+ node = block(node(Sass::Tree::IfNode.new(expr), start_pos), :directive)
254
271
  pos = @scanner.pos
255
272
  line = @line
256
273
  ss
@@ -265,10 +282,11 @@ module Sass
265
282
  end
266
283
 
267
284
  def else_block(node)
285
+ start_pos = source_position
268
286
  return unless tok(/@else/)
269
287
  ss
270
288
  else_node = block(
271
- Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))),
289
+ node(Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))), start_pos),
272
290
  :directive)
273
291
  node.add_else(else_node)
274
292
  pos = @scanner.pos
@@ -284,18 +302,18 @@ module Sass
284
302
  end
285
303
  end
286
304
 
287
- def else_directive
305
+ def else_directive(start_pos)
288
306
  err("Invalid CSS: @else must come after @if")
289
307
  end
290
308
 
291
- def extend_directive
309
+ def extend_directive(start_pos)
292
310
  selector = expr!(:selector_sequence)
293
311
  optional = tok(OPTIONAL)
294
312
  ss
295
- node(Sass::Tree::ExtendNode.new(selector, !!optional))
313
+ node(Sass::Tree::ExtendNode.new(selector, !!optional), start_pos)
296
314
  end
297
315
 
298
- def import_directive
316
+ def import_directive(start_pos)
299
317
  values = []
300
318
 
301
319
  loop do
@@ -307,13 +325,13 @@ module Sass
307
325
  end
308
326
 
309
327
  def import_arg
310
- line = @line
328
+ start_pos = source_position
311
329
  return unless (str = tok(STRING)) || (uri = tok?(/url\(/i))
312
330
  if uri
313
331
  str = sass_script(:parse_string)
314
332
  media = media_query_list
315
333
  ss
316
- return node(Tree::CssImportNode.new(str, media.to_a))
334
+ return node(Tree::CssImportNode.new(str, media.to_a), start_pos)
317
335
  end
318
336
 
319
337
  path = @scanner[1] || @scanner[2]
@@ -321,18 +339,16 @@ module Sass
321
339
 
322
340
  media = media_query_list
323
341
  if path =~ /^(https?:)?\/\// || media || use_css_import?
324
- node = Sass::Tree::CssImportNode.new(str, media.to_a)
325
- else
326
- node = Sass::Tree::ImportNode.new(path.strip)
342
+ return node(Sass::Tree::CssImportNode.new(str, media.to_a), start_pos)
327
343
  end
328
- node.line = line
329
- node
344
+
345
+ node(Sass::Tree::ImportNode.new(path.strip), start_pos)
330
346
  end
331
347
 
332
348
  def use_css_import?; false; end
333
349
 
334
- def media_directive
335
- block(node(Sass::Tree::MediaNode.new(expr!(:media_query_list).to_a)), :directive)
350
+ def media_directive(start_pos)
351
+ block(node(Sass::Tree::MediaNode.new(expr!(:media_query_list).to_a), start_pos), :directive)
336
352
  end
337
353
 
338
354
  # http://www.w3.org/TR/css3-mediaqueries/#syntax
@@ -400,11 +416,11 @@ module Sass
400
416
  res
401
417
  end
402
418
 
403
- def charset_directive
419
+ def charset_directive(start_pos)
404
420
  tok! STRING
405
421
  name = @scanner[1] || @scanner[2]
406
422
  ss
407
- node(Sass::Tree::CharsetNode.new(name))
423
+ node(Sass::Tree::CharsetNode.new(name), start_pos)
408
424
  end
409
425
 
410
426
  # The document directive is specified in
@@ -415,14 +431,14 @@ module Sass
415
431
  # We could parse all document directives according to Mozilla's syntax,
416
432
  # but if someone's using e.g. @-webkit-document we don't want them to
417
433
  # think WebKit works sans quotes.
418
- def _moz_document_directive
434
+ def _moz_document_directive(start_pos)
419
435
  res = ["@-moz-document "]
420
436
  loop do
421
437
  res << str{ss} << expr!(:moz_document_function)
422
438
  break unless c = tok(/,/)
423
439
  res << c
424
440
  end
425
- directive_body(res.flatten)
441
+ directive_body(res.flatten, start_pos)
426
442
  end
427
443
 
428
444
  def moz_document_function
@@ -433,16 +449,16 @@ module Sass
433
449
  end
434
450
 
435
451
  # http://www.w3.org/TR/css3-conditional/
436
- def supports_directive(name)
452
+ def supports_directive(name, start_pos)
437
453
  condition = expr!(:supports_condition)
438
- node = node(Sass::Tree::SupportsNode.new(name, condition))
454
+ node = Sass::Tree::SupportsNode.new(name, condition)
439
455
 
440
456
  tok!(/\{/)
441
457
  node.has_children = true
442
458
  block_contents(node, :directive)
443
459
  tok!(/\}/)
444
460
 
445
- node
461
+ node(node, start_pos)
446
462
  end
447
463
 
448
464
  def supports_condition
@@ -494,12 +510,14 @@ module Sass
494
510
 
495
511
  def variable
496
512
  return unless tok(/\$/)
513
+ start_pos = source_position
497
514
  name = tok!(IDENT)
498
515
  ss; tok!(/:/); ss
499
516
 
500
517
  expr = sass_script(:parse)
501
518
  guarded = tok(DEFAULT)
502
- node(Sass::Tree::VariableNode.new(name, expr, guarded))
519
+ result = Sass::Tree::VariableNode.new(name, expr, guarded)
520
+ node(result, start_pos)
503
521
  end
504
522
 
505
523
  def operator
@@ -511,8 +529,9 @@ module Sass
511
529
  end
512
530
 
513
531
  def ruleset
532
+ start_pos = source_position
514
533
  return unless rules = selector_sequence
515
- block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :ruleset)
534
+ block(node(Sass::Tree::RuleNode.new(rules.flatten.compact), start_pos), :ruleset)
516
535
  end
517
536
 
518
537
  def block(node, context)
@@ -520,6 +539,7 @@ module Sass
520
539
  tok!(/\{/)
521
540
  block_contents(node, context)
522
541
  tok!(/\}/)
542
+ node.source_range.end_pos = source_position if node.source_range
523
543
  node
524
544
  end
525
545
 
@@ -829,6 +849,7 @@ module Sass
829
849
 
830
850
  def declaration
831
851
  # This allows the "*prop: val", ":prop: val", and ".prop: val" hacks
852
+ name_start_pos = source_position
832
853
  if s = tok(/[:\*\.]|\#(?!\{)/)
833
854
  @use_property_exception = s !~ /[\.\#]/
834
855
  name = [s, str{ss}, *expr!(:interp_ident)]
@@ -839,14 +860,18 @@ module Sass
839
860
  if comment = tok(COMMENT)
840
861
  name << comment
841
862
  end
863
+ name_end_pos = source_position
842
864
  ss
843
865
 
844
866
  tok!(/:/)
845
- space, value = value!
867
+ value_start_pos, space, value = value!
868
+ value_end_pos = source_position
846
869
  ss
847
870
  require_block = tok?(/\{/)
848
871
 
849
- node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new))
872
+ node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new), name_start_pos, value_end_pos)
873
+ node.name_source_range = range(name_start_pos, name_end_pos)
874
+ node.value_source_range = range(value_start_pos, value_end_pos)
850
875
 
851
876
  return node unless require_block
852
877
  nested_properties! node, space
@@ -854,18 +879,19 @@ module Sass
854
879
 
855
880
  def value!
856
881
  space = !str {ss}.empty?
882
+ value_start_pos = source_position
857
883
  @use_property_exception ||= space || !tok?(IDENT)
858
884
 
859
- return true, Sass::Script::String.new("") if tok?(/\{/)
885
+ return value_start_pos, true, Sass::Script::String.new("") if tok?(/\{/)
860
886
  # This is a bit of a dirty trick:
861
887
  # if the value is completely static,
862
888
  # we don't parse it at all, and instead return a plain old string
863
889
  # containing the value.
864
890
  # This results in a dramatic speed increase.
865
891
  if val = tok(STATIC_VALUE, true)
866
- return space, Sass::Script::String.new(val.strip)
892
+ return value_start_pos, space, Sass::Script::String.new(val.strip)
867
893
  end
868
- return space, sass_script(:parse)
894
+ return value_start_pos, space, sass_script(:parse)
869
895
  end
870
896
 
871
897
  def nested_properties!(node, space)
@@ -982,18 +1008,21 @@ MESSAGE
982
1008
  def str?
983
1009
  pos = @scanner.pos
984
1010
  line = @line
1011
+ offset = @offset
985
1012
  @strs.push ""
986
1013
  throw_error {yield} && @strs.last
987
1014
  rescue Sass::SyntaxError => e
988
1015
  @scanner.pos = pos
989
1016
  @line = line
1017
+ @offset = offset
990
1018
  nil
991
1019
  ensure
992
1020
  @strs.pop
993
1021
  end
994
1022
 
995
- def node(node)
996
- node.line = @line
1023
+ def node(node, start_pos, end_pos = source_position)
1024
+ node.line = start_pos.line
1025
+ node.source_range = range(start_pos, end_pos)
997
1026
  node
998
1027
  end
999
1028
 
@@ -1003,8 +1032,7 @@ MESSAGE
1003
1032
  def self.sass_script_parser; @sass_script_parser; end
1004
1033
 
1005
1034
  def sass_script(*args)
1006
- parser = self.class.sass_script_parser.new(@scanner, @line,
1007
- @scanner.pos - (@scanner.string[0...@scanner.pos].rindex("\n") || 0))
1035
+ parser = self.class.sass_script_parser.new(@scanner, @line, @offset, :filename => @filename)
1008
1036
  result = parser.send(*args)
1009
1037
  unless @strs.empty?
1010
1038
  # Convert to CSS manually so that comments are ignored.
@@ -1012,6 +1040,7 @@ MESSAGE
1012
1040
  @strs.each {|s| s << src}
1013
1041
  end
1014
1042
  @line = parser.line
1043
+ @offset = parser.offset
1015
1044
  result
1016
1045
  rescue Sass::SyntaxError => e
1017
1046
  throw(:_sass_parser_error, true) if @throw_error
@@ -1086,10 +1115,12 @@ MESSAGE
1086
1115
  old_throw_error, @throw_error = @throw_error, true
1087
1116
  pos = @scanner.pos
1088
1117
  line = @line
1118
+ offset = @offset
1089
1119
  expected = @expected
1090
1120
  if catch(:_sass_parser_error) {yield; false}
1091
1121
  @scanner.pos = pos
1092
1122
  @line = line
1123
+ @offset = offset
1093
1124
  @expected = expected
1094
1125
  {:pos => pos, :line => line, :expected => @expected, :block => block}
1095
1126
  end
@@ -1152,7 +1183,15 @@ MESSAGE
1152
1183
  @scanner.pos -= @scanner[-1].length
1153
1184
  res.slice!(-@scanner[-1].length..-1)
1154
1185
  end
1155
- @line += res.count(NEWLINE)
1186
+
1187
+ newline_count = res.count(NEWLINE)
1188
+ if newline_count > 0
1189
+ @line += newline_count
1190
+ @offset = res[res.rindex(NEWLINE)..-1].size
1191
+ else
1192
+ @offset += res.size
1193
+ end
1194
+
1156
1195
  @expected = nil
1157
1196
  if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT
1158
1197
  @strs.each {|s| s << res}