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.
- data/HISTORY.md +4 -0
- data/LICENSE +18 -0
- data/README.md +157 -0
- data/lib/crass.rb +13 -0
- data/lib/crass/parser.rb +403 -0
- data/lib/crass/scanner.rb +125 -0
- data/lib/crass/token-scanner.rb +41 -0
- data/lib/crass/tokenizer.rb +668 -0
- data/lib/crass/version.rb +3 -0
- metadata +86 -0
@@ -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
|