Files
deerflow-code/frontend-web/scripts/postcss-firefox52-rgb-vars.mjs
T

165 lines
5.1 KiB
JavaScript

const LEGACY_RGB_NAME = /^--legacy-[a-z0-9-]+-rgb$/i;
const RGB_NAME = /^--[a-z0-9-]+-rgb$/i;
const RGB_CHANNELS = /^\s*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s*$/;
const RGB_CHANNEL_REFERENCE = /^\s*var\(\s*(--[a-z0-9-]+-rgb)(?:\s*,\s*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?))?\s*\)\s*$/i;
const INHERENT_ALPHA_TOKENS = new Set([
"border",
"input",
"ring",
"sidebar-border",
]);
const ALPHA_MULTIPLIERS = [0, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 1];
function alphaVariableSuffix(multiplier) {
return String(Math.round(multiplier * 1000));
}
function formatAlpha(value) {
return String(Math.round(value * 1_000_000) / 1_000_000);
}
function legacyAlphaToken(variableName) {
const match = variableName.match(/^--legacy-(.+)-rgb$/);
return match && INHERENT_ALPHA_TOKENS.has(match[1]) ? match[1] : null;
}
function normalizeAlpha(variableName, alpha) {
const percentage = alpha.match(/^(\d+(?:\.\d+)?)%$/);
if (percentage) return String(Number(percentage[1]) / 100);
const token = legacyAlphaToken(variableName);
if (!token) return alpha;
const composite = alpha.match(
new RegExp(
`^calc\\(var\\(--${token}-alpha,\\s*1\\)\\s*\\*\\s*(.+)\\)$`,
),
);
if (!composite) return alpha;
const multiplier = composite[1].trim();
const numeric = Number(multiplier);
if (Number.isFinite(numeric)) {
return `var(--ff52-${token}-alpha-${alphaVariableSuffix(numeric)})`;
}
if (/^var\(--tw-[a-z-]+-opacity,\s*1\)$/.test(multiplier)) {
return `var(--ff52-${token}-alpha-${alphaVariableSuffix(1)})`;
}
return alpha;
}
function findClosingParenthesis(value, openingIndex) {
let depth = 0;
for (let index = openingIndex; index < value.length; index += 1) {
if (value[index] === "(") depth += 1;
if (value[index] !== ")") continue;
depth -= 1;
if (depth === 0) return index;
}
return -1;
}
function commaChannelValue(value) {
const channels = value.match(RGB_CHANNELS);
if (channels) return `${channels[1]}, ${channels[2]}, ${channels[3]}`;
const reference = value.match(RGB_CHANNEL_REFERENCE);
if (!reference) return null;
const fallback = reference[2]
? `, ${reference[2]}, ${reference[3]}, ${reference[4]}`
: "";
return `var(${reference[1]}-comma${fallback})`;
}
function rewriteLegacyRgbFunctions(
value,
convertibleNames = { has: (name) => LEGACY_RGB_NAME.test(name) },
) {
let output = "";
let cursor = 0;
while (cursor < value.length) {
const start = value.indexOf("rgb(var(--", cursor);
if (start < 0) return output + value.slice(cursor);
output += value.slice(cursor, start);
const varStart = start + 4;
const varEnd = findClosingParenthesis(value, varStart + 3);
const rgbEnd = findClosingParenthesis(value, start + 3);
if (varEnd < 0 || rgbEnd < 0 || varEnd >= rgbEnd) {
output += value.slice(start, start + 4);
cursor = start + 4;
continue;
}
const variableName = value.slice(varStart + 4, varEnd).trim();
const separator = value.slice(varEnd + 1, rgbEnd);
const slash = separator.indexOf("/");
if (!RGB_NAME.test(variableName) || !convertibleNames.has(variableName) || slash < 0) {
output += value.slice(start, rgbEnd + 1);
cursor = rgbEnd + 1;
continue;
}
const alpha = normalizeAlpha(
variableName,
separator.slice(slash + 1).trim(),
);
output += `rgba(var(${variableName}-comma), ${alpha})`;
cursor = rgbEnd + 1;
}
return output;
}
/**
* Firefox 52 cannot parse CSS Color 4's `rgb(var(--channels) / alpha)` form.
* Keep the space-separated live channels for modern builds, add an equivalent
* comma-separated variable, and rewrite only the Firefox artifact to rgba().
*/
export default function firefox52RgbVars() {
const convertibleNames = new Set();
return {
postcssPlugin: "postcss-firefox52-rgb-vars",
Once(root) {
root.walkDecls((declaration) => {
if (RGB_NAME.test(declaration.prop)) {
const commaValue = commaChannelValue(declaration.value);
if (commaValue) {
convertibleNames.add(declaration.prop);
declaration.cloneAfter({
prop: `${declaration.prop}-comma`,
value: commaValue,
});
const token = legacyAlphaToken(declaration.prop);
if (token) {
const alphaDeclaration = declaration.parent?.nodes?.find(
(node) =>
node.type === "decl" && node.prop === `--${token}-alpha`,
);
const inherentAlpha = Number(alphaDeclaration?.value ?? 1);
for (const multiplier of ALPHA_MULTIPLIERS) {
declaration.cloneAfter({
prop: `--ff52-${token}-alpha-${alphaVariableSuffix(multiplier)}`,
value: formatAlpha(inherentAlpha * multiplier),
});
}
}
}
}
});
},
Declaration(declaration) {
if (!declaration.value.includes("rgb(var(--")) return;
declaration.value = rewriteLegacyRgbFunctions(
declaration.value,
convertibleNames,
);
},
};
}
firefox52RgbVars.postcss = true;
export { rewriteLegacyRgbFunctions };