var tokenizer = require('../tokenizer'); var isIdentifierStart = tokenizer.isIdentifierStart; var isHexDigit = tokenizer.isHexDigit; var isDigit = tokenizer.isDigit; var cmpStr = tokenizer.cmpStr; var consumeNumber = tokenizer.consumeNumber; var TYPE = tokenizer.TYPE; var anPlusB = require('./generic-an-plus-b'); var urange = require('./generic-urange'); var cssWideKeywords = ['unset', 'initial', 'inherit']; var calcFunctionNames = ['calc(', '-moz-calc(', '-webkit-calc(']; // https://www.w3.org/TR/css-values-3/#lengths var LENGTH = { // absolute length units 'px': true, 'mm': true, 'cm': true, 'in': true, 'pt': true, 'pc': true, 'q': true, // relative length units 'em': true, 'ex': true, 'ch': true, 'rem': true, // viewport-percentage lengths 'vh': true, 'vw': true, 'vmin': true, 'vmax': true, 'vm': true }; var ANGLE = { 'deg': true, 'grad': true, 'rad': true, 'turn': true }; var TIME = { 's': true, 'ms': true }; var FREQUENCY = { 'hz': true, 'khz': true }; // https://www.w3.org/TR/css-values-3/#resolution (https://drafts.csswg.org/css-values/#resolution) var RESOLUTION = { 'dpi': true, 'dpcm': true, 'dppx': true, 'x': true // https://github.com/w3c/csswg-drafts/issues/461 }; // https://drafts.csswg.org/css-grid/#fr-unit var FLEX = { 'fr': true }; // https://www.w3.org/TR/css3-speech/#mixing-props-voice-volume var DECIBEL = { 'db': true }; // https://www.w3.org/TR/css3-speech/#voice-props-voice-pitch var SEMITONES = { 'st': true }; // safe char code getter function charCode(str, index) { return index < str.length ? str.charCodeAt(index) : 0; } function eqStr(actual, expected) { return cmpStr(actual, 0, actual.length, expected); } function eqStrAny(actual, expected) { for (var i = 0; i < expected.length; i++) { if (eqStr(actual, expected[i])) { return true; } } return false; } // IE postfix hack, i.e. 123\0 or 123px\9 function isPostfixIeHack(str, offset) { if (offset !== str.length - 2) { return false; } return ( str.charCodeAt(offset) === 0x005C && // U+005C REVERSE SOLIDUS (\) isDigit(str.charCodeAt(offset + 1)) ); } function outOfRange(opts, value, numEnd) { if (opts && opts.type === 'Range') { var num = Number( numEnd !== undefined && numEnd !== value.length ? value.substr(0, numEnd) : value ); if (isNaN(num)) { return true; } if (opts.min !== null && num < opts.min) { return true; } if (opts.max !== null && num > opts.max) { return true; } } return false; } function consumeFunction(token, getNextToken) { var startIdx = token.index; var length = 0; // balanced token consuming do { length++; if (token.balance <= startIdx) { break; } } while (token = getNextToken(length)); return length; } // TODO: implement // can be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed // https://drafts.csswg.org/css-values/#calc-notation function calc(next) { return function(token, getNextToken, opts) { if (token === null) { return 0; } if (token.type === TYPE.Function && eqStrAny(token.value, calcFunctionNames)) { return consumeFunction(token, getNextToken); } return next(token, getNextToken, opts); }; } function tokenType(expectedTokenType) { return function(token) { if (token === null || token.type !== expectedTokenType) { return 0; } return 1; }; } function func(name) { name = name + '('; return function(token, getNextToken) { if (token !== null && eqStr(token.value, name)) { return consumeFunction(token, getNextToken); } return 0; }; } // ========================= // Complex types // // https://drafts.csswg.org/css-values-4/#custom-idents // 4.2. Author-defined Identifiers: the <custom-ident> type // Some properties accept arbitrary author-defined identifiers as a component value. // This generic data type is denoted by <custom-ident>, and represents any valid CSS identifier // that would not be misinterpreted as a pre-defined keyword in that property’s value definition. // // See also: https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident function customIdent(token) { if (token === null || token.type !== TYPE.Ident) { return 0; } var name = token.value.toLowerCase(); // The CSS-wide keywords are not valid <custom-ident>s if (eqStrAny(name, cssWideKeywords)) { return 0; } // The default keyword is reserved and is also not a valid <custom-ident> if (eqStr(name, 'default')) { return 0; } // TODO: ignore property specific keywords (as described https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident) // Specifications using <custom-ident> must specify clearly what other keywords // are excluded from <custom-ident>, if any—for example by saying that any pre-defined keywords // in that property’s value definition are excluded. Excluded keywords are excluded // in all ASCII case permutations. return 1; } // https://drafts.csswg.org/css-variables/#typedef-custom-property-name // A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo. // The <custom-property-name> production corresponds to this: it’s defined as any valid identifier // that starts with two dashes, except -- itself, which is reserved for future use by CSS. // NOTE: Current implementation treat `--` as a valid name since most (all?) major browsers treat it as valid. function customPropertyName(token) { // ... defined as any valid identifier if (token === null || token.type !== TYPE.Ident) { return 0; } // ... that starts with two dashes (U+002D HYPHEN-MINUS) if (charCode(token.value, 0) !== 0x002D || charCode(token.value, 1) !== 0x002D) { return 0; } return 1; } // https://drafts.csswg.org/css-color-4/#hex-notation // The syntax of a <hex-color> is a <hash-token> token whose value consists of 3, 4, 6, or 8 hexadecimal digits. // In other words, a hex color is written as a hash character, "#", followed by some number of digits 0-9 or // letters a-f (the case of the letters doesn’t matter - #00ff00 is identical to #00FF00). function hexColor(token) { if (token === null || token.type !== TYPE.Hash) { return 0; } var length = token.value.length; // valid values (length): #rgb (4), #rgba (5), #rrggbb (7), #rrggbbaa (9) if (length !== 4 && length !== 5 && length !== 7 && length !== 9) { return 0; } for (var i = 1; i < length; i++) { if (!isHexDigit(token.value.charCodeAt(i))) { return 0; } } return 1; } function idSelector(token) { if (token === null || token.type !== TYPE.Hash) { return 0; } if (!isIdentifierStart(charCode(token.value, 1), charCode(token.value, 2), charCode(token.value, 3))) { return 0; } return 1; } // https://drafts.csswg.org/css-syntax/#any-value // It represents the entirety of what a valid declaration can have as its value. function declarationValue(token, getNextToken) { if (!token) { return 0; } var length = 0; var level = 0; var startIdx = token.index; // The <declaration-value> production matches any sequence of one or more tokens, // so long as the sequence ... scan: do { switch (token.type) { // ... does not contain <bad-string-token>, <bad-url-token>, case TYPE.BadString: case TYPE.BadUrl: break scan; // ... unmatched <)-token>, <]-token>, or <}-token>, case TYPE.RightCurlyBracket: case TYPE.RightParenthesis: case TYPE.RightSquareBracket: if (token.balance > token.index || token.balance < startIdx) { break scan; } level--; break; // ... or top-level <semicolon-token> tokens case TYPE.Semicolon: if (level === 0) { break scan; } break; // ... or <delim-token> tokens with a value of "!" case TYPE.Delim: if (token.value === '!' && level === 0) { break scan; } break; case TYPE.Function: case TYPE.LeftParenthesis: case TYPE.LeftSquareBracket: case TYPE.LeftCurlyBracket: level++; break; } length++; // until balance closing if (token.balance <= startIdx) { break; } } while (token = getNextToken(length)); return length; } // https://drafts.csswg.org/css-syntax/#any-value // The <any-value> production is identical to <declaration-value>, but also // allows top-level <semicolon-token> tokens and <delim-token> tokens // with a value of "!". It represents the entirety of what valid CSS can be in any context. function anyValue(token, getNextToken) { if (!token) { return 0; } var startIdx = token.index; var length = 0; // The <any-value> production matches any sequence of one or more tokens, // so long as the sequence ... scan: do { switch (token.type) { // ... does not contain <bad-string-token>, <bad-url-token>, case TYPE.BadString: case TYPE.BadUrl: break scan; // ... unmatched <)-token>, <]-token>, or <}-token>, case TYPE.RightCurlyBracket: case TYPE.RightParenthesis: case TYPE.RightSquareBracket: if (token.balance > token.index || token.balance < startIdx) { break scan; } break; } length++; // until balance closing if (token.balance <= startIdx) { break; } } while (token = getNextToken(length)); return length; } // ========================= // Dimensions // function dimension(type) { return function(token, getNextToken, opts) { if (token === null || token.type !== TYPE.Dimension) { return 0; } var numberEnd = consumeNumber(token.value, 0); // check unit if (type !== null) { // check for IE postfix hack, i.e. 123px\0 or 123px\9 var reverseSolidusOffset = token.value.indexOf('\\', numberEnd); var unit = reverseSolidusOffset === -1 || !isPostfixIeHack(token.value, reverseSolidusOffset) ? token.value.substr(numberEnd) : token.value.substring(numberEnd, reverseSolidusOffset); if (type.hasOwnProperty(unit.toLowerCase()) === false) { return 0; } } // check range if specified if (outOfRange(opts, token.value, numberEnd)) { return 0; } return 1; }; } // ========================= // Percentage // // §5.5. Percentages: the <percentage> type // https://drafts.csswg.org/css-values-4/#percentages function percentage(token, getNextToken, opts) { // ... corresponds to the <percentage-token> production if (token === null || token.type !== TYPE.Percentage) { return 0; } // check range if specified if (outOfRange(opts, token.value, token.value.length - 1)) { return 0; } return 1; } // ========================= // Numeric // // https://drafts.csswg.org/css-values-4/#numbers // The value <zero> represents a literal number with the value 0. Expressions that merely // evaluate to a <number> with the value 0 (for example, calc(0)) do not match <zero>; // only literal <number-token>s do. function zero(next) { if (typeof next !== 'function') { next = function() { return 0; }; } return function(token, getNextToken, opts) { if (token !== null && token.type === TYPE.Number) { if (Number(token.value) === 0) { return 1; } } return next(token, getNextToken, opts); }; } // § 5.3. Real Numbers: the <number> type // https://drafts.csswg.org/css-values-4/#numbers // Number values are denoted by <number>, and represent real numbers, possibly with a fractional component. // ... It corresponds to the <number-token> production function number(token, getNextToken, opts) { if (token === null) { return 0; } var numberEnd = consumeNumber(token.value, 0); var isNumber = numberEnd === token.value.length; if (!isNumber && !isPostfixIeHack(token.value, numberEnd)) { return 0; } // check range if specified if (outOfRange(opts, token.value, numberEnd)) { return 0; } return 1; } // §5.2. Integers: the <integer> type // https://drafts.csswg.org/css-values-4/#integers function integer(token, getNextToken, opts) { // ... corresponds to a subset of the <number-token> production if (token === null || token.type !== TYPE.Number) { return 0; } // The first digit of an integer may be immediately preceded by `-` or `+` to indicate the integer’s sign. var i = token.value.charCodeAt(0) === 0x002B || // U+002B PLUS SIGN (+) token.value.charCodeAt(0) === 0x002D ? 1 : 0; // U+002D HYPHEN-MINUS (-) // When written literally, an integer is one or more decimal digits 0 through 9 ... for (; i < token.value.length; i++) { if (!isDigit(token.value.charCodeAt(i))) { return 0; } } // check range if specified if (outOfRange(opts, token.value, i)) { return 0; } return 1; } module.exports = { // token types 'ident-token': tokenType(TYPE.Ident), 'function-token': tokenType(TYPE.Function), 'at-keyword-token': tokenType(TYPE.AtKeyword), 'hash-token': tokenType(TYPE.Hash), 'string-token': tokenType(TYPE.String), 'bad-string-token': tokenType(TYPE.BadString), 'url-token': tokenType(TYPE.Url), 'bad-url-token': tokenType(TYPE.BadUrl), 'delim-token': tokenType(TYPE.Delim), 'number-token': tokenType(TYPE.Number), 'percentage-token': tokenType(TYPE.Percentage), 'dimension-token': tokenType(TYPE.Dimension), 'whitespace-token': tokenType(TYPE.WhiteSpace), 'CDO-token': tokenType(TYPE.CDO), 'CDC-token': tokenType(TYPE.CDC), 'colon-token': tokenType(TYPE.Colon), 'semicolon-token': tokenType(TYPE.Semicolon), 'comma-token': tokenType(TYPE.Comma), '[-token': tokenType(TYPE.LeftSquareBracket), ']-token': tokenType(TYPE.RightSquareBracket), '(-token': tokenType(TYPE.LeftParenthesis), ')-token': tokenType(TYPE.RightParenthesis), '{-token': tokenType(TYPE.LeftCurlyBracket), '}-token': tokenType(TYPE.RightCurlyBracket), // token type aliases 'string': tokenType(TYPE.String), 'ident': tokenType(TYPE.Ident), // complex types 'custom-ident': customIdent, 'custom-property-name': customPropertyName, 'hex-color': hexColor, 'id-selector': idSelector, // element( <id-selector> ) 'an-plus-b': anPlusB, 'urange': urange, 'declaration-value': declarationValue, 'any-value': anyValue, // dimensions 'dimension': calc(dimension(null)), 'angle': calc(dimension(ANGLE)), 'decibel': calc(dimension(DECIBEL)), 'frequency': calc(dimension(FREQUENCY)), 'flex': calc(dimension(FLEX)), 'length': calc(zero(dimension(LENGTH))), 'resolution': calc(dimension(RESOLUTION)), 'semitones': calc(dimension(SEMITONES)), 'time': calc(dimension(TIME)), // percentage 'percentage': calc(percentage), // numeric 'zero': zero(), 'number': calc(number), 'integer': calc(integer), // old IE stuff '-ms-legacy-expression': func('expression') };