crass 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,125 @@
1
+ module Crass
2
+
3
+ # Similar to a StringScanner, but with extra functionality needed to tokenize
4
+ # CSS while preserving the original text.
5
+ class Scanner
6
+ # Current character, or `nil` if the scanner hasn't yet consumed a
7
+ # character, or is at the end of the string.
8
+ attr_reader :current
9
+
10
+ # Current marker position. Use {#marked} to get the substring between
11
+ # {#marker} and {#pos}.
12
+ attr_accessor :marker
13
+
14
+ # Position of the next character that will be consumed. This is a character
15
+ # position, not a byte position, so it accounts for multi-byte characters.
16
+ attr_accessor :pos
17
+
18
+ # The string being scanned.
19
+ attr_reader :string
20
+
21
+ # Creates a Scanner instance for the given _input_ string or IO instance.
22
+ def initialize(input)
23
+ @string = input.is_a?(IO) ? input.read : input.to_s
24
+ @chars = @string.chars.to_a
25
+
26
+ reset
27
+ end
28
+
29
+ # Consumes the next character and returns it, advancing the pointer, or
30
+ # an empty string if the end of the string has been reached.
31
+ def consume
32
+ @current = @chars[@pos] || ''
33
+ @pos += 1 if @current
34
+ @current
35
+ end
36
+
37
+ # Consumes the rest of the string and returns it, advancing the pointer to
38
+ # the end of the string. Returns an empty string is the end of the string
39
+ # has already been reached.
40
+ def consume_rest
41
+ rest = @string[@pos..@len] || ''
42
+ @current = rest[-1] || ''
43
+ @pos = @len
44
+
45
+ rest
46
+ end
47
+
48
+ # Returns `true` if the end of the string has been reached, `false`
49
+ # otherwise.
50
+ def eos?
51
+ @pos == @len
52
+ end
53
+
54
+ # Sets the marker to the position of the next character that will be
55
+ # consumed.
56
+ def mark
57
+ @marker = @pos
58
+ end
59
+
60
+ # Returns the substring between {#marker} and {#pos}, without altering the
61
+ # pointer.
62
+ def marked
63
+ if result = @chars[@marker...@pos]
64
+ result.join('')
65
+ else
66
+ ''
67
+ end
68
+ end
69
+
70
+ # Returns up to _length_ characters starting at the current position, but
71
+ # doesn't consume them. The number of characters returned may be less than
72
+ # _length_ if the end of the string is reached.
73
+ def peek(length = 1)
74
+ if result = @chars[@pos, length]
75
+ result.join('')
76
+ else
77
+ ''
78
+ end
79
+ end
80
+
81
+ # Moves the pointer back one character without changing the value of
82
+ # {#current}. The next call to {#consume} will re-consume the current
83
+ # character.
84
+ def reconsume
85
+ @pos -= 1 if @pos > 0
86
+ end
87
+
88
+ # Resets the pointer to the beginning of the string.
89
+ def reset
90
+ @current = nil
91
+ @len = @string.length
92
+ @marker = 0
93
+ @pos = 0
94
+ end
95
+
96
+ # Tries to match _pattern_ at the current position. If it matches, the
97
+ # matched substring will be returned and the pointer will be advanced.
98
+ # Otherwise, `nil` will be returned.
99
+ def scan(pattern)
100
+ match = pattern.match(@string, @pos)
101
+ return nil if match.nil? || match.begin(0) != @pos
102
+
103
+ @pos = match.end(0)
104
+ @current = @chars[@pos - 1]
105
+
106
+ match[0]
107
+ end
108
+
109
+ # Scans the string until the _pattern_ is matched. Returns the substring up
110
+ # to and including the end of the match, and advances the pointer. If there
111
+ # is no match, `nil` is returned and the pointer is not advanced.
112
+ def scan_until(pattern)
113
+ start = @pos
114
+ match = pattern.match(@string, @pos)
115
+
116
+ return nil if match.nil?
117
+
118
+ @pos = match.end(0)
119
+ @current = @chars[@pos - 1]
120
+
121
+ @string[start...@pos]
122
+ end
123
+ end
124
+
125
+ end
@@ -0,0 +1,41 @@
1
+ module Crass
2
+
3
+ # Like {Scanner}, but for tokens!
4
+ class TokenScanner
5
+ attr_reader :current, :pos, :tokens
6
+
7
+ def initialize(tokens)
8
+ @tokens = tokens.to_a
9
+ reset
10
+ end
11
+
12
+ # Executes the given block, collects all tokens that are consumed during its
13
+ # execution, and returns them.
14
+ def collect
15
+ start = @pos
16
+ yield
17
+ @tokens[start...@pos] || []
18
+ end
19
+
20
+ # Consumes the next token and returns it, advancing the pointer.
21
+ def consume
22
+ @current = @tokens[@pos]
23
+ @pos += 1 if @current
24
+ @current
25
+ end
26
+
27
+ # Reconsumes the current token, moving the pointer back one position.
28
+ #
29
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#reconsume-the-current-input-token
30
+ def reconsume
31
+ @pos -= 1 if @pos > 0
32
+ end
33
+
34
+ # Resets the pointer to the first token in the list.
35
+ def reset
36
+ @current = nil
37
+ @pos = 0
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,668 @@
1
+ # encoding: utf-8
2
+ require_relative 'scanner'
3
+
4
+ module Crass
5
+
6
+ # Tokenizes a CSS string.
7
+ #
8
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#tokenization
9
+ class Tokenizer
10
+ RE_COMMENT_CLOSE = /\*\//
11
+ RE_DIGIT = /[0-9]+/
12
+ RE_ESCAPE = /\\[^\n]/
13
+ RE_HEX = /[0-9A-Fa-f]{1,6}/
14
+ RE_NAME = /[0-9A-Za-z_\u0080-\u{10ffff}-]+/
15
+ RE_NAME_START = /[A-Za-z_\u0080-\u{10ffff}]+/
16
+ RE_NON_PRINTABLE = /[\u0000-\u0008\u000b\u000e-\u001f\u007f]+/
17
+ RE_NUMBER_DECIMAL = /\.[0-9]+/
18
+ RE_NUMBER_EXPONENT = /[Ee][+-]?[0-9]+/
19
+ RE_NUMBER_SIGN = /[+-]/
20
+
21
+ RE_NUMBER_STR = /\A
22
+ (?<sign> [+-]?)
23
+ (?<integer> [0-9]*)
24
+ (?:\.
25
+ (?<fractional> [0-9]*)
26
+ )?
27
+ (?:[Ee]
28
+ (?<exponent_sign> [+-]?)
29
+ (?<exponent> [0-9]*)
30
+ )?
31
+ \z/x
32
+
33
+ RE_UNICODE_RANGE_START = /\+(?:[0-9A-Fa-f]|\?)/
34
+ RE_UNICODE_RANGE_END = /-[0-9A-Fa-f]/
35
+ RE_URL_QUOTE = /["']/
36
+ RE_WHITESPACE = /[\n\u0009\u0020]+/
37
+
38
+ # -- Class Methods ---------------------------------------------------------
39
+
40
+ # Tokenizes the given _input_ as a CSS string and returns an array of
41
+ # tokens.
42
+ #
43
+ # See {#initialize} for _options_.
44
+ def self.tokenize(input, options = {})
45
+ Tokenizer.new(input, options).tokenize
46
+ end
47
+
48
+ # -- Instance Methods ------------------------------------------------------
49
+
50
+ # Initializes a new Tokenizer.
51
+ #
52
+ # Options:
53
+ #
54
+ # * **:preserve_comments** - If `true`, comments will be preserved as
55
+ # `:comment` tokens.
56
+ #
57
+ # * **:preserve_hacks** - If `true`, certain non-standard browser hacks
58
+ # such as the IE "*" hack will be preserved even though they violate
59
+ # CSS 3 syntax rules.
60
+ #
61
+ def initialize(input, options = {})
62
+ @s = Scanner.new(preprocess(input))
63
+ @options = options
64
+ end
65
+
66
+ # Consumes a token and returns the token that was consumed.
67
+ #
68
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-token0
69
+ def consume
70
+ return token(:eof) if @s.eos?
71
+
72
+ @s.mark
73
+ return token(:whitespace) if @s.scan(RE_WHITESPACE)
74
+
75
+ case char = @s.consume
76
+ when '"'
77
+ consume_string('"')
78
+
79
+ when '#'
80
+ if @s.peek =~ RE_NAME || valid_escape?
81
+ value = consume_name
82
+
83
+ token(:hash,
84
+ :type => start_identifier? ? :id : :unrestricted,
85
+ :value => value)
86
+ else
87
+ token(:delim, :value => char)
88
+ end
89
+
90
+ when '$'
91
+ if @s.peek == '='
92
+ @s.consume
93
+ token(:suffix_match)
94
+ else
95
+ token(:delim, :value => char)
96
+ end
97
+
98
+ when "'"
99
+ consume_string("'")
100
+
101
+ when '('
102
+ token(:'(')
103
+
104
+ when ')'
105
+ token(:')')
106
+
107
+ when '*'
108
+ if @s.peek == '='
109
+ @s.consume
110
+ token(:substring_match)
111
+
112
+ elsif @options[:preserve_hacks] && @s.peek =~ RE_NAME_START
113
+ # NON-STANDARD: IE * hack
114
+ @s.reconsume
115
+ consume_ident
116
+
117
+ else
118
+ token(:delim, :value => char)
119
+ end
120
+
121
+ when '+'
122
+ if start_number?
123
+ @s.reconsume
124
+ consume_numeric
125
+ else
126
+ token(:delim, :value => char)
127
+ end
128
+
129
+ when ','
130
+ token(:comma)
131
+
132
+ when '-'
133
+ if start_number?
134
+ @s.reconsume
135
+ consume_numeric
136
+ elsif start_identifier?
137
+ @s.reconsume
138
+ consume_ident
139
+ elsif @s.peek(2) == '->'
140
+ @s.consume
141
+ @s.consume
142
+ token(:cdc)
143
+ else
144
+ token(:delim, :value => char)
145
+ end
146
+
147
+ when '.'
148
+ if start_number?
149
+ @s.reconsume
150
+ consume_numeric
151
+ else
152
+ token(:delim, :value => char)
153
+ end
154
+
155
+ when '/'
156
+ if @s.peek == '*'
157
+ @s.consume
158
+
159
+ if text = @s.scan_until(RE_COMMENT_CLOSE)
160
+ text.slice!(-2, 2)
161
+ else
162
+ text = @s.rest
163
+ end
164
+
165
+ if @options[:preserve_comments]
166
+ token(:comment, :value => text)
167
+ else
168
+ consume
169
+ end
170
+ else
171
+ token(:delim, :value => char)
172
+ end
173
+
174
+ when ':'
175
+ token(:colon)
176
+
177
+ when ';'
178
+ token(:semicolon)
179
+
180
+ when '<'
181
+ if @s.peek(3) == '!--'
182
+ @s.consume
183
+ @s.consume
184
+ @s.consume
185
+
186
+ token(:cdo)
187
+ else
188
+ token(:delim, :value => char)
189
+ end
190
+
191
+ when '@'
192
+ if start_identifier?
193
+ token(:at_keyword, :value => consume_name)
194
+ else
195
+ token(:delim, :value => char)
196
+ end
197
+
198
+ when '['
199
+ token(:'[')
200
+
201
+ when '\\'
202
+ if valid_escape?(char + @s.peek)
203
+ @s.reconsume
204
+ consume_ident
205
+ else
206
+ token(:delim,
207
+ :error => true,
208
+ :value => char)
209
+ end
210
+
211
+ when ']'
212
+ token(:']')
213
+
214
+ when '^'
215
+ if @s.peek == '='
216
+ @s.consume
217
+ token(:prefix_match)
218
+ else
219
+ token(:delim, :value => char)
220
+ end
221
+
222
+ when '{'
223
+ token(:'{')
224
+
225
+ when '}'
226
+ token(:'}')
227
+
228
+ when RE_DIGIT
229
+ @s.reconsume
230
+ consume_numeric
231
+
232
+ when 'U', 'u'
233
+ if @s.peek(2) =~ RE_UNICODE_RANGE_START
234
+ @s.consume
235
+ consume_unicode_range
236
+ else
237
+ @s.reconsume
238
+ consume_ident
239
+ end
240
+
241
+ when RE_NAME_START
242
+ @s.reconsume
243
+ consume_ident
244
+
245
+ when '|'
246
+ case @s.peek
247
+ when '='
248
+ @s.consume
249
+ token(:dash_match)
250
+
251
+ when '|'
252
+ @s.consume
253
+ token(:column)
254
+
255
+ else
256
+ token(:delim, :value => char)
257
+ end
258
+
259
+ when '~'
260
+ if @s.peek == '='
261
+ @s.consume
262
+ token(:include_match)
263
+ else
264
+ token(:delim, :value => char)
265
+ end
266
+
267
+ else
268
+ token(:delim, :value => char)
269
+ end
270
+ end
271
+
272
+ # Consumes the remnants of a bad URL and returns the consumed text.
273
+ #
274
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-the-remnants-of-a-bad-url0
275
+ def consume_bad_url
276
+ text = ''
277
+
278
+ while true
279
+ return text if @s.eos?
280
+
281
+ if valid_escape?
282
+ text << consume_escaped
283
+ else
284
+ char = @s.consume
285
+
286
+ if char == ')'
287
+ return text
288
+ else
289
+ text << char
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ # Consumes an escaped code point and returns its unescaped value.
296
+ #
297
+ # This method assumes that the `\` has already been consumed, and that the
298
+ # next character in the input has already been verified not to be a newline
299
+ # or EOF.
300
+ #
301
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-an-escaped-code-point0
302
+ def consume_escaped
303
+ case
304
+ when @s.eos?
305
+ "\ufffd"
306
+
307
+ when hex_str = @s.scan(RE_HEX)
308
+ @s.consume if @s.peek =~ RE_WHITESPACE
309
+
310
+ codepoint = hex_str.hex
311
+
312
+ if codepoint == 0 ||
313
+ codepoint.between?(0xD800, 0xDFFF) ||
314
+ codepoint > 0x10FFFF
315
+
316
+ "\ufffd"
317
+ else
318
+ codepoint.chr(Encoding::UTF_8)
319
+ end
320
+
321
+ else
322
+ @s.consume
323
+ end
324
+ end
325
+
326
+ # Consumes an ident-like token and returns it.
327
+ #
328
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-an-ident-like-token0
329
+ def consume_ident
330
+ value = consume_name
331
+
332
+ if value.downcase == 'url' && @s.peek == '('
333
+ @s.consume
334
+ consume_url
335
+ elsif @s.peek == '('
336
+ @s.consume
337
+ token(:function, :value => value)
338
+ else
339
+ token(:ident, :value => value)
340
+ end
341
+ end
342
+
343
+ # Consumes a name and returns it.
344
+ #
345
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-name0
346
+ def consume_name
347
+ result = ''
348
+
349
+ while char = @s.peek
350
+ if char =~ RE_NAME
351
+ result << @s.consume
352
+
353
+ elsif char == '\\' && valid_escape?
354
+ result << @s.consume
355
+ result << consume_escaped
356
+
357
+ # NON-STANDARD: IE * hack
358
+ elsif @options[:preserve_hacks] && char == '*'
359
+ result << @s.consume
360
+
361
+ else
362
+ return result
363
+ end
364
+ end
365
+ end
366
+
367
+ # Consumes a number and returns a 3-element array containing the number's
368
+ # original representation, its numeric value, and its type (either
369
+ # `:integer` or `:number`).
370
+ #
371
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-number0
372
+ def consume_number
373
+ repr = ''
374
+ type = :integer
375
+
376
+ repr << @s.consume if @s.peek =~ RE_NUMBER_SIGN
377
+ repr << (@s.scan(RE_DIGIT) || '')
378
+
379
+ if match = @s.scan(RE_NUMBER_DECIMAL)
380
+ repr << match
381
+ type = :number
382
+ end
383
+
384
+ if match = @s.scan(RE_NUMBER_EXPONENT)
385
+ repr << match
386
+ type = :number
387
+ end
388
+
389
+ [repr, convert_string_to_number(repr), type]
390
+ end
391
+
392
+ # Consumes a numeric token and returns it.
393
+ #
394
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-numeric-token0
395
+ def consume_numeric
396
+ number = consume_number
397
+
398
+ if start_identifier?
399
+ token(:dimension,
400
+ :repr => number[0],
401
+ :type => number[2],
402
+ :unit => consume_name,
403
+ :value => number[1])
404
+
405
+ elsif @s.peek == '%'
406
+ @s.consume
407
+
408
+ token(:percentage,
409
+ :repr => number[0],
410
+ :value => number[1])
411
+
412
+ else
413
+ token(:number,
414
+ :repr => number[0],
415
+ :type => number[2],
416
+ :value => number[1])
417
+ end
418
+ end
419
+
420
+ # Consumes a string token that ends at the given character, and returns the
421
+ # token.
422
+ #
423
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-string-token0
424
+ def consume_string(ending)
425
+ value = ''
426
+
427
+ while char = @s.consume
428
+ case char
429
+ when ending then break
430
+
431
+ when "\n"
432
+ return token(:bad_string,
433
+ :error => true,
434
+ :value => value)
435
+
436
+ when '\\'
437
+ case @s.peek
438
+ when ''
439
+ # End of the input, so do nothing.
440
+ next
441
+
442
+ when "\n"
443
+ @s.consume
444
+
445
+ else
446
+ value += consume_escaped
447
+ end
448
+
449
+ else
450
+ value << char
451
+ end
452
+ end
453
+
454
+ token(:string, :value => value)
455
+ end
456
+
457
+ # Consumes a Unicode range token and returns it. Assumes the initial "u+" or
458
+ # "U+" has already been consumed.
459
+ #
460
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-unicode-range-token0
461
+ def consume_unicode_range
462
+ value = @s.scan(RE_HEX)
463
+
464
+ while value.length < 6
465
+ break unless @s.peek == '?'
466
+ value << @s.consume
467
+ end
468
+
469
+ range = {}
470
+
471
+ if value.include?('?')
472
+ range[:start] = value.gsub('?', '0').hex
473
+ range[:end] = value.gsub('?', 'F').hex
474
+ return token(:unicode_range, range)
475
+ end
476
+
477
+ range[:start] = value.hex
478
+
479
+ if @s.peek(2) =~ RE_UNICODE_RANGE_END
480
+ range[:value] << @s.consume << end_value = @s.scan(RE_HEX)
481
+ range[:end] = end_value.hex
482
+ else
483
+ range[:end] = range[:start]
484
+ end
485
+
486
+ token(:unicode_range, range)
487
+ end
488
+
489
+ # Consumes a URL token and returns it. Assumes the original "url(" has
490
+ # already been consumed.
491
+ #
492
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#consume-a-url-token0
493
+ def consume_url
494
+ value = ''
495
+
496
+ @s.scan(RE_WHITESPACE)
497
+ return token(:url, :value => value) if @s.eos?
498
+
499
+ # Quoted URL.
500
+ if @s.peek =~ RE_URL_QUOTE
501
+ string = consume_string(@s.consume)
502
+
503
+ if string[:node] == :bad_string
504
+ return token(:bad_url, :value => string[:value] + consume_bad_url)
505
+ end
506
+
507
+ value = string[:value]
508
+ @s.scan(RE_WHITESPACE)
509
+
510
+ if @s.eos? || @s.peek == ')'
511
+ @s.consume
512
+ return token(:url, :value => value)
513
+ else
514
+ return token(:bad_url, :value => value + consume_bad_url)
515
+ end
516
+ end
517
+
518
+ # Unquoted URL.
519
+ while !@s.eos?
520
+ case char = @s.consume
521
+ when ')' then break
522
+
523
+ when RE_WHITESPACE
524
+ @s.scan(RE_WHITESPACE)
525
+
526
+ if @s.eos? || @s.peek == ')'
527
+ @s.consume
528
+ break
529
+ else
530
+ return token(:bad_url, :value => value + consume_bad_url)
531
+ end
532
+
533
+ when '"', "'", '(', RE_NON_PRINTABLE
534
+ return token(:bad_url,
535
+ :error => true,
536
+ :value => value + consume_bad_url)
537
+
538
+ when '\\'
539
+ if valid_escape?
540
+ value << consume_escaped
541
+ else
542
+ return token(:bad_url,
543
+ :error => true,
544
+ :value => value + consume_bad_url
545
+ )
546
+ end
547
+
548
+ else
549
+ value << char
550
+ end
551
+ end
552
+
553
+ token(:url, :value => value)
554
+ end
555
+
556
+ # Converts a valid CSS number string into a number and returns the number.
557
+ #
558
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#convert-a-string-to-a-number0
559
+ def convert_string_to_number(str)
560
+ matches = RE_NUMBER_STR.match(str)
561
+
562
+ s = matches[:sign] == '-' ? -1 : 1
563
+ i = matches[:integer].to_i
564
+ f = matches[:fractional].to_i
565
+ d = matches[:fractional] ? matches[:fractional].length : 0
566
+ t = matches[:exponent_sign] == '-' ? -1 : 1
567
+ e = matches[:exponent].to_i
568
+
569
+ # I know this looks nutty, but it's exactly what's defined in the spec,
570
+ # and it works.
571
+ s * (i + f * 10**-d) * 10**(t * e)
572
+ end
573
+
574
+ # Preprocesses _input_ to prepare it for the tokenizer.
575
+ #
576
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#input-preprocessing
577
+ def preprocess(input)
578
+ input = input.to_s.encode('UTF-8',
579
+ :invalid => :replace,
580
+ :undef => :replace)
581
+
582
+ input.gsub!(/(?:\r\n|[\r\f])/, "\n")
583
+ input.gsub!("\u0000", "\ufffd")
584
+ input
585
+ end
586
+
587
+ # Returns `true` if the given three-character _text_ would start an
588
+ # identifier. If _text_ is `nil`, the next three characters in the input
589
+ # stream will be checked, but will not be consumed.
590
+ #
591
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#check-if-three-code-points-would-start-an-identifier
592
+ def start_identifier?(text = nil)
593
+ text = @s.peek(3) if text.nil?
594
+
595
+ case text[0]
596
+ when '-'
597
+ !!(text[1] =~ RE_NAME_START || valid_escape?(text[1, 2]))
598
+
599
+ when RE_NAME_START
600
+ true
601
+
602
+ when '\\'
603
+ valid_escape?(text[0, 2])
604
+
605
+ else
606
+ false
607
+ end
608
+ end
609
+
610
+ # Returns `true` if the given three-character _text_ would start a number.
611
+ # If _text_ is `nil`, the next three characters in the input stream will be
612
+ # checked, but will not be consumed.
613
+ #
614
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#check-if-three-code-points-would-start-a-number
615
+ def start_number?(text = nil)
616
+ text = @s.peek(3) if text.nil?
617
+
618
+ case text[0]
619
+ when '+', '-'
620
+ !!(text[1] =~ RE_DIGIT || (text[1] == '.' && text[2] =~ RE_DIGIT))
621
+
622
+ when '.'
623
+ !!(text[1] =~ RE_DIGIT)
624
+
625
+ when RE_DIGIT
626
+ true
627
+
628
+ else
629
+ false
630
+ end
631
+ end
632
+
633
+ # Creates and returns a new token with the given _properties_.
634
+ def token(type, properties = {})
635
+ {
636
+ :node => type,
637
+ :pos => @s.marker,
638
+ :raw => @s.marked
639
+ }.merge!(properties)
640
+ end
641
+
642
+ # Tokenizes the input stream and returns an array of tokens.
643
+ def tokenize
644
+ @s.reset
645
+
646
+ tokens = []
647
+ token = consume
648
+
649
+ while token && token[:node] != :eof
650
+ tokens << token
651
+ token = consume
652
+ end
653
+
654
+ tokens
655
+ end
656
+
657
+ # Returns `true` if the given two-character _text_ is the beginning of a
658
+ # valid escape sequence. If _text_ is `nil`, the next two characters in the
659
+ # input stream will be checked, but will not be consumed.
660
+ #
661
+ # http://www.w3.org/TR/2013/WD-css-syntax-3-20130919/#check-if-two-code-points-are-a-valid-escape
662
+ def valid_escape?(text = nil)
663
+ text = @s.peek(2) if text.nil?
664
+ !!(text[0] == '\\' && text[1] != "\n")
665
+ end
666
+ end
667
+
668
+ end