@nvl/lightningcss-plugin-egal
Version:
Lightning CSS plugin to simplify uniformity in color saturation.
182 lines (181 loc) • 6.25 kB
JavaScript
/* eslint-disable tsdoc/syntax */
/**
* @packageDocumentation
* The `@nvl/lightningcss-plugin-egal` module has just one non-type export (a
* default export, in fact), which is a Lightning CSS visitor (i.e., plugin)
* that replaces `egal` colors with their equivalent CSS color.
*
* @module egalVisitor
*/
/* eslint-enable tsdoc/syntax */
import { egal } from '@nvl/egal';
/**
* A visitor (i.e., plugin) for Lightning CSS that transforms calls to the
* `egal` CSS function into the output of the corresponding `egal` function
* call.
*
* @param options - Default options for the egal function.
* @returns A visitor that transforms `egal` function calls in CSS.
*/
function egalVisitor(options) {
return {
Function: {
egal(fn) {
const args = astArgumentsToEgalArguments(fn.arguments);
if (args) {
if (options)
args[3] = { ...options, ...args[3] };
return { raw: egal(...args) };
}
else {
return undefined;
}
},
},
};
}
function astArgumentsToEgalArguments(args) {
let pos = 0;
const next = () => args[pos++];
const peek = () => args[pos];
// Parse required tokens: lightness, chroma, hue.
const lightness = parseNumberOrPercentageOrNone(next());
const sep1 = next();
const chroma = parseNumberOrPercentageOrNone(next());
const sep2 = next();
const hue = parseHue(next());
if (lightness === null ||
chroma === null ||
hue === null ||
!isWhitespaceOrComma(sep1) ||
!isWhitespaceOrComma(sep2)) {
return null;
}
let overrideOptions = {};
// Process remaining tokens for optional arguments
while (pos < args.length) {
const token = next();
if (isSlash(token)) {
// The slash indicates that the next token is opacity.
const opacityToken = next();
const opacity = parseNumberOrPercentageOrNone(opacityToken);
if (opacity === null)
return null;
overrideOptions.opacity = opacity;
}
else if (isWhitespaceOrComma(token)) {
// Look ahead at the next token to decide if it's a gamut or a JSON options string.
const candidate = next();
const gamut = parseGamut(candidate);
if (gamut) {
overrideOptions.gamut = gamut;
// Optionally, check if there is a following JSON options segment
if (peek() && isWhitespaceOrComma(peek())) {
// Consume the delimiter and then try parsing JSON options
next();
const json = parseJsonOptions(next());
if (json) {
overrideOptions = mergeJsonOptions(json, overrideOptions);
}
}
}
else {
// If the candidate isn’t a gamut then it might be JSON
const json = parseJsonOptions(candidate);
if (json) {
overrideOptions = mergeJsonOptions(json, overrideOptions);
}
}
}
else {
// Unrecognized token pattern in the optional arguments.
return null;
}
}
return [lightness, chroma, hue, overrideOptions];
}
const gamutRegexes = {
srgb: /s?rgb/iu,
p3: /p3/iu,
rec2020: /rec\.?2020/iu,
};
function mergeJsonOptions(json, overrideOptions) {
const { gamut, opacity, ...jsonWithoutGamutAndOpacity } = json;
const opts = { ...overrideOptions };
// Only merge 'opacity' if not already set by a directly parsed value.
if (opacity !== undefined && overrideOptions.opacity === undefined) {
opts.opacity = opacity;
}
// Only merge 'gamut' if not already set by a directly parsed value.
if (gamut !== undefined && overrideOptions.gamut === undefined) {
opts.gamut = gamut;
}
return { ...opts, ...jsonWithoutGamutAndOpacity };
}
function parseGamut(token) {
if ((token === null || token === void 0 ? void 0 : token.type) === 'token' && token.value.type === 'ident') {
const gamut = token.value.value;
for (const [key, regex] of Object.entries(gamutRegexes)) {
if (regex.test(gamut))
return key;
}
}
return null;
}
function parseJsonOptions(token) {
if ((token === null || token === void 0 ? void 0 : token.type) === 'token' && token.value.type === 'string') {
const json = token.value.value;
try {
return JSON.parse(json);
}
catch (e) {
console.error('Invalid JSON in egal function:', e);
return null;
}
}
return null;
}
const cssAngleUnitsToDegrees = {
deg: 1,
grad: 360 / 400,
rad: 180 / Math.PI,
turn: 360,
};
function parseNumberOrPercentageOrNone(token) {
if (!token)
return null;
if (token.type === 'token' &&
(token.value.type === 'number' || token.value.type === 'percentage')) {
return token.value.value;
}
else if (token.type === 'token' && token.value.type === 'ident') {
if (token.value.value === 'none')
return 0;
}
return null;
}
function parseHue(token) {
if (!token)
return null;
if (token.type === 'token' && token.value.type === 'number') {
return token.value.value;
}
else if (token.type === 'angle') {
return token.value.value * cssAngleUnitsToDegrees[token.value.type];
}
else if (token.type === 'token' && token.value.type === 'ident') {
if (token.value.value === 'none')
return 0;
}
return null;
}
function isWhitespaceOrComma(token) {
return ((token === null || token === void 0 ? void 0 : token.type) === 'token' &&
(token.value.type === 'white-space' || token.value.type === 'comma'));
}
function isSlash(token) {
return ((token === null || token === void 0 ? void 0 : token.type) === 'token' &&
token.value.type === 'delim' &&
token.value.value === '/');
}
export default egalVisitor;