postcss-ordered-values
Version:
Ensure values are ordered consistently in your CSS.
215 lines (191 loc) • 5.63 kB
JavaScript
import cssnanoUtils from 'cssnano-utils';
import cssWideKeywords from '../rules/cssWideKeywords.json' with { type: 'json' };
const {
TokenType,
asciiLowerCase,
closeForOpening,
decoded,
tokenEnd,
tokenStart,
tokens,
} = cssnanoUtils;
/**
* A raw top-level component. Its source is intentionally never reserialized:
* CSS escapes and malformed-but-tokenizable input must survive a rewrite.
*
* @typedef {{ raw: string, tokens: import('@csstools/css-tokenizer').CSSToken[] }} Term
*/
/** @param {Term} term */
function name(term) {
const token = term.tokens[0];
if (
!token ||
(token[0] !== TokenType.Ident && token[0] !== TokenType.Function)
) {
return '';
}
return asciiLowerCase(decoded(token) ?? '');
}
/** @param {string} value @param {string} expected */
function equalsAsciiCaseInsensitive(value, expected) {
if (value.length !== expected.length) return false;
for (let index = 0; index < value.length; index++) {
let code = value.charCodeAt(index);
if (code >= 65 && code <= 90) code += 32;
if (code !== expected.charCodeAt(index)) return false;
}
return true;
}
/** @param {import('@csstools/css-tokenizer').CSSToken} token */
function isVariableFunction(token) {
if (token[0] !== TokenType.Function) return false;
const value = decoded(token);
if (!value) return false;
return (
equalsAsciiCaseInsensitive(value, 'var') ||
equalsAsciiCaseInsensitive(value, 'env') ||
equalsAsciiCaseInsensitive(value, 'constant')
);
}
/** @param {Term} term */
function isDimension(term) {
return term.tokens.length === 1 && term.tokens[0][0] === TokenType.Dimension;
}
/** @param {Term} term */
function isNumber(term) {
return term.tokens.length === 1 && term.tokens[0][0] === TokenType.Number;
}
/** @param {Term} term */
function isFunction(term) {
return term.tokens[0]?.[0] === TokenType.Function;
}
/** @param {Term} term */
function isUrl(term) {
return (
(term.tokens.length === 1 && term.tokens[0][0] === TokenType.URL) ||
(isFunction(term) && name(term) === 'url')
);
}
/** @param {Term} term */
function isIdent(term) {
return term.tokens.length === 1 && term.tokens[0][0] === TokenType.Ident;
}
/** @param {Term} term */
function isHash(term) {
return term.tokens.length === 1 && term.tokens[0][0] === TokenType.Hash;
}
/** @param {Term} term */
function isString(term) {
return term.tokens.length === 1 && term.tokens[0][0] === TokenType.String;
}
/** @param {Term} term */
function isPercentage(term) {
return term.tokens.length === 1 && term.tokens[0][0] === TokenType.Percentage;
}
/** @param {Term} term @param {string} [char] */
function isDelim(term, char) {
if (term.tokens.length !== 1 || term.tokens[0][0] !== TokenType.Delim) {
return false;
}
return char === undefined || term.tokens[0][1] === char;
}
/**
* Tokenize a declaration in a single streaming pass and split it at top-level
* whitespace, commas, and structural slashes.
*
* @param {string} value
* @return {{ arguments: Term[][], terms: Term[], abort: boolean, value: string }}
*/
function tokenizeValue(value) {
/** @type {Term[][]} */
const argumentsList = [[]];
/** @type {Term[]} */
const terms = [];
/** @type {import('@csstools/css-tokenizer').CSSToken[]} */
let current = [];
/** @type {string[]} */
const stack = [];
let abort = false;
const hasCssLoaderImport = value.includes('___CSS_LOADER_IMPORT___');
const pushTerm = () => {
if (current.length === 0) return;
const term = {
raw: value.slice(
tokenStart(current[0]),
tokenEnd(current[current.length - 1])
),
tokens: current,
};
argumentsList[argumentsList.length - 1].push(term);
terms.push(term);
current = [];
};
for (const token of tokens(value)) {
const type = token[0];
if (
type === TokenType.Comment ||
type === TokenType.BadString ||
type === TokenType.BadURL ||
isVariableFunction(token) ||
(hasCssLoaderImport && token[1].includes('___CSS_LOADER_IMPORT___'))
) {
abort = true;
}
if (stack.length === 0) {
if (type === TokenType.Whitespace) {
pushTerm();
continue;
}
// Keep slash delimiters available to grammar-aware consumers. A slash
// inside a function or block remains part of that single nested term.
if (type === TokenType.Delim && token[1] === '/') {
pushTerm();
current.push(token);
pushTerm();
continue;
}
if (type === TokenType.Comma) {
pushTerm();
argumentsList.push([]);
continue;
}
}
current.push(token);
const expected = closeForOpening(type);
if (expected !== undefined) {
stack.push(expected);
} else if (
type === TokenType.CloseParen ||
type === TokenType.CloseSquare ||
type === TokenType.CloseCurly
) {
if (stack.pop() !== type) abort = true;
}
}
pushTerm();
if (stack.length > 0) abort = true;
return { arguments: argumentsList, terms, abort, value };
}
/** @param {Term[][]} arguments_ */
function serializeArguments(arguments_) {
return arguments_
.map((terms) => terms.map((term) => term.raw).join(' '))
.join(',');
}
/* `default` is reserved for custom identifiers although it is not CSS-wide. */
const reservedIdentKeywords = new Set([...cssWideKeywords.keywords, 'default']);
export {
reservedIdentKeywords,
isDelim,
isDimension,
isFunction,
isHash,
isIdent,
isNumber,
isPercentage,
isString,
isUrl,
name,
serializeArguments,
tokenizeValue,
};