Tim van der Lippe | 706ec96 | 2021-06-04 13:24:42 +0100 | [diff] [blame^] | 1 | var tokenizer = require('../tokenizer'); |
| 2 | var isIdentifierStart = tokenizer.isIdentifierStart; |
| 3 | var isHexDigit = tokenizer.isHexDigit; |
| 4 | var isDigit = tokenizer.isDigit; |
| 5 | var cmpStr = tokenizer.cmpStr; |
| 6 | var consumeNumber = tokenizer.consumeNumber; |
| 7 | var TYPE = tokenizer.TYPE; |
| 8 | var anPlusB = require('./generic-an-plus-b'); |
| 9 | var urange = require('./generic-urange'); |
| 10 | |
| 11 | var cssWideKeywords = ['unset', 'initial', 'inherit']; |
| 12 | var calcFunctionNames = ['calc(', '-moz-calc(', '-webkit-calc(']; |
| 13 | |
| 14 | // https://www.w3.org/TR/css-values-3/#lengths |
| 15 | var LENGTH = { |
| 16 | // absolute length units |
| 17 | 'px': true, |
| 18 | 'mm': true, |
| 19 | 'cm': true, |
| 20 | 'in': true, |
| 21 | 'pt': true, |
| 22 | 'pc': true, |
| 23 | 'q': true, |
| 24 | |
| 25 | // relative length units |
| 26 | 'em': true, |
| 27 | 'ex': true, |
| 28 | 'ch': true, |
| 29 | 'rem': true, |
| 30 | |
| 31 | // viewport-percentage lengths |
| 32 | 'vh': true, |
| 33 | 'vw': true, |
| 34 | 'vmin': true, |
| 35 | 'vmax': true, |
| 36 | 'vm': true |
| 37 | }; |
| 38 | |
| 39 | var ANGLE = { |
| 40 | 'deg': true, |
| 41 | 'grad': true, |
| 42 | 'rad': true, |
| 43 | 'turn': true |
| 44 | }; |
| 45 | |
| 46 | var TIME = { |
| 47 | 's': true, |
| 48 | 'ms': true |
| 49 | }; |
| 50 | |
| 51 | var FREQUENCY = { |
| 52 | 'hz': true, |
| 53 | 'khz': true |
| 54 | }; |
| 55 | |
| 56 | // https://www.w3.org/TR/css-values-3/#resolution (https://drafts.csswg.org/css-values/#resolution) |
| 57 | var RESOLUTION = { |
| 58 | 'dpi': true, |
| 59 | 'dpcm': true, |
| 60 | 'dppx': true, |
| 61 | 'x': true // https://github.com/w3c/csswg-drafts/issues/461 |
| 62 | }; |
| 63 | |
| 64 | // https://drafts.csswg.org/css-grid/#fr-unit |
| 65 | var FLEX = { |
| 66 | 'fr': true |
| 67 | }; |
| 68 | |
| 69 | // https://www.w3.org/TR/css3-speech/#mixing-props-voice-volume |
| 70 | var DECIBEL = { |
| 71 | 'db': true |
| 72 | }; |
| 73 | |
| 74 | // https://www.w3.org/TR/css3-speech/#voice-props-voice-pitch |
| 75 | var SEMITONES = { |
| 76 | 'st': true |
| 77 | }; |
| 78 | |
| 79 | // safe char code getter |
| 80 | function charCode(str, index) { |
| 81 | return index < str.length ? str.charCodeAt(index) : 0; |
| 82 | } |
| 83 | |
| 84 | function eqStr(actual, expected) { |
| 85 | return cmpStr(actual, 0, actual.length, expected); |
| 86 | } |
| 87 | |
| 88 | function eqStrAny(actual, expected) { |
| 89 | for (var i = 0; i < expected.length; i++) { |
| 90 | if (eqStr(actual, expected[i])) { |
| 91 | return true; |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | return false; |
| 96 | } |
| 97 | |
| 98 | // IE postfix hack, i.e. 123\0 or 123px\9 |
| 99 | function isPostfixIeHack(str, offset) { |
| 100 | if (offset !== str.length - 2) { |
| 101 | return false; |
| 102 | } |
| 103 | |
| 104 | return ( |
| 105 | str.charCodeAt(offset) === 0x005C && // U+005C REVERSE SOLIDUS (\) |
| 106 | isDigit(str.charCodeAt(offset + 1)) |
| 107 | ); |
| 108 | } |
| 109 | |
| 110 | function outOfRange(opts, value, numEnd) { |
| 111 | if (opts && opts.type === 'Range') { |
| 112 | var num = Number( |
| 113 | numEnd !== undefined && numEnd !== value.length |
| 114 | ? value.substr(0, numEnd) |
| 115 | : value |
| 116 | ); |
| 117 | |
| 118 | if (isNaN(num)) { |
| 119 | return true; |
| 120 | } |
| 121 | |
| 122 | if (opts.min !== null && num < opts.min) { |
| 123 | return true; |
| 124 | } |
| 125 | |
| 126 | if (opts.max !== null && num > opts.max) { |
| 127 | return true; |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | return false; |
| 132 | } |
| 133 | |
| 134 | function consumeFunction(token, getNextToken) { |
| 135 | var startIdx = token.index; |
| 136 | var length = 0; |
| 137 | |
| 138 | // balanced token consuming |
| 139 | do { |
| 140 | length++; |
| 141 | |
| 142 | if (token.balance <= startIdx) { |
| 143 | break; |
| 144 | } |
| 145 | } while (token = getNextToken(length)); |
| 146 | |
| 147 | return length; |
| 148 | } |
| 149 | |
| 150 | // TODO: implement |
| 151 | // can be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed |
| 152 | // https://drafts.csswg.org/css-values/#calc-notation |
| 153 | function calc(next) { |
| 154 | return function(token, getNextToken, opts) { |
| 155 | if (token === null) { |
| 156 | return 0; |
| 157 | } |
| 158 | |
| 159 | if (token.type === TYPE.Function && eqStrAny(token.value, calcFunctionNames)) { |
| 160 | return consumeFunction(token, getNextToken); |
| 161 | } |
| 162 | |
| 163 | return next(token, getNextToken, opts); |
| 164 | }; |
| 165 | } |
| 166 | |
| 167 | function tokenType(expectedTokenType) { |
| 168 | return function(token) { |
| 169 | if (token === null || token.type !== expectedTokenType) { |
| 170 | return 0; |
| 171 | } |
| 172 | |
| 173 | return 1; |
| 174 | }; |
| 175 | } |
| 176 | |
| 177 | function func(name) { |
| 178 | name = name + '('; |
| 179 | |
| 180 | return function(token, getNextToken) { |
| 181 | if (token !== null && eqStr(token.value, name)) { |
| 182 | return consumeFunction(token, getNextToken); |
| 183 | } |
| 184 | |
| 185 | return 0; |
| 186 | }; |
| 187 | } |
| 188 | |
| 189 | // ========================= |
| 190 | // Complex types |
| 191 | // |
| 192 | |
| 193 | // https://drafts.csswg.org/css-values-4/#custom-idents |
| 194 | // 4.2. Author-defined Identifiers: the <custom-ident> type |
| 195 | // Some properties accept arbitrary author-defined identifiers as a component value. |
| 196 | // This generic data type is denoted by <custom-ident>, and represents any valid CSS identifier |
| 197 | // that would not be misinterpreted as a pre-defined keyword in that property’s value definition. |
| 198 | // |
| 199 | // See also: https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident |
| 200 | function customIdent(token) { |
| 201 | if (token === null || token.type !== TYPE.Ident) { |
| 202 | return 0; |
| 203 | } |
| 204 | |
| 205 | var name = token.value.toLowerCase(); |
| 206 | |
| 207 | // The CSS-wide keywords are not valid <custom-ident>s |
| 208 | if (eqStrAny(name, cssWideKeywords)) { |
| 209 | return 0; |
| 210 | } |
| 211 | |
| 212 | // The default keyword is reserved and is also not a valid <custom-ident> |
| 213 | if (eqStr(name, 'default')) { |
| 214 | return 0; |
| 215 | } |
| 216 | |
| 217 | // TODO: ignore property specific keywords (as described https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident) |
| 218 | // Specifications using <custom-ident> must specify clearly what other keywords |
| 219 | // are excluded from <custom-ident>, if any—for example by saying that any pre-defined keywords |
| 220 | // in that property’s value definition are excluded. Excluded keywords are excluded |
| 221 | // in all ASCII case permutations. |
| 222 | |
| 223 | return 1; |
| 224 | } |
| 225 | |
| 226 | // https://drafts.csswg.org/css-variables/#typedef-custom-property-name |
| 227 | // A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo. |
| 228 | // The <custom-property-name> production corresponds to this: it’s defined as any valid identifier |
| 229 | // that starts with two dashes, except -- itself, which is reserved for future use by CSS. |
| 230 | // NOTE: Current implementation treat `--` as a valid name since most (all?) major browsers treat it as valid. |
| 231 | function customPropertyName(token) { |
| 232 | // ... defined as any valid identifier |
| 233 | if (token === null || token.type !== TYPE.Ident) { |
| 234 | return 0; |
| 235 | } |
| 236 | |
| 237 | // ... that starts with two dashes (U+002D HYPHEN-MINUS) |
| 238 | if (charCode(token.value, 0) !== 0x002D || charCode(token.value, 1) !== 0x002D) { |
| 239 | return 0; |
| 240 | } |
| 241 | |
| 242 | return 1; |
| 243 | } |
| 244 | |
| 245 | // https://drafts.csswg.org/css-color-4/#hex-notation |
| 246 | // The syntax of a <hex-color> is a <hash-token> token whose value consists of 3, 4, 6, or 8 hexadecimal digits. |
| 247 | // In other words, a hex color is written as a hash character, "#", followed by some number of digits 0-9 or |
| 248 | // letters a-f (the case of the letters doesn’t matter - #00ff00 is identical to #00FF00). |
| 249 | function hexColor(token) { |
| 250 | if (token === null || token.type !== TYPE.Hash) { |
| 251 | return 0; |
| 252 | } |
| 253 | |
| 254 | var length = token.value.length; |
| 255 | |
| 256 | // valid values (length): #rgb (4), #rgba (5), #rrggbb (7), #rrggbbaa (9) |
| 257 | if (length !== 4 && length !== 5 && length !== 7 && length !== 9) { |
| 258 | return 0; |
| 259 | } |
| 260 | |
| 261 | for (var i = 1; i < length; i++) { |
| 262 | if (!isHexDigit(token.value.charCodeAt(i))) { |
| 263 | return 0; |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | return 1; |
| 268 | } |
| 269 | |
| 270 | function idSelector(token) { |
| 271 | if (token === null || token.type !== TYPE.Hash) { |
| 272 | return 0; |
| 273 | } |
| 274 | |
| 275 | if (!isIdentifierStart(charCode(token.value, 1), charCode(token.value, 2), charCode(token.value, 3))) { |
| 276 | return 0; |
| 277 | } |
| 278 | |
| 279 | return 1; |
| 280 | } |
| 281 | |
| 282 | // https://drafts.csswg.org/css-syntax/#any-value |
| 283 | // It represents the entirety of what a valid declaration can have as its value. |
| 284 | function declarationValue(token, getNextToken) { |
| 285 | if (!token) { |
| 286 | return 0; |
| 287 | } |
| 288 | |
| 289 | var length = 0; |
| 290 | var level = 0; |
| 291 | var startIdx = token.index; |
| 292 | |
| 293 | // The <declaration-value> production matches any sequence of one or more tokens, |
| 294 | // so long as the sequence ... |
| 295 | scan: |
| 296 | do { |
| 297 | switch (token.type) { |
| 298 | // ... does not contain <bad-string-token>, <bad-url-token>, |
| 299 | case TYPE.BadString: |
| 300 | case TYPE.BadUrl: |
| 301 | break scan; |
| 302 | |
| 303 | // ... unmatched <)-token>, <]-token>, or <}-token>, |
| 304 | case TYPE.RightCurlyBracket: |
| 305 | case TYPE.RightParenthesis: |
| 306 | case TYPE.RightSquareBracket: |
| 307 | if (token.balance > token.index || token.balance < startIdx) { |
| 308 | break scan; |
| 309 | } |
| 310 | |
| 311 | level--; |
| 312 | break; |
| 313 | |
| 314 | // ... or top-level <semicolon-token> tokens |
| 315 | case TYPE.Semicolon: |
| 316 | if (level === 0) { |
| 317 | break scan; |
| 318 | } |
| 319 | |
| 320 | break; |
| 321 | |
| 322 | // ... or <delim-token> tokens with a value of "!" |
| 323 | case TYPE.Delim: |
| 324 | if (token.value === '!' && level === 0) { |
| 325 | break scan; |
| 326 | } |
| 327 | |
| 328 | break; |
| 329 | |
| 330 | case TYPE.Function: |
| 331 | case TYPE.LeftParenthesis: |
| 332 | case TYPE.LeftSquareBracket: |
| 333 | case TYPE.LeftCurlyBracket: |
| 334 | level++; |
| 335 | break; |
| 336 | } |
| 337 | |
| 338 | length++; |
| 339 | |
| 340 | // until balance closing |
| 341 | if (token.balance <= startIdx) { |
| 342 | break; |
| 343 | } |
| 344 | } while (token = getNextToken(length)); |
| 345 | |
| 346 | return length; |
| 347 | } |
| 348 | |
| 349 | // https://drafts.csswg.org/css-syntax/#any-value |
| 350 | // The <any-value> production is identical to <declaration-value>, but also |
| 351 | // allows top-level <semicolon-token> tokens and <delim-token> tokens |
| 352 | // with a value of "!". It represents the entirety of what valid CSS can be in any context. |
| 353 | function anyValue(token, getNextToken) { |
| 354 | if (!token) { |
| 355 | return 0; |
| 356 | } |
| 357 | |
| 358 | var startIdx = token.index; |
| 359 | var length = 0; |
| 360 | |
| 361 | // The <any-value> production matches any sequence of one or more tokens, |
| 362 | // so long as the sequence ... |
| 363 | scan: |
| 364 | do { |
| 365 | switch (token.type) { |
| 366 | // ... does not contain <bad-string-token>, <bad-url-token>, |
| 367 | case TYPE.BadString: |
| 368 | case TYPE.BadUrl: |
| 369 | break scan; |
| 370 | |
| 371 | // ... unmatched <)-token>, <]-token>, or <}-token>, |
| 372 | case TYPE.RightCurlyBracket: |
| 373 | case TYPE.RightParenthesis: |
| 374 | case TYPE.RightSquareBracket: |
| 375 | if (token.balance > token.index || token.balance < startIdx) { |
| 376 | break scan; |
| 377 | } |
| 378 | |
| 379 | break; |
| 380 | } |
| 381 | |
| 382 | length++; |
| 383 | |
| 384 | // until balance closing |
| 385 | if (token.balance <= startIdx) { |
| 386 | break; |
| 387 | } |
| 388 | } while (token = getNextToken(length)); |
| 389 | |
| 390 | return length; |
| 391 | } |
| 392 | |
| 393 | // ========================= |
| 394 | // Dimensions |
| 395 | // |
| 396 | |
| 397 | function dimension(type) { |
| 398 | return function(token, getNextToken, opts) { |
| 399 | if (token === null || token.type !== TYPE.Dimension) { |
| 400 | return 0; |
| 401 | } |
| 402 | |
| 403 | var numberEnd = consumeNumber(token.value, 0); |
| 404 | |
| 405 | // check unit |
| 406 | if (type !== null) { |
| 407 | // check for IE postfix hack, i.e. 123px\0 or 123px\9 |
| 408 | var reverseSolidusOffset = token.value.indexOf('\\', numberEnd); |
| 409 | var unit = reverseSolidusOffset === -1 || !isPostfixIeHack(token.value, reverseSolidusOffset) |
| 410 | ? token.value.substr(numberEnd) |
| 411 | : token.value.substring(numberEnd, reverseSolidusOffset); |
| 412 | |
| 413 | if (type.hasOwnProperty(unit.toLowerCase()) === false) { |
| 414 | return 0; |
| 415 | } |
| 416 | } |
| 417 | |
| 418 | // check range if specified |
| 419 | if (outOfRange(opts, token.value, numberEnd)) { |
| 420 | return 0; |
| 421 | } |
| 422 | |
| 423 | return 1; |
| 424 | }; |
| 425 | } |
| 426 | |
| 427 | // ========================= |
| 428 | // Percentage |
| 429 | // |
| 430 | |
| 431 | // §5.5. Percentages: the <percentage> type |
| 432 | // https://drafts.csswg.org/css-values-4/#percentages |
| 433 | function percentage(token, getNextToken, opts) { |
| 434 | // ... corresponds to the <percentage-token> production |
| 435 | if (token === null || token.type !== TYPE.Percentage) { |
| 436 | return 0; |
| 437 | } |
| 438 | |
| 439 | // check range if specified |
| 440 | if (outOfRange(opts, token.value, token.value.length - 1)) { |
| 441 | return 0; |
| 442 | } |
| 443 | |
| 444 | return 1; |
| 445 | } |
| 446 | |
| 447 | // ========================= |
| 448 | // Numeric |
| 449 | // |
| 450 | |
| 451 | // https://drafts.csswg.org/css-values-4/#numbers |
| 452 | // The value <zero> represents a literal number with the value 0. Expressions that merely |
| 453 | // evaluate to a <number> with the value 0 (for example, calc(0)) do not match <zero>; |
| 454 | // only literal <number-token>s do. |
| 455 | function zero(next) { |
| 456 | if (typeof next !== 'function') { |
| 457 | next = function() { |
| 458 | return 0; |
| 459 | }; |
| 460 | } |
| 461 | |
| 462 | return function(token, getNextToken, opts) { |
| 463 | if (token !== null && token.type === TYPE.Number) { |
| 464 | if (Number(token.value) === 0) { |
| 465 | return 1; |
| 466 | } |
| 467 | } |
| 468 | |
| 469 | return next(token, getNextToken, opts); |
| 470 | }; |
| 471 | } |
| 472 | |
| 473 | // § 5.3. Real Numbers: the <number> type |
| 474 | // https://drafts.csswg.org/css-values-4/#numbers |
| 475 | // Number values are denoted by <number>, and represent real numbers, possibly with a fractional component. |
| 476 | // ... It corresponds to the <number-token> production |
| 477 | function number(token, getNextToken, opts) { |
| 478 | if (token === null) { |
| 479 | return 0; |
| 480 | } |
| 481 | |
| 482 | var numberEnd = consumeNumber(token.value, 0); |
| 483 | var isNumber = numberEnd === token.value.length; |
| 484 | if (!isNumber && !isPostfixIeHack(token.value, numberEnd)) { |
| 485 | return 0; |
| 486 | } |
| 487 | |
| 488 | // check range if specified |
| 489 | if (outOfRange(opts, token.value, numberEnd)) { |
| 490 | return 0; |
| 491 | } |
| 492 | |
| 493 | return 1; |
| 494 | } |
| 495 | |
| 496 | // §5.2. Integers: the <integer> type |
| 497 | // https://drafts.csswg.org/css-values-4/#integers |
| 498 | function integer(token, getNextToken, opts) { |
| 499 | // ... corresponds to a subset of the <number-token> production |
| 500 | if (token === null || token.type !== TYPE.Number) { |
| 501 | return 0; |
| 502 | } |
| 503 | |
| 504 | // The first digit of an integer may be immediately preceded by `-` or `+` to indicate the integer’s sign. |
| 505 | var i = token.value.charCodeAt(0) === 0x002B || // U+002B PLUS SIGN (+) |
| 506 | token.value.charCodeAt(0) === 0x002D ? 1 : 0; // U+002D HYPHEN-MINUS (-) |
| 507 | |
| 508 | // When written literally, an integer is one or more decimal digits 0 through 9 ... |
| 509 | for (; i < token.value.length; i++) { |
| 510 | if (!isDigit(token.value.charCodeAt(i))) { |
| 511 | return 0; |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | // check range if specified |
| 516 | if (outOfRange(opts, token.value, i)) { |
| 517 | return 0; |
| 518 | } |
| 519 | |
| 520 | return 1; |
| 521 | } |
| 522 | |
| 523 | module.exports = { |
| 524 | // token types |
| 525 | 'ident-token': tokenType(TYPE.Ident), |
| 526 | 'function-token': tokenType(TYPE.Function), |
| 527 | 'at-keyword-token': tokenType(TYPE.AtKeyword), |
| 528 | 'hash-token': tokenType(TYPE.Hash), |
| 529 | 'string-token': tokenType(TYPE.String), |
| 530 | 'bad-string-token': tokenType(TYPE.BadString), |
| 531 | 'url-token': tokenType(TYPE.Url), |
| 532 | 'bad-url-token': tokenType(TYPE.BadUrl), |
| 533 | 'delim-token': tokenType(TYPE.Delim), |
| 534 | 'number-token': tokenType(TYPE.Number), |
| 535 | 'percentage-token': tokenType(TYPE.Percentage), |
| 536 | 'dimension-token': tokenType(TYPE.Dimension), |
| 537 | 'whitespace-token': tokenType(TYPE.WhiteSpace), |
| 538 | 'CDO-token': tokenType(TYPE.CDO), |
| 539 | 'CDC-token': tokenType(TYPE.CDC), |
| 540 | 'colon-token': tokenType(TYPE.Colon), |
| 541 | 'semicolon-token': tokenType(TYPE.Semicolon), |
| 542 | 'comma-token': tokenType(TYPE.Comma), |
| 543 | '[-token': tokenType(TYPE.LeftSquareBracket), |
| 544 | ']-token': tokenType(TYPE.RightSquareBracket), |
| 545 | '(-token': tokenType(TYPE.LeftParenthesis), |
| 546 | ')-token': tokenType(TYPE.RightParenthesis), |
| 547 | '{-token': tokenType(TYPE.LeftCurlyBracket), |
| 548 | '}-token': tokenType(TYPE.RightCurlyBracket), |
| 549 | |
| 550 | // token type aliases |
| 551 | 'string': tokenType(TYPE.String), |
| 552 | 'ident': tokenType(TYPE.Ident), |
| 553 | |
| 554 | // complex types |
| 555 | 'custom-ident': customIdent, |
| 556 | 'custom-property-name': customPropertyName, |
| 557 | 'hex-color': hexColor, |
| 558 | 'id-selector': idSelector, // element( <id-selector> ) |
| 559 | 'an-plus-b': anPlusB, |
| 560 | 'urange': urange, |
| 561 | 'declaration-value': declarationValue, |
| 562 | 'any-value': anyValue, |
| 563 | |
| 564 | // dimensions |
| 565 | 'dimension': calc(dimension(null)), |
| 566 | 'angle': calc(dimension(ANGLE)), |
| 567 | 'decibel': calc(dimension(DECIBEL)), |
| 568 | 'frequency': calc(dimension(FREQUENCY)), |
| 569 | 'flex': calc(dimension(FLEX)), |
| 570 | 'length': calc(zero(dimension(LENGTH))), |
| 571 | 'resolution': calc(dimension(RESOLUTION)), |
| 572 | 'semitones': calc(dimension(SEMITONES)), |
| 573 | 'time': calc(dimension(TIME)), |
| 574 | |
| 575 | // percentage |
| 576 | 'percentage': calc(percentage), |
| 577 | |
| 578 | // numeric |
| 579 | 'zero': zero(), |
| 580 | 'number': calc(number), |
| 581 | 'integer': calc(integer), |
| 582 | |
| 583 | // old IE stuff |
| 584 | '-ms-legacy-expression': func('expression') |
| 585 | }; |