| var tokenizerUtils = require('../tokenizer/utils'); |
| var findIdentifierEnd = tokenizerUtils.findIdentifierEnd; |
| var findNumberEnd = tokenizerUtils.findNumberEnd; |
| var findDecimalNumberEnd = tokenizerUtils.findDecimalNumberEnd; |
| var isHex = tokenizerUtils.isHex; |
| var tokenizerConst = require('../tokenizer/const'); |
| var SYMBOL_TYPE = tokenizerConst.SYMBOL_TYPE; |
| var IDENTIFIER = tokenizerConst.TYPE.Identifier; |
| var PLUSSIGN = tokenizerConst.TYPE.PlusSign; |
| var HYPHENMINUS = tokenizerConst.TYPE.HyphenMinus; |
| var NUMBERSIGN = tokenizerConst.TYPE.NumberSign; |
| |
| var PERCENTAGE = { |
| '%': true |
| }; |
| |
| // 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 |
| }; |
| |
| function consumeFunction(token, addTokenToMatch, getNextToken) { |
| var length = 1; |
| var cursor; |
| |
| do { |
| cursor = getNextToken(length++); |
| } while (cursor !== null && cursor.node !== token.node); |
| |
| if (cursor === null) { |
| return false; |
| } |
| |
| while (true) { |
| // consume tokens until cursor |
| if (addTokenToMatch() === cursor) { |
| break; |
| } |
| } |
| |
| return true; |
| } |
| |
| // 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(token, addTokenToMatch, getNextToken) { |
| if (token === null) { |
| return false; |
| } |
| |
| var name = token.value.toLowerCase(); |
| if (name !== 'calc(' && |
| name !== '-moz-calc(' && |
| name !== '-webkit-calc(') { |
| return false; |
| } |
| |
| return consumeFunction(token, addTokenToMatch, getNextToken); |
| } |
| |
| function attr(token, addTokenToMatch, getNextToken) { |
| if (token === null || token.value.toLowerCase() !== 'attr(') { |
| return false; |
| } |
| |
| return consumeFunction(token, addTokenToMatch, getNextToken); |
| } |
| |
| function expression(token, addTokenToMatch, getNextToken) { |
| if (token === null || token.value.toLowerCase() !== 'expression(') { |
| return false; |
| } |
| |
| return consumeFunction(token, addTokenToMatch, getNextToken); |
| } |
| |
| function url(token, addTokenToMatch, getNextToken) { |
| if (token === null || token.value.toLowerCase() !== 'url(') { |
| return false; |
| } |
| |
| return consumeFunction(token, addTokenToMatch, getNextToken); |
| } |
| |
| function idSelector(token, addTokenToMatch) { |
| if (token === null) { |
| return false; |
| } |
| |
| if (token.value.charCodeAt(0) !== NUMBERSIGN) { |
| return false; |
| } |
| |
| if (consumeIdentifier(token.value, 1) !== token.value.length) { |
| return false; |
| } |
| |
| addTokenToMatch(); |
| return true; |
| } |
| |
| function isNumber(str) { |
| return /^[-+]?(\d+|\d*\.\d+)([eE][-+]?\d+)?$/.test(str); |
| } |
| |
| function consumeNumber(str, allowFraction) { |
| var code = str.charCodeAt(0); |
| |
| return findNumberEnd(str, code === PLUSSIGN || code === HYPHENMINUS ? 1 : 0, allowFraction); |
| } |
| |
| function consumeIdentifier(str, offset) { |
| var code = str.charCodeAt(offset); |
| |
| if (code < 0x80 && SYMBOL_TYPE[code] !== IDENTIFIER && code !== HYPHENMINUS) { |
| return offset; |
| } |
| |
| return findIdentifierEnd(str, offset + 1); |
| } |
| |
| function astNode(type) { |
| return function(token, addTokenToMatch) { |
| if (token === null || token.node.type !== type) { |
| return false; |
| } |
| |
| addTokenToMatch(); |
| return true; |
| }; |
| } |
| |
| function dimension(type) { |
| return function(token, addTokenToMatch, getNextToken) { |
| if (calc(token, addTokenToMatch, getNextToken)) { |
| return true; |
| } |
| |
| if (token === null) { |
| return false; |
| } |
| |
| var numberEnd = consumeNumber(token.value, true); |
| if (numberEnd === 0) { |
| return false; |
| } |
| |
| if (type) { |
| if (!type.hasOwnProperty(token.value.substr(numberEnd).toLowerCase())) { |
| return false; |
| } |
| } else { |
| var unitEnd = consumeIdentifier(token.value, numberEnd); |
| if (unitEnd === numberEnd || unitEnd !== token.value.length) { |
| return false; |
| } |
| } |
| |
| addTokenToMatch(); |
| return true; |
| }; |
| } |
| |
| function zeroUnitlessDimension(type) { |
| var isDimension = dimension(type); |
| |
| return function(token, addTokenToMatch, getNextToken) { |
| if (isDimension(token, addTokenToMatch, getNextToken)) { |
| return true; |
| } |
| |
| if (token === null || Number(token.value) !== 0) { |
| return false; |
| } |
| |
| addTokenToMatch(); |
| return true; |
| }; |
| } |
| |
| function number(token, addTokenToMatch, getNextToken) { |
| if (calc(token, addTokenToMatch, getNextToken)) { |
| return true; |
| } |
| |
| if (token === null) { |
| return false; |
| } |
| |
| var numberEnd = consumeNumber(token.value, true); |
| if (numberEnd !== token.value.length) { |
| return false; |
| } |
| |
| addTokenToMatch(); |
| return true; |
| } |
| |
| function numberZeroOne(token, addTokenToMatch, getNextToken) { |
| if (calc(token, addTokenToMatch, getNextToken)) { |
| return true; |
| } |
| |
| if (token === null || !isNumber(token.value)) { |
| return false; |
| } |
| |
| var value = Number(token.value); |
| if (value < 0 || value > 1) { |
| return false; |
| } |
| |
| addTokenToMatch(); |
| return true; |
| } |
| |
| function numberOneOrGreater(token, addTokenToMatch, getNextToken) { |
| if (calc(token, addTokenToMatch, getNextToken)) { |
| return true; |
| } |
| |
| if (token === null || !isNumber(token.value)) { |
| return false; |
| } |
| |
| var value = Number(token.value); |
| if (value < 1) { |
| return false; |
| } |
| |
| addTokenToMatch(); |
| return true; |
| } |
| |
| // TODO: fail on 10e-2 |
| function integer(token, addTokenToMatch, getNextToken) { |
| if (calc(token, addTokenToMatch, getNextToken)) { |
| return true; |
| } |
| |
| if (token === null) { |
| return false; |
| } |
| |
| var numberEnd = consumeNumber(token.value, false); |
| if (numberEnd !== token.value.length) { |
| return false; |
| } |
| |
| addTokenToMatch(); |
| return true; |
| } |
| |
| // TODO: fail on 10e-2 |
| function positiveInteger(token, addTokenToMatch, getNextToken) { |
| if (calc(token, addTokenToMatch, getNextToken)) { |
| return true; |
| } |
| |
| if (token === null) { |
| return false; |
| } |
| |
| var numberEnd = findDecimalNumberEnd(token.value, 0); |
| if (numberEnd !== token.value.length || token.value.charCodeAt(0) === HYPHENMINUS) { |
| return false; |
| } |
| |
| addTokenToMatch(); |
| return true; |
| } |
| |
| function hexColor(token, addTokenToMatch) { |
| if (token === null || token.value.charCodeAt(0) !== NUMBERSIGN) { |
| return false; |
| } |
| |
| var length = token.value.length - 1; |
| |
| // valid length is 3, 4, 6 and 8 (+1 for #) |
| if (length !== 3 && length !== 4 && length !== 6 && length !== 8) { |
| return false; |
| } |
| |
| for (var i = 1; i < length; i++) { |
| if (!isHex(token.value.charCodeAt(i))) { |
| return false; |
| } |
| } |
| |
| addTokenToMatch(); |
| return true; |
| } |
| |
| // https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident |
| // https://drafts.csswg.org/css-values-4/#identifier-value |
| function customIdent(token, addTokenToMatch) { |
| if (token === null) { |
| return false; |
| } |
| |
| var identEnd = consumeIdentifier(token.value, 0); |
| if (identEnd !== token.value.length) { |
| return false; |
| } |
| |
| var name = token.value.toLowerCase(); |
| |
| // ยง 3.2. Author-defined Identifiers: the <custom-ident> type |
| // The CSS-wide keywords are not valid <custom-ident>s |
| if (name === 'unset' || name === 'initial' || name === 'inherit') { |
| return false; |
| } |
| |
| // The default keyword is reserved and is also not a valid <custom-ident> |
| if (name === 'default') { |
| return false; |
| } |
| |
| // TODO: ignore property specific keywords (as described https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident) |
| |
| addTokenToMatch(); |
| return true; |
| } |
| |
| module.exports = { |
| 'angle': zeroUnitlessDimension(ANGLE), |
| 'attr()': attr, |
| 'custom-ident': customIdent, |
| 'decibel': dimension(DECIBEL), |
| 'dimension': dimension(), |
| 'frequency': dimension(FREQUENCY), |
| 'flex': dimension(FLEX), |
| 'hex-color': hexColor, |
| 'id-selector': idSelector, // element( <id-selector> ) |
| 'ident': astNode('Identifier'), |
| 'integer': integer, |
| 'length': zeroUnitlessDimension(LENGTH), |
| 'number': number, |
| 'number-zero-one': numberZeroOne, |
| 'number-one-or-greater': numberOneOrGreater, |
| 'percentage': dimension(PERCENTAGE), |
| 'positive-integer': positiveInteger, |
| 'resolution': dimension(RESOLUTION), |
| 'semitones': dimension(SEMITONES), |
| 'string': astNode('String'), |
| 'time': dimension(TIME), |
| 'unicode-range': astNode('UnicodeRange'), |
| 'url': url, |
| |
| // old IE stuff |
| 'progid': astNode('Raw'), |
| 'expression': expression |
| }; |