You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
585 lines
16 KiB
585 lines
16 KiB
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') |
|
};
|
|
|