|
|
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') };
|