You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

607 lines
17 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { cssWideKeywords } from './generic-const.js';
import anPlusB from './generic-an-plus-b.js';
import urange from './generic-urange.js';
import {
isIdentifierStart,
isHexDigit,
isDigit,
cmpStr,
consumeNumber,
Ident,
Function as FunctionToken,
AtKeyword,
Hash,
String as StringToken,
BadString,
Url,
BadUrl,
Delim,
Number as NumberToken,
Percentage,
Dimension,
WhiteSpace,
CDO,
CDC,
Colon,
Semicolon,
Comma,
LeftSquareBracket,
RightSquareBracket,
LeftParenthesis,
RightParenthesis,
LeftCurlyBracket,
RightCurlyBracket
} from '../tokenizer/index.js';
const calcFunctionNames = ['calc(', '-moz-calc(', '-webkit-calc('];
const balancePair = new Map([
[FunctionToken, RightParenthesis],
[LeftParenthesis, RightParenthesis],
[LeftSquareBracket, RightSquareBracket],
[LeftCurlyBracket, RightCurlyBracket]
]);
// safe char code getter
function charCodeAt(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 (let 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 (
charCodeAt(str, offset) === 0x005C && // U+005C REVERSE SOLIDUS (\)
isDigit(charCodeAt(str, offset + 1))
);
}
function outOfRange(opts, value, numEnd) {
if (opts && opts.type === 'Range') {
const num = Number(
numEnd !== undefined && numEnd !== value.length
? value.substr(0, numEnd)
: value
);
if (isNaN(num)) {
return true;
}
// FIXME: when opts.min is a string it's a dimension, skip a range validation
// for now since it requires a type covertation which is not implmented yet
if (opts.min !== null && num < opts.min && typeof opts.min !== 'string') {
return true;
}
// FIXME: when opts.max is a string it's a dimension, skip a range validation
// for now since it requires a type covertation which is not implmented yet
if (opts.max !== null && num > opts.max && typeof opts.max !== 'string') {
return true;
}
}
return false;
}
function consumeFunction(token, getNextToken) {
let balanceCloseType = 0;
let balanceStash = [];
let length = 0;
// balanced token consuming
scan:
do {
switch (token.type) {
case RightCurlyBracket:
case RightParenthesis:
case RightSquareBracket:
if (token.type !== balanceCloseType) {
break scan;
}
balanceCloseType = balanceStash.pop();
if (balanceStash.length === 0) {
length++;
break scan;
}
break;
case FunctionToken:
case LeftParenthesis:
case LeftSquareBracket:
case LeftCurlyBracket:
balanceStash.push(balanceCloseType);
balanceCloseType = balancePair.get(token.type);
break;
}
length++;
} 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 === FunctionToken && 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;
};
}
// =========================
// 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 propertys value definition.
//
// See also: https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident
function customIdent(token) {
if (token === null || token.type !== Ident) {
return 0;
}
const 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 propertys 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: its 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 !== Ident) {
return 0;
}
// ... that starts with two dashes (U+002D HYPHEN-MINUS)
if (charCodeAt(token.value, 0) !== 0x002D || charCodeAt(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 doesnt matter - #00ff00 is identical to #00FF00).
function hexColor(token) {
if (token === null || token.type !== Hash) {
return 0;
}
const 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 (let i = 1; i < length; i++) {
if (!isHexDigit(charCodeAt(token.value, i))) {
return 0;
}
}
return 1;
}
function idSelector(token) {
if (token === null || token.type !== Hash) {
return 0;
}
if (!isIdentifierStart(charCodeAt(token.value, 1), charCodeAt(token.value, 2), charCodeAt(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;
}
let balanceCloseType = 0;
let balanceStash = [];
let length = 0;
// The <declaration-value> production matches any sequence of one or more tokens,
// so long as the sequence does not contain ...
scan:
do {
switch (token.type) {
// ... <bad-string-token>, <bad-url-token>,
case BadString:
case BadUrl:
break scan;
// ... unmatched <)-token>, <]-token>, or <}-token>,
case RightCurlyBracket:
case RightParenthesis:
case RightSquareBracket:
if (token.type !== balanceCloseType) {
break scan;
}
balanceCloseType = balanceStash.pop();
break;
// ... or top-level <semicolon-token> tokens
case Semicolon:
if (balanceCloseType === 0) {
break scan;
}
break;
// ... or <delim-token> tokens with a value of "!"
case Delim:
if (balanceCloseType === 0 && token.value === '!') {
break scan;
}
break;
case FunctionToken:
case LeftParenthesis:
case LeftSquareBracket:
case LeftCurlyBracket:
balanceStash.push(balanceCloseType);
balanceCloseType = balancePair.get(token.type);
break;
}
length++;
} 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;
}
let balanceCloseType = 0;
let balanceStash = [];
let 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 BadString:
case BadUrl:
break scan;
// ... unmatched <)-token>, <]-token>, or <}-token>,
case RightCurlyBracket:
case RightParenthesis:
case RightSquareBracket:
if (token.type !== balanceCloseType) {
break scan;
}
balanceCloseType = balanceStash.pop();
break;
case FunctionToken:
case LeftParenthesis:
case LeftSquareBracket:
case LeftCurlyBracket:
balanceStash.push(balanceCloseType);
balanceCloseType = balancePair.get(token.type);
break;
}
length++;
} while (token = getNextToken(length));
return length;
}
// =========================
// Dimensions
//
function dimension(type) {
if (type) {
type = new Set(type);
}
return function(token, getNextToken, opts) {
if (token === null || token.type !== Dimension) {
return 0;
}
const numberEnd = consumeNumber(token.value, 0);
// check unit
if (type !== null) {
// check for IE postfix hack, i.e. 123px\0 or 123px\9
const reverseSolidusOffset = token.value.indexOf('\\', numberEnd);
const unit = reverseSolidusOffset === -1 || !isPostfixIeHack(token.value, reverseSolidusOffset)
? token.value.substr(numberEnd)
: token.value.substring(numberEnd, reverseSolidusOffset);
if (type.has(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 !== 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 === NumberToken) {
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;
}
const numberEnd = consumeNumber(token.value, 0);
const 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 !== NumberToken) {
return 0;
}
// The first digit of an integer may be immediately preceded by `-` or `+` to indicate the integers sign.
let i = charCodeAt(token.value, 0) === 0x002B || // U+002B PLUS SIGN (+)
charCodeAt(token.value, 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(charCodeAt(token.value, i))) {
return 0;
}
}
// check range if specified
if (outOfRange(opts, token.value, i)) {
return 0;
}
return 1;
}
// token types
export const tokenTypes = {
'ident-token': tokenType(Ident),
'function-token': tokenType(FunctionToken),
'at-keyword-token': tokenType(AtKeyword),
'hash-token': tokenType(Hash),
'string-token': tokenType(StringToken),
'bad-string-token': tokenType(BadString),
'url-token': tokenType(Url),
'bad-url-token': tokenType(BadUrl),
'delim-token': tokenType(Delim),
'number-token': tokenType(NumberToken),
'percentage-token': tokenType(Percentage),
'dimension-token': tokenType(Dimension),
'whitespace-token': tokenType(WhiteSpace),
'CDO-token': tokenType(CDO),
'CDC-token': tokenType(CDC),
'colon-token': tokenType(Colon),
'semicolon-token': tokenType(Semicolon),
'comma-token': tokenType(Comma),
'[-token': tokenType(LeftSquareBracket),
']-token': tokenType(RightSquareBracket),
'(-token': tokenType(LeftParenthesis),
')-token': tokenType(RightParenthesis),
'{-token': tokenType(LeftCurlyBracket),
'}-token': tokenType(RightCurlyBracket)
};
// token production types
export const productionTypes = {
// token type aliases
'string': tokenType(StringToken),
'ident': tokenType(Ident),
// percentage
'percentage': calc(percentage),
// numeric
'zero': zero(),
'number': calc(number),
'integer': calc(integer),
// 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
};
export const unitGroups = [
'length',
'angle',
'time',
'frequency',
'resolution',
'flex',
'decibel',
'semitones'
];
// dimensions types depend on units set
export function createDemensionTypes(units) {
const {
angle,
decibel,
frequency,
flex,
length,
resolution,
semitones,
time
} = units || {};
return {
'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))
};
}
export function createGenericTypes(units) {
return {
...tokenTypes,
...productionTypes,
...createDemensionTypes(units)
};
};