UNPKG

6.54 kBJavaScriptView Raw
1'use strict'
2
3const SINGLE_QUOTE = "'".charCodeAt(0)
4const DOUBLE_QUOTE = '"'.charCodeAt(0)
5const BACKSLASH = '\\'.charCodeAt(0)
6const SLASH = '/'.charCodeAt(0)
7const NEWLINE = '\n'.charCodeAt(0)
8const SPACE = ' '.charCodeAt(0)
9const FEED = '\f'.charCodeAt(0)
10const TAB = '\t'.charCodeAt(0)
11const CR = '\r'.charCodeAt(0)
12const OPEN_SQUARE = '['.charCodeAt(0)
13const CLOSE_SQUARE = ']'.charCodeAt(0)
14const OPEN_PARENTHESES = '('.charCodeAt(0)
15const CLOSE_PARENTHESES = ')'.charCodeAt(0)
16const OPEN_CURLY = '{'.charCodeAt(0)
17const CLOSE_CURLY = '}'.charCodeAt(0)
18const SEMICOLON = ';'.charCodeAt(0)
19const ASTERISK = '*'.charCodeAt(0)
20const COLON = ':'.charCodeAt(0)
21const AT = '@'.charCodeAt(0)
22
23const RE_AT_END = /[\t\n\f\r "#'()/;[\\\]{}]/g
24const RE_WORD_END = /[\t\n\f\r !"#'():;@[\\\]{}]|\/(?=\*)/g
25const RE_BAD_BRACKET = /.[\n"'(/\\]/
26const RE_HEX_ESCAPE = /[\da-f]/i
27
28module.exports = function tokenizer(input, options = {}) {
29 let css = input.css.valueOf()
30 let ignore = options.ignoreErrors
31
32 let code, next, quote, content, escape
33 let escaped, escapePos, prev, n, currentToken
34
35 let length = css.length
36 let pos = 0
37 let buffer = []
38 let returned = []
39
40 function position() {
41 return pos
42 }
43
44 function unclosed(what) {
45 throw input.error('Unclosed ' + what, pos)
46 }
47
48 function endOfFile() {
49 return returned.length === 0 && pos >= length
50 }
51
52 function nextToken(opts) {
53 if (returned.length) return returned.pop()
54 if (pos >= length) return
55
56 let ignoreUnclosed = opts ? opts.ignoreUnclosed : false
57
58 code = css.charCodeAt(pos)
59
60 switch (code) {
61 case NEWLINE:
62 case SPACE:
63 case TAB:
64 case CR:
65 case FEED: {
66 next = pos
67 do {
68 next += 1
69 code = css.charCodeAt(next)
70 } while (
71 code === SPACE ||
72 code === NEWLINE ||
73 code === TAB ||
74 code === CR ||
75 code === FEED
76 )
77
78 currentToken = ['space', css.slice(pos, next)]
79 pos = next - 1
80 break
81 }
82
83 case OPEN_SQUARE:
84 case CLOSE_SQUARE:
85 case OPEN_CURLY:
86 case CLOSE_CURLY:
87 case COLON:
88 case SEMICOLON:
89 case CLOSE_PARENTHESES: {
90 let controlChar = String.fromCharCode(code)
91 currentToken = [controlChar, controlChar, pos]
92 break
93 }
94
95 case OPEN_PARENTHESES: {
96 prev = buffer.length ? buffer.pop()[1] : ''
97 n = css.charCodeAt(pos + 1)
98 if (
99 prev === 'url' &&
100 n !== SINGLE_QUOTE &&
101 n !== DOUBLE_QUOTE &&
102 n !== SPACE &&
103 n !== NEWLINE &&
104 n !== TAB &&
105 n !== FEED &&
106 n !== CR
107 ) {
108 next = pos
109 do {
110 escaped = false
111 next = css.indexOf(')', next + 1)
112 if (next === -1) {
113 if (ignore || ignoreUnclosed) {
114 next = pos
115 break
116 } else {
117 unclosed('bracket')
118 }
119 }
120 escapePos = next
121 while (css.charCodeAt(escapePos - 1) === BACKSLASH) {
122 escapePos -= 1
123 escaped = !escaped
124 }
125 } while (escaped)
126
127 currentToken = ['brackets', css.slice(pos, next + 1), pos, next]
128
129 pos = next
130 } else {
131 next = css.indexOf(')', pos + 1)
132 content = css.slice(pos, next + 1)
133
134 if (next === -1 || RE_BAD_BRACKET.test(content)) {
135 currentToken = ['(', '(', pos]
136 } else {
137 currentToken = ['brackets', content, pos, next]
138 pos = next
139 }
140 }
141
142 break
143 }
144
145 case SINGLE_QUOTE:
146 case DOUBLE_QUOTE: {
147 quote = code === SINGLE_QUOTE ? "'" : '"'
148 next = pos
149 do {
150 escaped = false
151 next = css.indexOf(quote, next + 1)
152 if (next === -1) {
153 if (ignore || ignoreUnclosed) {
154 next = pos + 1
155 break
156 } else {
157 unclosed('string')
158 }
159 }
160 escapePos = next
161 while (css.charCodeAt(escapePos - 1) === BACKSLASH) {
162 escapePos -= 1
163 escaped = !escaped
164 }
165 } while (escaped)
166
167 currentToken = ['string', css.slice(pos, next + 1), pos, next]
168 pos = next
169 break
170 }
171
172 case AT: {
173 RE_AT_END.lastIndex = pos + 1
174 RE_AT_END.test(css)
175 if (RE_AT_END.lastIndex === 0) {
176 next = css.length - 1
177 } else {
178 next = RE_AT_END.lastIndex - 2
179 }
180
181 currentToken = ['at-word', css.slice(pos, next + 1), pos, next]
182
183 pos = next
184 break
185 }
186
187 case BACKSLASH: {
188 next = pos
189 escape = true
190 while (css.charCodeAt(next + 1) === BACKSLASH) {
191 next += 1
192 escape = !escape
193 }
194 code = css.charCodeAt(next + 1)
195 if (
196 escape &&
197 code !== SLASH &&
198 code !== SPACE &&
199 code !== NEWLINE &&
200 code !== TAB &&
201 code !== CR &&
202 code !== FEED
203 ) {
204 next += 1
205 if (RE_HEX_ESCAPE.test(css.charAt(next))) {
206 while (RE_HEX_ESCAPE.test(css.charAt(next + 1))) {
207 next += 1
208 }
209 if (css.charCodeAt(next + 1) === SPACE) {
210 next += 1
211 }
212 }
213 }
214
215 currentToken = ['word', css.slice(pos, next + 1), pos, next]
216
217 pos = next
218 break
219 }
220
221 default: {
222 if (code === SLASH && css.charCodeAt(pos + 1) === ASTERISK) {
223 next = css.indexOf('*/', pos + 2) + 1
224 if (next === 0) {
225 if (ignore || ignoreUnclosed) {
226 next = css.length
227 } else {
228 unclosed('comment')
229 }
230 }
231
232 currentToken = ['comment', css.slice(pos, next + 1), pos, next]
233 pos = next
234 } else {
235 RE_WORD_END.lastIndex = pos + 1
236 RE_WORD_END.test(css)
237 if (RE_WORD_END.lastIndex === 0) {
238 next = css.length - 1
239 } else {
240 next = RE_WORD_END.lastIndex - 2
241 }
242
243 currentToken = ['word', css.slice(pos, next + 1), pos, next]
244 buffer.push(currentToken)
245 pos = next
246 }
247
248 break
249 }
250 }
251
252 pos++
253 return currentToken
254 }
255
256 function back(token) {
257 returned.push(token)
258 }
259
260 return {
261 back,
262 endOfFile,
263 nextToken,
264 position
265 }
266}