Skip to content

Commit

Permalink
[BUGFIX] Copy rules with :...of-type without type to <style> element (#…
Browse files Browse the repository at this point in the history
…904)

Resolves #876.
  • Loading branch information
JakeQZ authored Jun 16, 2020
1 parent 38cbe53 commit 19a8f0b
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 140 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ This project adheres to [Semantic Versioning](https://semver.org/).
([#880](https://github.com/MyIntervals/emogrifier/pull/880))

### Fixed
- Copy rules using `:...of-type` without a type to the `<style>` element
([#904](https://github.com/MyIntervals/emogrifier/pull/904))
- Support combinator followed by dynamic pseudo-class in minified CSS
([#903](https://github.com/MyIntervals/emogrifier/pull/903))
- Preserve all uninlinable (or otherwise unprocessed) at-rules
Expand Down
36 changes: 17 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,43 +307,41 @@ Emogrifier currently supports the following
* [empty](https://developer.mozilla.org/en-US/docs/Web/CSS/:empty)
* [first-child](https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child)
* [first-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type)
(with a type, e.g. `p:first-of-type` but not `*:first-of-type` which will
currently be treated as `*:not(*)`)
(with a type, e.g. `p:first-of-type` but not `*:first-of-type`)
* [last-child](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child)
* [last-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type)
(with a type &ndash; without a type, it will be treated as `:not(*)`)
(with a type)
* [not()](https://developer.mozilla.org/en-US/docs/Web/CSS/:not)
* [nth-child()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child)
* [nth-last-child()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-child)
* [nth-last-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type)
(with a type &ndash; without a type, it will be treated as `:not(*)`)
(with a type)
* [nth-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type)
(with a type &ndash; without a type, it will be applied as if `:nth-child`)
(with a type)
* [only-child](https://developer.mozilla.org/en-US/docs/Web/CSS/:only-child)
* [only-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type)
(with a type &ndash; without a type, it will be applied as if `:only-child`
or `:not(*)`, depending on version constraints for `symfony/css-selector`)
(with a type)

The following selectors are not implemented yet:

* [case-insensitive attribute value](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors#case-insensitive)
* static [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes):
* static [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes)
not listed above as supported – rules involving them will nonetheless be
preserved and copied to a `<style>` element in the HTML – including (but not
necessarily limited to) the following:
* [any-link](https://developer.mozilla.org/en-US/docs/Web/CSS/:any-link)
* [first-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:first-of-type)
without a type (declarations discarded)
without a type
* [last-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type)
without a type (declarations discarded)
without a type
* [nth-last-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type)
without a type (declarations discarded)
without a type
* [nth-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type)
without a type (will behave as `:nth-child()`)
without a type
* [only-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type)
without a type (will behave as `:only-child()` or `:not(*)`)
* any pseudo-classes not listed above as supported – rules involving them
will nonetheless be preserved and copied to a `<style>` element in the
HTML – including (but not necessarily limited to) the following:
* [any-link](https://developer.mozilla.org/en-US/docs/Web/CSS/:any-link)
* [optional](https://developer.mozilla.org/en-US/docs/Web/CSS/:optional)
* [required](https://developer.mozilla.org/en-US/docs/Web/CSS/:required)
without a type
* [optional](https://developer.mozilla.org/en-US/docs/Web/CSS/:optional)
* [required](https://developer.mozilla.org/en-US/docs/Web/CSS/:required)

Rules involving the following selectors cannot be applied as inline styles.
They will, however, be preserved and copied to a `<style>` element in the HTML:
Expand Down
98 changes: 93 additions & 5 deletions src/CssInliner.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ class CssInliner extends AbstractHtmlProcessor
private const PSEUDO_CLASS_MATCHER
= 'empty|(?:first|last|nth(?:-last)?+|only)-(?:child|of-type)|not\\([[:ascii:]]*\\)';

/**
* This regular expression componenet matches an `...of-type` pseudo class name, without the preceding ":". These
* pseudo-classes can currently online be inlined if they have an associated type in the selector expression.
*
* @var string
*/
private const OF_TYPE_PSEUDO_CLASS_MATCHER = '(?:first|last|nth(?:-last)?+|only)-of-type';

/**
* regular expression component to match a selector combinator
*
* @var string
*/
private const COMBINATOR_MATCHER = '(?:\\s++|\\s*+[>+~]\\s*+)(?=[[:alpha:]_\\-.#*:\\[])';

/**
* @var bool[]
*/
Expand Down Expand Up @@ -693,16 +708,50 @@ private function parseCssRules(string $css): array
}

/**
* Tests if a selector contains a pseudo-class which would mean it cannot be converted to an XPath expression for
* inlining CSS declarations.
*
* Any pseudo class that does not match {@see PSEUDO_CLASS_MATCHER} cannot be converted. Additionally, `...of-type`
* pseudo-classes cannot be converted if they are not associated with a type selector.
*
* @param string $selector
*
* @return bool
*/
private function hasUnsupportedPseudoClass(string $selector): bool
{
return (bool)\preg_match(
'/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i',
$selector
);
if (\preg_match('/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i', $selector)) {
return true;
}

if (!\preg_match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selector)) {
return false;
}

foreach (\preg_split('/' . self::COMBINATOR_MATCHER . '/', $selector) as $selectorPart) {
if ($this->selectorPartHasUnsupportedOfTypePseudoClass($selectorPart)) {
return true;
}
}

return false;
}

/**
* Tests if part of a selector contains an `...of-type` pseudo-class such that it cannot be converted to an XPath
* expression.
*
* @param string $selectorPart part of a selector which has been split up at combinators
*
* @return bool `true` if the selector part does not have a type but does have an `...of-type` pseudo-class
*/
private function selectorPartHasUnsupportedOfTypePseudoClass(string $selectorPart): bool
{
if (\preg_match('/^[\\w\\-]/', $selectorPart)) {
return false;
}

return (bool)\preg_match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selectorPart);
}

/**
Expand Down Expand Up @@ -1099,10 +1148,28 @@ private function removeUnmatchablePseudoComponents(string $selector): string
' ' . $selector
));

return $this->removeSelectorComponents(
$selectorWithoutUnmatchablePseudoComponents = $this->removeSelectorComponents(
':(?!' . self::PSEUDO_CLASS_MATCHER . '):?+[\\w\\-]++(?:\\([^\\)]*+\\))?+',
$selectorWithoutNots
);

if (
!\preg_match(
'/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i',
$selectorWithoutUnmatchablePseudoComponents
)
) {
return $selectorWithoutUnmatchablePseudoComponents;
}
return \implode('', \array_map(
[$this, 'removeUnsupportedOfTypePseudoClasses'],
\preg_split(
'/(' . self::COMBINATOR_MATCHER . ')/',
$selectorWithoutUnmatchablePseudoComponents,
-1,
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
)
));
}

/**
Expand Down Expand Up @@ -1142,6 +1209,27 @@ private function removeSelectorComponents(string $matcher, string $selector): st
);
}

/**
* Removes any `...-of-type` pseudo-classes from part of a CSS selector, if it does not have a type, replacing them
* with "*" if necessary.
*
* @param string $selectorPart part of a selector which has been split up at combinators
*
* @return string selector part which will match the relevant DOM elements if the pseudo-classes are assumed to
* apply
*/
private function removeUnsupportedOfTypePseudoClasses(string $selectorPart): string
{
if (!$this->selectorPartHasUnsupportedOfTypePseudoClass($selectorPart)) {
return $selectorPart;
}

return $this->removeSelectorComponents(
':(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')(?:\\([^\\)]*+\\))?+',
$selectorPart
);
}

/**
* Applies `$this->matchingUninlinableCssRules` to `$this->domDocument` by placing them as CSS in a `<style>`
* element.
Expand Down
Loading

0 comments on commit 19a8f0b

Please sign in to comment.