postcss-ordered-values
Version:
Ensure values are ordered consistently in your CSS.
165 lines (146 loc) • 4.75 kB
JavaScript
import joinGridValue from '../lib/joinGridValue.js';
import {
isIdent,
isNumber,
name,
reservedIdentKeywords,
} from '../lib/tokenize.js';
/**
* @param {import('../lib/tokenize.js').Term[]} gridAutoFlow
* @return {string | null}
*/
const normalizeGridAutoFlow = (gridAutoFlow) => {
const newValue = { front: '', back: '' };
let shouldNormalize = false;
let hasDense = false;
let hasTrack = false;
for (const node of gridAutoFlow) {
const value = node.raw;
const keyword = isIdent(node) ? name(node) : '';
if (keyword === 'dense') {
if (hasDense) return null;
hasDense = true;
shouldNormalize = true;
newValue.back = value;
} else if (['row', 'column'].includes(keyword)) {
if (hasTrack) return null;
hasTrack = true;
shouldNormalize = true;
newValue.front = value;
} else {
return null;
}
}
if (shouldNormalize) {
return [newValue.front.trim(), newValue.back.trim()]
.filter(Boolean)
.join(' ');
}
return null;
};
const gridLineExcludedIdents = new Set([
...reservedIdentKeywords,
'span',
'auto',
]);
/** @param {import('../lib/tokenize.js').Term} term */
function isGridInteger(term) {
if (!isNumber(term)) return false;
const data = /** @type {{ type?: string, value?: number }} */ (
term.tokens[0][4]
);
return (
data.type === 'integer' &&
typeof data.value === 'number' &&
data.value !== 0 &&
Math.abs(data.value) <= Number.MAX_SAFE_INTEGER
);
}
/** @param {import('../lib/tokenize.js').Term} term */
function isGridCustomIdent(term) {
return isIdent(term) && !gridLineExcludedIdents.has(name(term));
}
/** @param {import('../lib/tokenize.js').Term} term */
function classifyGridTerm(term) {
if (isGridInteger(term)) return 'integer';
if (isGridCustomIdent(term)) return 'ident';
if (isIdent(term) && name(term) === 'span') return 'span';
if (isIdent(term) && name(term) === 'auto') return 'auto';
return null;
}
/** @param {import('../lib/tokenize.js').Term[]} line */
function isGridLine(line) {
if (line.length === 0 || line.length > 3) return false;
const kinds = line.map(classifyGridTerm);
if (kinds.includes(null)) return false;
// `auto` is a complete grid-line on its own.
if (kinds.includes('auto')) return line.length === 1;
if (kinds.includes('span')) {
const integerCount = kinds.filter((kind) => kind === 'integer').length;
const identCount = kinds.filter((kind) => kind === 'ident').length;
if (
kinds.filter((kind) => kind === 'span').length !== 1 ||
integerCount > 1 ||
identCount > 1 ||
integerCount + identCount === 0 ||
line.length !== 1 + integerCount + identCount
) {
return false;
}
const integerIndex = kinds.indexOf('integer');
if (integerIndex === -1) return true;
const value = /** @type {{ value: number }} */ (
line[integerIndex].tokens[0][4]
).value;
return value > 0;
}
// Standalone custom-ident line.
if (line.length === 1 && kinds[0] === 'ident') return true;
// integer && custom-ident?, where the ordinary integer may be negative.
return (
line.length <= 2 &&
kinds.filter((kind) => kind === 'integer').length === 1 &&
kinds.every((kind) => kind === 'integer' || kind === 'ident')
);
}
/**
* @param {import('../lib/tokenize.js').Term[]} grid
* @param {number} [maxLines=2] Maximum number of <grid-line>s the property accepts.
* @return {string | null}
*/
const normalizeGridColumnRow = (grid, maxLines = 2) => {
/** @type {import('../lib/tokenize.js').Term[][]} */
const lines = [[]];
for (const term of grid) {
if (term.raw === '/' && term.tokens.length === 1) {
lines.push([]);
} else {
lines[lines.length - 1].push(term);
}
}
// grid-column / grid-row take at most two <grid-line>s; the longhand
// grid-*-start/end properties take only one.
if (lines.length > maxLines) return null;
if (lines.length > 1 && lines.some((line) => line.length === 0)) {
return null;
}
if (!lines.every(isGridLine)) return null;
const normalized = lines.map((line) => {
const span = line.find((term) => isIdent(term) && name(term) === 'span');
if (span) {
const operands = line.filter((term) => term !== span);
return [
span,
...operands.filter(isGridInteger),
...operands.filter(isGridCustomIdent),
];
}
const integer = line.find(isGridInteger);
if (!integer) return line;
return [integer, ...line.filter((term) => term !== integer)];
});
return joinGridValue(
normalized.map((line) => line.map((term) => term.raw).join(' '))
);
};
export { normalizeGridAutoFlow, normalizeGridColumnRow };