Support for CSS Custom Variables
mjgchase opened this issue · comments
Would be nice to support and convert css custom variables
https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables
Since email clients have generally poor support for CSS variables, it would be nice if Juice would inline them using getComputedStyle
.
I agree - passing on the variable doesn't work, so computing it and inlining the computed value would be stellar. I have tested variables and it didn't work as expected, with missing styles on the email client side.
Any ETA on implementing this? Very important as I use components and have colors, borders, etc defined once and used everywhere, including email templates.
@huksley would love to take a PR from you if you have time to do it!
Here's a solution for replacing the CSS variables after juice is done, using cheerio:
const variableDefRegex = /^(--[a-zA-Z0-9-_]+)$/;
const variableUsageRegex = /var\((--[a-zA-Z0-9-_]+)(?:\)|,\s*(.*)\))/;
/**
* Resolves any variable usages found in a style value to their definitions.
*/
export function resolveCSSVariables(defs: Map<string, string>, styleValue = ''): string {
let match;
while (match = styleValue.match(variableUsageRegex)) {
const [found, variableName, fallback] = match;
const { index } = match;
const before = styleValue.slice(0, index);
const after = styleValue.slice(index! + found.length);
const replacement = defs.has(variableName) ? defs.get(variableName) : fallback;
styleValue = `${before}${resolveCSSVariables(defs, replacement || '')}${after}`;
}
return styleValue;
}
/**
* Walks the tree depth-first,
* collecting variable definitions and then resolving any usages found.
*/
export function replaceCSSVariables($: CheerioAPI, $el : Cheerio<any>, defs = new Map<string, string>()) {
const styles = $el.css();
if (styles) {
/**
* Collect the defs first.
*/
Object.entries(styles).forEach(([key, value]) => {
if (variableDefRegex.test(key)) {
defs.set(key, value);
}
});
/**
* Resolve any usages.
* Done separately from above in case any defs and usages are found on the same element.
*/
Object.entries(styles).forEach(([key, value]) => {
styles[key] = resolveCSSVariables(defs, value);
});
$el.css(styles);
}
$el.children().each(
(index, el) => replaceCSSVariables($, $(el), new Map<string, string>(defs)),
);
}
export default function inlineWithVariablesReplaced(html: string, css: string): string {
const inlined = juice.inlineContent(html, css, { inlinePseudoElements: true });
const $ = cheerio.load(inlined, null, false);
const $root = $.root();
replaceCSSVariables($, $root);
return cheerio.html($root);
}
Here's a solution for replacing the CSS variables after juice is done, using cheerio:
const variableDefRegex = /^(--[a-zA-Z0-9-_]+)$/; const variableUsageRegex = /var\((--[a-zA-Z0-9-_]+)(?:\)|,\s*(.*)\))/; /** * Resolves any variable usages found in a style value to their definitions. */ export function resolveCSSVariables(defs: Map<string, string>, styleValue = ''): string { let match; while (match = styleValue.match(variableUsageRegex)) { const [found, variableName, fallback] = match; const { index } = match; const before = styleValue.slice(0, index); const after = styleValue.slice(index! + found.length); const replacement = defs.has(variableName) ? defs.get(variableName) : fallback; styleValue = `${before}${resolveCSSVariables(defs, replacement || '')}${after}`; } return styleValue; } /** * Walks the tree depth-first, * collecting variable definitions and then resolving any usages found. */ export function replaceCSSVariables($: CheerioAPI, $el : Cheerio<any>, defs = new Map<string, string>()) { const styles = $el.css(); if (styles) { /** * Collect the defs first. */ Object.entries(styles).forEach(([key, value]) => { if (variableDefRegex.test(key)) { defs.set(key, value); } }); /** * Resolve any usages. * Done separately from above in case any defs and usages are found on the same element. */ Object.entries(styles).forEach(([key, value]) => { styles[key] = resolveCSSVariables(defs, value); }); $el.css(styles); } $el.children().each( (index, el) => replaceCSSVariables($, $(el), new Map<string, string>(defs)), ); } export default function inlineWithVariablesReplaced(html: string, css: string): string { const inlined = juice.inlineContent(html, css, { inlinePseudoElements: true }); const $ = cheerio.load(inlined, null, false); const $root = $.root(); replaceCSSVariables($, $root); return cheerio.html($root); }
This is great. Am I correct in understanding, though, that if the variable is defined in various classes that apply to an element, this will just use the most-recently-encountered variable definition that matches, rather than the definition from the highest-priority class?