postcss-ordered-values
Version:
Ensure values are ordered consistently in your CSS.
189 lines (175 loc) • 5.64 kB
JavaScript
import {
isFunction,
isIdent,
isNumber,
isString,
isUrl,
name,
reservedIdentKeywords,
serializeArguments,
} from '../lib/tokenize.js';
import { easingKeywords, easingFunctionNames } from '../lib/easingSets.js';
import classifyTime from '../lib/isTime.js';
// animation: [ none | <keyframes-name> ] || <time> || <single-timing-function> || <time> || <single-animation-iteration-count> || <single-animation-direction> || <single-animation-fill-mode> || <single-animation-play-state>
const timingFunctions = new Set([...easingFunctionNames, 'frames']);
const timingKeywords = easingKeywords;
const directions = new Set([
'normal',
'reverse',
'alternate',
'alternate-reverse',
]);
const fillModes = new Set(['none', 'forwards', 'backwards', 'both']);
const playStates = new Set(['running', 'paused']);
/**
* @param {string} value
* @param {import('../lib/tokenize.js').Term} node
* @return {boolean}
*/
const isTimingFunction = (value, node) => {
return (
(isFunction(node) && timingFunctions.has(value)) ||
(isIdent(node) && timingKeywords.has(value))
);
};
/** @param {string} value @param {import('../lib/tokenize.js').Term} node */
const isDirection = (value, node) => {
return isIdent(node) && directions.has(value);
};
/** @param {string} value @param {import('../lib/tokenize.js').Term} node */
const isFillMode = (value, node) => {
return isIdent(node) && fillModes.has(value);
};
/** @param {string} value @param {import('../lib/tokenize.js').Term} node */
const isPlayState = (value, node) => {
return isIdent(node) && playStates.has(value);
};
/**
* A non-negative count per CSS Animations 1: <number [0,∞]> | infinite.
* Negative numbers are not iteration counts and fail closed via the name slot.
*
* @param {string} value
* @param {import('../lib/tokenize.js').Term} node
* @return {boolean}
*/
const isIterationCount = (value, node) => {
if (isIdent(node) && value === 'infinite') return true;
if (!isNumber(node)) return false;
const { value: count, signCharacter } =
/** @type {{value: number, signCharacter?: string}} */ (node.tokens[0][4]);
return signCharacter !== '-' && count >= 0;
};
/**
* @param {import('../lib/tokenize.js').Term} node
* @param {string} value
* @param {boolean} hasIdentOrStringName
* @return {boolean}
*/
function isInvalidAnimationName(node, value, hasIdentOrStringName) {
if (!isIdent(node) && !isString(node) && !isFunction(node)) {
return true;
}
if (isUrl(node)) {
return true;
}
if ((isIdent(node) || isString(node)) && hasIdentOrStringName) {
return true;
}
return isIdent(node) && reservedIdentKeywords.has(value);
}
const stateConditions = [
{ property: 'timingFunction', delegate: isTimingFunction },
{ property: 'iterationCount', delegate: isIterationCount },
{ property: 'direction', delegate: isDirection },
{ property: 'fillMode', delegate: isFillMode },
{ property: 'playState', delegate: isPlayState },
];
/**
* A name `none` swaps harmlessly only with a fill mode that is also `none`.
*
* @param {import('../lib/tokenize.js').Term | undefined} term
* @param {import('../lib/tokenize.js').Term | undefined} fillMode
* @return {boolean}
*/
function isShadowedName(term, fillMode) {
if (!term || !isIdent(term)) return false;
const value = name(term);
if (value === 'none') {
return fillMode !== undefined && name(fillMode) !== 'none';
}
return stateConditions.some(({ delegate }) => delegate(value, term));
}
/**
* @param {import('../lib/tokenize.js').Term[][]} args
* @return {import('../lib/tokenize.js').Term[][] | null}
*/
function normalize(args) {
const list = [];
for (const arg of args) {
/** @type {Record<string, import('../lib/tokenize.js').Term[]>} */
const state = {
name: [],
duration: [],
timingFunction: [],
delay: [],
iterationCount: [],
direction: [],
fillMode: [],
playState: [],
};
let hasIdentOrStringName = false;
for (const node of arg) {
const value = name(node);
const time = classifyTime(node);
if (time.isMath && time.dimension !== 'time') return null;
if (time.dimension === 'time') {
if (!state.duration.length && time.isNonNegative) {
state.duration.push(node);
} else if (state.duration.length && !state.delay.length) {
state.delay.push(node);
} else {
return null;
}
continue;
}
const hasMatch = stateConditions.some(({ property, delegate }) => {
if (delegate(value, node) && !state[property].length) {
state[property].push(node);
return true;
} else {
return false;
}
});
if (!hasMatch) {
if (isInvalidAnimationName(node, value, hasIdentOrStringName)) {
return null;
}
if (isIdent(node) || isString(node)) {
hasIdentOrStringName = true;
}
state.name.push(node);
}
}
if (isShadowedName(state.name[0], state.fillMode[0])) return null;
list.push([
...state.name,
...state.duration,
...state.timingFunction,
...state.delay,
...state.iterationCount,
...state.direction,
...state.fillMode,
...state.playState,
]);
}
return list;
}
/**
* @param {{ arguments: import('../lib/tokenize.js').Term[][] }} parsed
* @return {string | null}
*/
function normalizeAnimation(parsed) {
const normalized = normalize(parsed.arguments);
return normalized === null ? null : serializeArguments(normalized);
}
export default normalizeAnimation;