Skip to content

Commit

Permalink
[FEATURE] Selectors with states & pseudo-elements
Browse files Browse the repository at this point in the history
Copy CSS for selectors involving pseudo-elements and dynamic (state-based)
pseudo-classes to the style element added to the document (along with the media
queries).

`parseCssRules`:
- Include rules for such selectors in the "uninlineable" rules array (instead
  of discarding them), flagging them with "hasUnmatchablePseudo";
- Don't consider selectors with both an unsupported and a supported
  pseudo-class as matchable for inlining styles.

`copyUninlineableCssToStyleNode`:
- Strip the unmatchable pseudo-components from selectors before calling
  `existsMatchForCssSelector`.

Various unit tests added.

This addresses #280 for the `Emogrifier` class.
  • Loading branch information
JakeQZ committed Apr 5, 2018
1 parent a872420 commit 9e67fd9
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 24 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ This project adheres to [Semantic Versioning](https://semver.org/).
## x.y.z

### Added
- Copy matching rules with dynamic pseudo-classes or pseudo-elements in
selectors to the style element
([#280](https://github.com/MyIntervals/emogrifier/issues/280),
[#562](https://github.com/MyIntervals/emogrifier/pull/562))
- Add a CssToAttributeConverter
([#546](https://github.com/jjriv/emogrifier/pull/546))
- Expose the DOMDocument in AbstractHtmlProcessor
Expand Down
48 changes: 39 additions & 9 deletions src/Emogrifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ class Emogrifier
*/
const CLASS_ATTRIBUTE_MATCHER = '/(\\w+|[\\*\\]])?((\\.[\\w\\-]+)+)/';
/**
* Regular expression component matching a static pseudo class in a selector, without the preceding ":",
* for which the applicable elements can be determined (by converting the selector to an XPath expression).
* (Contains alternation without a group and is intended to be placed within a capturing, non-capturing or lookahead
* group, as appropriate for the usage context.)
*
* @var string
*/
const PSEUDO_CLASS_MATCHER = '\\S+\\-(?:child|type\\()|not\\([[:ascii:]]*\\)';

/**
* @var string
*/
Expand Down Expand Up @@ -753,23 +763,21 @@ private function parseCssRules($css)
// don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
// only allow structural pseudo-classes
$hasPseudoElement = strpos($selector, '::') !== false;
$hasAnyPseudoClass = (bool)preg_match('/:[a-zA-Z]/', $selector);
$hasSupportedPseudoClass = (bool)preg_match(
'/:(\\S+\\-(child|type\\()|not\\([[:ascii:]]*\\))/i',
$hasUnsupportedPseudoClass = (bool)preg_match(
'/:(?!' . static::PSEUDO_CLASS_MATCHER . ')[\\w-]/i',
$selector
);
if ($hasPseudoElement || ($hasAnyPseudoClass && !$hasSupportedPseudoClass)) {
continue;
}
$hasUnmatchablePseudo = $hasPseudoElement || $hasUnsupportedPseudoClass;

$parsedCssRule = [
'media' => $cssRule['media'],
'selector' => trim($selector),
'hasUnmatchablePseudo' => $hasUnmatchablePseudo,
'declarationsBlock' => $cssDeclaration,
// keep track of where it appears in the file, since order is important
'line' => $key,
];
$ruleType = ($cssRule['media'] === '') ? 'inlineable' : 'uninlineable';
$ruleType = ($cssRule['media'] === '' && !$hasUnmatchablePseudo) ? 'inlineable' : 'uninlineable';
$cssRules[$ruleType][] = $parsedCssRule;
}
}
Expand Down Expand Up @@ -1186,8 +1194,12 @@ private function copyUninlineableCssToStyleNode(\DOMDocument $xmlDocument, \DOMX
{
$cssRulesRelevantForDocument = array_filter(
$cssRules,
function ($cssRule) use ($xPath) {
return $this->existsMatchForCssSelector($xPath, $cssRule['selector']);
function (array $cssRule) use ($xPath) {
$selector = $cssRule['selector'];
if ($cssRule['hasUnmatchablePseudo']) {
$selector = $this->removeUnmatchablePseudoComponents($selector);
}
return $this->existsMatchForCssSelector($xPath, $selector);
}
);

Expand All @@ -1209,6 +1221,24 @@ function ($cssRule) use ($xPath) {
$this->addStyleElementToDocument($xmlDocument, $cssConcatenator->getCss());
}

/**
* Removes pseudo-elements and dynamic pseudo-classes from a CSS selector, replacing them with "*" if necessary.
*
* @param string $selector
*
* @return string Selector which will match the relevant DOM elements if the pseudo-classes are assumed to apply,
* or in the case of pseudo-elements will match their originating element.
*/
private function removeUnmatchablePseudoComponents($selector)
{
$pseudoComponentMatcher = ':(?!' . static::PSEUDO_CLASS_MATCHER . '):?+[\\w-]++(?:\\([^\\)]*+\\))?+';
return preg_replace(
['/(\\s|^)' . $pseudoComponentMatcher . '/i', '/' . $pseudoComponentMatcher . '/i'],
['$1*', ''],
$selector
);
}

/**
* Checks whether there is at least one matching element for $cssSelector.
* When not in debug mode, it returns true also for invalid selectors (because they may be valid,
Expand Down
Loading

0 comments on commit 9e67fd9

Please sign in to comment.