@nvl/postcss-egal
Version:
PostCSS plugin to simplify uniformity in color saturation.
177 lines (175 loc) • 5.52 kB
JavaScript
import { egal } from '@nvl/egal';
import { regex } from 'regex';
import MagicString from 'magic-string';
const cssAngleUnitsToDegrees = {
deg: 1,
grad: 360 / 400,
rad: 180 / Math.PI,
turn: 360,
};
const gamutRegexes = {
srgb: /s?rgb/iu,
p3: /p3/iu,
rec2020: /rec\.?2020/iu,
};
const falseMatchRegex = regex('gim') `
(?<![^\s:;,\(])
egal
\(
`;
const realMatchRegex = regex('gim') `
(?<![^\s:;,\(])
egal
\s* \( \s*
(?<lightness> \g<float> | none )
(?<lightness_percent> (?
)
)?
\s* \)
(?(DEFINE)
(?<float> [\+\-]?\d+\.? | [\+\-]?\d*\.\d+ )
(?<sep> \s*? [,\s] \s* )
(?<escaped_single_quote> (?<! \\ ) \\ (\\\\)* ' )
(?<nonescaped_single_quote> (?<! \\ ) (\\\\)* ' )
)
`;
export const transformer = (decl, { result }, otherPluginOptions) => {
let value = decl.value;
const s = new MagicString(value);
let realMatch;
let falseMatch;
// First pass, where stuff is actually modified
while ((realMatch = realMatchRegex.exec(value))) {
processRealMatch({ magicString: s, realMatch, otherPluginOptions });
}
value = s.toString();
decl.value = value;
while ((falseMatch = falseMatchRegex.exec(value))) {
processFalseMatch({
value,
falseMatch,
otherPluginOptions,
decl,
result,
});
}
};
function processFalseMatch({ value, falseMatch, decl, result, }) {
var _a, _b;
let index = falseMatch.index;
let endIndex = indexOfClosingParenthesis(value, value.indexOf('(', index)) + 1;
// I'm not sure how `decl.raws.between` would ever be `undefined`, so I'm
// choosing to exclude it from the test coverage report.
/* v8 ignore next 1 */
const shift = decl.prop.length + ((_b = (_a = decl.raws.between) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0);
const problemString = value.slice(index, endIndex);
for (const [ppr, message] of potentialProblems) {
const match = ppr.exec(problemString);
if (match) {
index += match.index;
endIndex = index + match[0].length;
index += shift;
endIndex += shift;
decl.warn(result, message, { index, endIndex });
return;
}
}
index += shift;
endIndex += shift;
decl.warn(result, "Couldn't parse egal color", { index, endIndex });
}
const potentialProblems = [
[/none\s*%/u, 'none% is not a valid value'],
[
/(\d+\.?\d*|\d*\.\d+)\s+%/u,
'Whitespace between number and % is forbidden',
],
[
regex('i') `['"](
${gamutRegexes.srgb}|${gamutRegexes.p3}|${gamutRegexes.rec2020}
)['"]`,
"Don't use quotes around gamut",
],
[regex('i') `"\{.*\}"`, 'Use single quotes around JSON object'],
];
export function indexOfClosingParenthesis(str, indexOfOpeningParenthesis) {
let depth = 0;
for (let i = indexOfOpeningParenthesis + 1; i < str.length; i++) {
if (str[i] === '(') {
depth++;
}
else if (str[i] === ')') {
if (depth === 0) {
return i;
}
depth--;
}
}
return -1; // No closing parenthesis found
}
function processRealMatch({ magicString, realMatch, otherPluginOptions, }) {
const { lightness, lightness_percent, chroma, chroma_percent, hue, hue_unit, alpha, alpha_percent, gamut, json, } = realMatch.groups;
let l = lightness === 'none' ? 0 : parseFloat(lightness);
if (lightness_percent)
l /= 100;
let c = chroma === 'none' ? 0 : parseFloat(chroma);
if (chroma_percent)
c /= 100;
let h = hue === 'none' ? 0 : parseFloat(hue);
if (hue_unit)
h *= cssAngleUnitsToDegrees[hue_unit];
let overrideOptions = {};
if (json) {
try {
overrideOptions = JSON.parse(json);
}
catch (_a) {
return;
}
}
if (alpha) {
let a = alpha === 'none' ? 0 : parseFloat(alpha);
if (alpha_percent)
a /= 100;
overrideOptions.opacity = a;
}
if (gamut) {
const g = Object.keys(gamutRegexes).find((key) => gamutRegexes[key].test(gamut));
if (g)
overrideOptions.gamut = g;
}
magicString.overwrite(realMatch.index, realMatch.index + realMatch[0].length, egal(l, c, h, {
...otherPluginOptions,
...overrideOptions,
}));
}