[6a3a178] | 1 | const openBracket = '{'.charCodeAt(0);
|
---|
| 2 | const closeBracket = '}'.charCodeAt(0);
|
---|
| 3 | const openParen = '('.charCodeAt(0);
|
---|
| 4 | const closeParen = ')'.charCodeAt(0);
|
---|
| 5 | const singleQuote = "'".charCodeAt(0);
|
---|
| 6 | const doubleQuote = '"'.charCodeAt(0);
|
---|
| 7 | const backslash = '\\'.charCodeAt(0);
|
---|
| 8 | const slash = '/'.charCodeAt(0);
|
---|
| 9 | const period = '.'.charCodeAt(0);
|
---|
| 10 | const comma = ','.charCodeAt(0);
|
---|
| 11 | const colon = ':'.charCodeAt(0);
|
---|
| 12 | const asterisk = '*'.charCodeAt(0);
|
---|
| 13 | const minus = '-'.charCodeAt(0);
|
---|
| 14 | const plus = '+'.charCodeAt(0);
|
---|
| 15 | const pound = '#'.charCodeAt(0);
|
---|
| 16 | const newline = '\n'.charCodeAt(0);
|
---|
| 17 | const space = ' '.charCodeAt(0);
|
---|
| 18 | const feed = '\f'.charCodeAt(0);
|
---|
| 19 | const tab = '\t'.charCodeAt(0);
|
---|
| 20 | const cr = '\r'.charCodeAt(0);
|
---|
| 21 | const at = '@'.charCodeAt(0);
|
---|
| 22 | const lowerE = 'e'.charCodeAt(0);
|
---|
| 23 | const upperE = 'E'.charCodeAt(0);
|
---|
| 24 | const digit0 = '0'.charCodeAt(0);
|
---|
| 25 | const digit9 = '9'.charCodeAt(0);
|
---|
| 26 | const lowerU = 'u'.charCodeAt(0);
|
---|
| 27 | const upperU = 'U'.charCodeAt(0);
|
---|
| 28 | const atEnd = /[ \n\t\r\{\(\)'"\\;,/]/g;
|
---|
| 29 | const wordEnd = /[ \n\t\r\(\)\{\}\*:;@!&'"\+\|~>,\[\]\\]|\/(?=\*)/g;
|
---|
| 30 | const wordEndNum = /[ \n\t\r\(\)\{\}\*:;@!&'"\-\+\|~>,\[\]\\]|\//g;
|
---|
| 31 | const alphaNum = /^[a-z0-9]/i;
|
---|
| 32 | const unicodeRange = /^[a-f0-9?\-]/i;
|
---|
| 33 |
|
---|
| 34 | const util = require('util');
|
---|
| 35 |
|
---|
| 36 | const TokenizeError = require('./errors/TokenizeError');
|
---|
| 37 |
|
---|
| 38 | module.exports = function tokenize(input, options) {
|
---|
| 39 | options = options || {};
|
---|
| 40 |
|
---|
| 41 | const tokens = [];
|
---|
| 42 |
|
---|
| 43 | const css = input.valueOf();
|
---|
| 44 |
|
---|
| 45 | const length = css.length;
|
---|
| 46 |
|
---|
| 47 | let offset = -1;
|
---|
| 48 |
|
---|
| 49 | let line = 1;
|
---|
| 50 |
|
---|
| 51 | let pos = 0;
|
---|
| 52 |
|
---|
| 53 | let parentCount = 0;
|
---|
| 54 |
|
---|
| 55 | let isURLArg = null;
|
---|
| 56 |
|
---|
| 57 | let code;
|
---|
| 58 | let next;
|
---|
| 59 | let quote;
|
---|
| 60 | let lines;
|
---|
| 61 | let last;
|
---|
| 62 | let content;
|
---|
| 63 | let escape;
|
---|
| 64 | let nextLine;
|
---|
| 65 | let nextOffset;
|
---|
| 66 |
|
---|
| 67 | let escaped;
|
---|
| 68 | let escapePos;
|
---|
| 69 | let nextChar;
|
---|
| 70 |
|
---|
| 71 | function unclosed(what) {
|
---|
| 72 | const message = util.format(
|
---|
| 73 | 'Unclosed %s at line: %d, column: %d, token: %d',
|
---|
| 74 | what,
|
---|
| 75 | line,
|
---|
| 76 | pos - offset,
|
---|
| 77 | pos
|
---|
| 78 | );
|
---|
| 79 | throw new TokenizeError(message);
|
---|
| 80 | }
|
---|
| 81 |
|
---|
| 82 | function tokenizeError() {
|
---|
| 83 | const message = util.format(
|
---|
| 84 | 'Syntax error at line: %d, column: %d, token: %d',
|
---|
| 85 | line,
|
---|
| 86 | pos - offset,
|
---|
| 87 | pos
|
---|
| 88 | );
|
---|
| 89 | throw new TokenizeError(message);
|
---|
| 90 | }
|
---|
| 91 |
|
---|
| 92 | while (pos < length) {
|
---|
| 93 | code = css.charCodeAt(pos);
|
---|
| 94 |
|
---|
| 95 | if (code === newline) {
|
---|
| 96 | offset = pos;
|
---|
| 97 | line += 1;
|
---|
| 98 | }
|
---|
| 99 |
|
---|
| 100 | switch (code) {
|
---|
| 101 | case newline:
|
---|
| 102 | case space:
|
---|
| 103 | case tab:
|
---|
| 104 | case cr:
|
---|
| 105 | case feed:
|
---|
| 106 | next = pos;
|
---|
| 107 | do {
|
---|
| 108 | next += 1;
|
---|
| 109 | code = css.charCodeAt(next);
|
---|
| 110 | if (code === newline) {
|
---|
| 111 | offset = next;
|
---|
| 112 | line += 1;
|
---|
| 113 | }
|
---|
| 114 | } while (
|
---|
| 115 | code === space ||
|
---|
| 116 | code === newline ||
|
---|
| 117 | code === tab ||
|
---|
| 118 | code === cr ||
|
---|
| 119 | code === feed
|
---|
| 120 | );
|
---|
| 121 |
|
---|
| 122 | tokens.push(['space', css.slice(pos, next), line, pos - offset, line, next - offset, pos]);
|
---|
| 123 |
|
---|
| 124 | pos = next - 1;
|
---|
| 125 | break;
|
---|
| 126 |
|
---|
| 127 | case colon:
|
---|
| 128 | next = pos + 1;
|
---|
| 129 | tokens.push(['colon', css.slice(pos, next), line, pos - offset, line, next - offset, pos]);
|
---|
| 130 |
|
---|
| 131 | pos = next - 1;
|
---|
| 132 | break;
|
---|
| 133 |
|
---|
| 134 | case comma:
|
---|
| 135 | next = pos + 1;
|
---|
| 136 | tokens.push(['comma', css.slice(pos, next), line, pos - offset, line, next - offset, pos]);
|
---|
| 137 |
|
---|
| 138 | pos = next - 1;
|
---|
| 139 | break;
|
---|
| 140 |
|
---|
| 141 | case openBracket:
|
---|
| 142 | tokens.push(['{', '{', line, pos - offset, line, next - offset, pos]);
|
---|
| 143 | break;
|
---|
| 144 |
|
---|
| 145 | case closeBracket:
|
---|
| 146 | tokens.push(['}', '}', line, pos - offset, line, next - offset, pos]);
|
---|
| 147 | break;
|
---|
| 148 |
|
---|
| 149 | case openParen:
|
---|
| 150 | parentCount++;
|
---|
| 151 | isURLArg =
|
---|
| 152 | !isURLArg &&
|
---|
| 153 | parentCount === 1 &&
|
---|
| 154 | tokens.length > 0 &&
|
---|
| 155 | tokens[tokens.length - 1][0] === 'word' &&
|
---|
| 156 | tokens[tokens.length - 1][1] === 'url';
|
---|
| 157 | tokens.push(['(', '(', line, pos - offset, line, next - offset, pos]);
|
---|
| 158 | break;
|
---|
| 159 |
|
---|
| 160 | case closeParen:
|
---|
| 161 | parentCount--;
|
---|
| 162 | isURLArg = !isURLArg && parentCount === 1;
|
---|
| 163 | tokens.push([')', ')', line, pos - offset, line, next - offset, pos]);
|
---|
| 164 | break;
|
---|
| 165 |
|
---|
| 166 | case singleQuote:
|
---|
| 167 | case doubleQuote:
|
---|
| 168 | quote = code === singleQuote ? "'" : '"';
|
---|
| 169 | next = pos;
|
---|
| 170 | do {
|
---|
| 171 | escaped = false;
|
---|
| 172 | next = css.indexOf(quote, next + 1);
|
---|
| 173 | if (next === -1) {
|
---|
| 174 | unclosed('quote', quote);
|
---|
| 175 | }
|
---|
| 176 | escapePos = next;
|
---|
| 177 | while (css.charCodeAt(escapePos - 1) === backslash) {
|
---|
| 178 | escapePos -= 1;
|
---|
| 179 | escaped = !escaped;
|
---|
| 180 | }
|
---|
| 181 | } while (escaped);
|
---|
| 182 |
|
---|
| 183 | tokens.push([
|
---|
| 184 | 'string',
|
---|
| 185 | css.slice(pos, next + 1),
|
---|
| 186 | line,
|
---|
| 187 | pos - offset,
|
---|
| 188 | line,
|
---|
| 189 | next - offset,
|
---|
| 190 | pos
|
---|
| 191 | ]);
|
---|
| 192 | pos = next;
|
---|
| 193 | break;
|
---|
| 194 |
|
---|
| 195 | case at:
|
---|
| 196 | atEnd.lastIndex = pos + 1;
|
---|
| 197 | atEnd.test(css);
|
---|
| 198 |
|
---|
| 199 | if (atEnd.lastIndex === 0) {
|
---|
| 200 | next = css.length - 1;
|
---|
| 201 | } else {
|
---|
| 202 | next = atEnd.lastIndex - 2;
|
---|
| 203 | }
|
---|
| 204 |
|
---|
| 205 | tokens.push([
|
---|
| 206 | 'atword',
|
---|
| 207 | css.slice(pos, next + 1),
|
---|
| 208 | line,
|
---|
| 209 | pos - offset,
|
---|
| 210 | line,
|
---|
| 211 | next - offset,
|
---|
| 212 | pos
|
---|
| 213 | ]);
|
---|
| 214 | pos = next;
|
---|
| 215 | break;
|
---|
| 216 |
|
---|
| 217 | case backslash:
|
---|
| 218 | next = pos;
|
---|
| 219 | code = css.charCodeAt(next + 1);
|
---|
| 220 |
|
---|
| 221 | if (
|
---|
| 222 | escape &&
|
---|
| 223 | (code !== slash &&
|
---|
| 224 | code !== space &&
|
---|
| 225 | code !== newline &&
|
---|
| 226 | code !== tab &&
|
---|
| 227 | code !== cr &&
|
---|
| 228 | code !== feed)
|
---|
| 229 | ) {
|
---|
| 230 | next += 1;
|
---|
| 231 | }
|
---|
| 232 |
|
---|
| 233 | tokens.push([
|
---|
| 234 | 'word',
|
---|
| 235 | css.slice(pos, next + 1),
|
---|
| 236 | line,
|
---|
| 237 | pos - offset,
|
---|
| 238 | line,
|
---|
| 239 | next - offset,
|
---|
| 240 | pos
|
---|
| 241 | ]);
|
---|
| 242 |
|
---|
| 243 | pos = next;
|
---|
| 244 | break;
|
---|
| 245 |
|
---|
| 246 | case plus:
|
---|
| 247 | case minus:
|
---|
| 248 | case asterisk:
|
---|
| 249 | next = pos + 1;
|
---|
| 250 | nextChar = css.slice(pos + 1, next + 1);
|
---|
| 251 |
|
---|
| 252 | const prevChar = css.slice(pos - 1, pos);
|
---|
| 253 |
|
---|
| 254 | // if the operator is immediately followed by a word character, then we
|
---|
| 255 | // have a prefix of some kind, and should fall-through. eg. -webkit
|
---|
| 256 |
|
---|
| 257 | // look for --* for custom variables
|
---|
| 258 | if (code === minus && nextChar.charCodeAt(0) === minus) {
|
---|
| 259 | next++;
|
---|
| 260 |
|
---|
| 261 | tokens.push(['word', css.slice(pos, next), line, pos - offset, line, next - offset, pos]);
|
---|
| 262 |
|
---|
| 263 | pos = next - 1;
|
---|
| 264 | break;
|
---|
| 265 | }
|
---|
| 266 |
|
---|
| 267 | tokens.push([
|
---|
| 268 | 'operator',
|
---|
| 269 | css.slice(pos, next),
|
---|
| 270 | line,
|
---|
| 271 | pos - offset,
|
---|
| 272 | line,
|
---|
| 273 | next - offset,
|
---|
| 274 | pos
|
---|
| 275 | ]);
|
---|
| 276 |
|
---|
| 277 | pos = next - 1;
|
---|
| 278 | break;
|
---|
| 279 |
|
---|
| 280 | default:
|
---|
| 281 | if (
|
---|
| 282 | code === slash &&
|
---|
| 283 | (css.charCodeAt(pos + 1) === asterisk ||
|
---|
| 284 | (options.loose && !isURLArg && css.charCodeAt(pos + 1) === slash))
|
---|
| 285 | ) {
|
---|
| 286 | const isStandardComment = css.charCodeAt(pos + 1) === asterisk;
|
---|
| 287 |
|
---|
| 288 | if (isStandardComment) {
|
---|
| 289 | next = css.indexOf('*/', pos + 2) + 1;
|
---|
| 290 | if (next === 0) {
|
---|
| 291 | unclosed('comment', '*/');
|
---|
| 292 | }
|
---|
| 293 | } else {
|
---|
| 294 | const newlinePos = css.indexOf('\n', pos + 2);
|
---|
| 295 |
|
---|
| 296 | next = newlinePos !== -1 ? newlinePos - 1 : length;
|
---|
| 297 | }
|
---|
| 298 |
|
---|
| 299 | content = css.slice(pos, next + 1);
|
---|
| 300 | lines = content.split('\n');
|
---|
| 301 | last = lines.length - 1;
|
---|
| 302 |
|
---|
| 303 | if (last > 0) {
|
---|
| 304 | nextLine = line + last;
|
---|
| 305 | nextOffset = next - lines[last].length;
|
---|
| 306 | } else {
|
---|
| 307 | nextLine = line;
|
---|
| 308 | nextOffset = offset;
|
---|
| 309 | }
|
---|
| 310 |
|
---|
| 311 | tokens.push(['comment', content, line, pos - offset, nextLine, next - nextOffset, pos]);
|
---|
| 312 |
|
---|
| 313 | offset = nextOffset;
|
---|
| 314 | line = nextLine;
|
---|
| 315 | pos = next;
|
---|
| 316 | } else if (code === pound && !alphaNum.test(css.slice(pos + 1, pos + 2))) {
|
---|
| 317 | next = pos + 1;
|
---|
| 318 |
|
---|
| 319 | tokens.push(['#', css.slice(pos, next), line, pos - offset, line, next - offset, pos]);
|
---|
| 320 |
|
---|
| 321 | pos = next - 1;
|
---|
| 322 | } else if ((code === lowerU || code === upperU) && css.charCodeAt(pos + 1) === plus) {
|
---|
| 323 | next = pos + 2;
|
---|
| 324 |
|
---|
| 325 | do {
|
---|
| 326 | next += 1;
|
---|
| 327 | code = css.charCodeAt(next);
|
---|
| 328 | } while (next < length && unicodeRange.test(css.slice(next, next + 1)));
|
---|
| 329 |
|
---|
| 330 | tokens.push([
|
---|
| 331 | 'unicoderange',
|
---|
| 332 | css.slice(pos, next),
|
---|
| 333 | line,
|
---|
| 334 | pos - offset,
|
---|
| 335 | line,
|
---|
| 336 | next - offset,
|
---|
| 337 | pos
|
---|
| 338 | ]);
|
---|
| 339 | pos = next - 1;
|
---|
| 340 | }
|
---|
| 341 | // catch a regular slash, that isn't a comment
|
---|
| 342 | else if (code === slash) {
|
---|
| 343 | next = pos + 1;
|
---|
| 344 |
|
---|
| 345 | tokens.push([
|
---|
| 346 | 'operator',
|
---|
| 347 | css.slice(pos, next),
|
---|
| 348 | line,
|
---|
| 349 | pos - offset,
|
---|
| 350 | line,
|
---|
| 351 | next - offset,
|
---|
| 352 | pos
|
---|
| 353 | ]);
|
---|
| 354 |
|
---|
| 355 | pos = next - 1;
|
---|
| 356 | } else {
|
---|
| 357 | let regex = wordEnd;
|
---|
| 358 |
|
---|
| 359 | // we're dealing with a word that starts with a number
|
---|
| 360 | // those get treated differently
|
---|
| 361 | if (code >= digit0 && code <= digit9) {
|
---|
| 362 | regex = wordEndNum;
|
---|
| 363 | }
|
---|
| 364 |
|
---|
| 365 | regex.lastIndex = pos + 1;
|
---|
| 366 | regex.test(css);
|
---|
| 367 |
|
---|
| 368 | if (regex.lastIndex === 0) {
|
---|
| 369 | next = css.length - 1;
|
---|
| 370 | } else {
|
---|
| 371 | next = regex.lastIndex - 2;
|
---|
| 372 | }
|
---|
| 373 |
|
---|
| 374 | // Exponential number notation with minus or plus: 1e-10, 1e+10
|
---|
| 375 | if (regex === wordEndNum || code === period) {
|
---|
| 376 | const ncode = css.charCodeAt(next);
|
---|
| 377 |
|
---|
| 378 | const ncode1 = css.charCodeAt(next + 1);
|
---|
| 379 |
|
---|
| 380 | const ncode2 = css.charCodeAt(next + 2);
|
---|
| 381 |
|
---|
| 382 | if (
|
---|
| 383 | (ncode === lowerE || ncode === upperE) &&
|
---|
| 384 | (ncode1 === minus || ncode1 === plus) &&
|
---|
| 385 | (ncode2 >= digit0 && ncode2 <= digit9)
|
---|
| 386 | ) {
|
---|
| 387 | wordEndNum.lastIndex = next + 2;
|
---|
| 388 | wordEndNum.test(css);
|
---|
| 389 |
|
---|
| 390 | if (wordEndNum.lastIndex === 0) {
|
---|
| 391 | next = css.length - 1;
|
---|
| 392 | } else {
|
---|
| 393 | next = wordEndNum.lastIndex - 2;
|
---|
| 394 | }
|
---|
| 395 | }
|
---|
| 396 | }
|
---|
| 397 |
|
---|
| 398 | tokens.push([
|
---|
| 399 | 'word',
|
---|
| 400 | css.slice(pos, next + 1),
|
---|
| 401 | line,
|
---|
| 402 | pos - offset,
|
---|
| 403 | line,
|
---|
| 404 | next - offset,
|
---|
| 405 | pos
|
---|
| 406 | ]);
|
---|
| 407 | pos = next;
|
---|
| 408 | }
|
---|
| 409 | break;
|
---|
| 410 | }
|
---|
| 411 |
|
---|
| 412 | pos++;
|
---|
| 413 | }
|
---|
| 414 |
|
---|
| 415 | return tokens;
|
---|
| 416 | };
|
---|