juice
juice copied to clipboard
Support for CSS Custom Variables
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);
}