MyIntervals / emogrifier

Converts CSS styles into inline style attributes in your HTML code.
https://www.myintervals.com/emogrifier.php
MIT License
906 stars 154 forks source link

Support to convert CSS Variables? #1276

Closed njt1982 closed 1 month ago

njt1982 commented 2 months ago

Hi,

Is it already possible to convert CSS variables into inline values?

For example...

body {
  --brand: #ff0000;
}
.bg-brand {
  background: var(--brand);
}
.link-color {
  color: var(--brand);
}
td.container {
  border-color: var(--brand);
}

(this is a simplified example!).

For now, I'm going to change my code to replace var(--brand) with something like [BRAND-COLOR] and then string replace that with the hex code...

I'm only asking as I already have CSS elsewhere that defines these brand colour variables and it would be nice to reuse that rather than repeat the same variables in different languages in different places :)

(PS Thank for all the work on this package!)

oliverklee commented 2 months ago

Hi! No, at the moment, this library does not convert CSS variables into inline values (yet). I’m also not sure this should happen at the parser level, though, or whether this should be the responsibility of the code taking the syntax tree (the result from parsing the CSS).

@sabberworm @JakeQZ What are your thoughts on this?

JakeQZ commented 2 months ago

The evaluation of CSS variables is based on the DOM tree. The value is taken from self-or-closest-ancestor where the variable is effectively a property defined according to normal CSS precedence rules.

So I think to support this, we need

Then there's non-inlinable CSS (like @media and :hover). The best we can do for that is to include all the variable definitions that might apply (to begin with we can include all, then, as an enhancement, filter out those that aren't needed).

Some refactoring might not go amiss first, though I'm not sure where or how. Alternatively, we go ahead, and review later where there seems to be common duplicated functionality.

JakeQZ commented 2 months ago

There will be caveats if we do this. E.g.

:root {
  --font-size: 16px;
}
@media (max-width: 640px) {
  :root {
    --font-size: 14px;
  }
}
body {
  font-size: var(--font-size);
}

If we copy through the actual value of the --font-size variable to the inline style, the @media rule will not be able to override it.

Maybe to begin with, we should just ensure that all variable definitions are included in the non-inlined CSS. That would at least ensure that this CSS feature works on clients that support it (probably not many).

oliverklee commented 2 months ago

I’m also not sure this should happen at the parser level, though, or whether this should be the responsibility of the code taking the syntax tree (the result from parsing the CSS).

I just realized that I had read this ticket in the context of our sister project PHP-CSS-Parser, not in the context of the Emogrifier project. 🤦 Yes, of course Emogrifier would be the right library to inline CSS variables (even if we might need to add support for parsing those to PHP-CSS-Parser).

njt1982 commented 2 months ago

You’ve raised some really interesting points I’d not even considered when asking the question! Especially the part about css variables in the DOM. This is very non-trivial!

JakeQZ commented 2 months ago

If we copy through the actual value of the --font-size variable to the inline style, the @media rule will not be able to override it.

I think (in the example given), we could set the inline style to font-size: 16px; font-size: var(--font-size); so that if the renderer picks up the variable definition (from the @media rule) it will be used, and otherwise the default (in this case for :root) would apply.

JakeQZ commented 2 months ago

An initial Emogrification pass to determine the variables specifically assigned at each element node, and some way of storing the results so they can be looked up by element

I've not tested this specifically for the case in point, but it's usually possible to simply assign custom properties to objects. E.g. $domElement->emogrifierCssVariables['--font-size'] = '16px'; should work. I've used similar tactics working with other third-party libraries without problem.

I don't think a separate Emogrification pass to determine the variable assignments is actually needed, since in the main pass, all ancestor elements will have already been processed. We just need to

  1. record all variable definitions encountered;
  2. look up the record from [1] by traversing the DOM tree whenever a property value uses a variable.
JakeQZ commented 2 months ago

Support for :root

I'm hoping this already works in the Symfony CssSelector component, and that all we need to do is confirm this (by adding a test - if we haven't already).

JakeQZ commented 1 month ago

I don't think a separate Emogrification pass to determine the variable assignments is actually needed, since in the main pass, all ancestor elements will have already been processed. We just need to

1. record all variable definitions encountered;

2. look up the record from [1] by traversing the DOM tree whenever a property value uses a variable.

I'm thinking of a slightly alternative approach now.

After emogrification with some CSS using custom properties (as they are officially known), we would end up with some HTML like this:

<html style="--text-color: blue;">
  <body>
    <p style="color: var(--text-color);">
      Hello universe
    </p>
  </body>
</html>

So the reconcilliation of custom properties could be done as an entirely separate HTML processing step, with a separate HtmlProcessor-derived class (akin to CssPruner and CssToAttributeConverter).

Options might include whether to retain a declaration using the variable (color: blue; color: var(--text-color);) in case some non-inlinable (e.g. @media) rules might override the variable definition, and/or to strip the variable definitions from the inline style once they have been applied (since they would then be redundant - I can't think of a case when they would not become redundant).

This approach would help avoid overburdening the CSSInliner class.

JakeQZ commented 1 month ago

Options might include whether to retain a declaration using the variable (color: blue; color: var(--text-color);)

I tested this in both Firefox and Chrome (using the inspector/style-editor). An undefined variable (custom property) seems to result in the property not being applied at all, even if specified by some other CSS. It possibly defaults to the value inherit, which is undesired. So that's not an 'option' that could sensibly be provided.

It makes the solution easier. We just simply make the replacements with the values that can be found in the DOM tree after Emogrification.

JakeQZ commented 1 month ago

An undefined variable (custom property) seems to result in the property not being applied at all, even if specified by some other CSS.

MDN confirms this:

When the browser encounters an invalid var() substitution, then the initial or inherited value of the property is used.