juice icon indicating copy to clipboard operation
juice copied to clipboard

Support for CSS Custom Variables

Open mjgchase opened this issue 7 years ago • 6 comments

Would be nice to support and convert css custom variables https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables

mjgchase avatar Sep 05 '18 07:09 mjgchase

Since email clients have generally poor support for CSS variables, it would be nice if Juice would inline them using getComputedStyle.

NC-piercej avatar Nov 09 '20 15:11 NC-piercej

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.

benaltair avatar Mar 23 '21 18:03 benaltair

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 avatar Oct 06 '21 18:10 huksley

@huksley would love to take a PR from you if you have time to do it!

jrit avatar Oct 06 '21 18:10 jrit

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);
}

shaunpersad avatar Dec 24 '21 02:12 shaunpersad