crass 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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