mirror of
https://github.com/Combodo/iTop.git
synced 2026-05-18 23:08:46 +02:00
N°8834 - Add compatibility with PHP 8.4 (#819)
* N°8834 - Add compatibility with PHP 8.4 * Rollback of scssphp/scssphp version upgrade due to compilation error
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
Please also have a look at our
|
||||
[API and deprecation policy](docs/API-and-deprecation-policy.md).
|
||||
|
||||
## x.y.z
|
||||
|
||||
### Added
|
||||
@@ -15,84 +18,123 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
### Fixed
|
||||
|
||||
## 7.3.0: Add support for PHP 8.4 and CSS custom properties
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for PHP 8.4 (#1278)
|
||||
- Support CSS custom properties (variables) (#1336)
|
||||
- Support `:root` pseudo-class (#1306)
|
||||
- Add CSS selectors exclusion feature (#1236)
|
||||
|
||||
### Changed
|
||||
|
||||
- Require `sabberworm/php-css-parser:^8.7.0` (#1355)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Preserve case of CSS custom property (variable) names (#1332)
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add an API and deprecation policy (#1323)
|
||||
|
||||
## 7.2.0: Add support for Symfony 7
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for Symfony 7 (#1243)
|
||||
|
||||
## 7.1.0: Add support for PHP 8.3
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for PHP 8.3 (#1218)
|
||||
|
||||
### Changed
|
||||
|
||||
- Disable HTML formatting by default (#1214)
|
||||
|
||||
## 7.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for PHP 8.2 (#1155)
|
||||
|
||||
### Changed
|
||||
|
||||
- Throw exception with invalid CSS in debug mode (#1142)
|
||||
- Only support up to 69 atomic expressions in a selector (#1113)
|
||||
- Require `sabberworm/php-css-parser:^8.4.0` (#1134)
|
||||
- Upgrade to PHPUnit 9 (#1112)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 7.3 will be removed in Emogrifier 8.0.
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop support for Symfony 3.x and 5.3 (#1120, #1162)
|
||||
- Drop support for PHP 7.2 (#1111)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bump the minimum Symfony 4.4 version to avoid PHP deprecation warnings (#1187)
|
||||
|
||||
## 6.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Test with Symfony 6-dev (#1109)
|
||||
- Add support for PHP 8.1 (#1103)
|
||||
- Add a dedicated class for caching (#1097)
|
||||
- Allow installation together with Symfony 6 (#1065)
|
||||
- Support more file types in the `.editorconfig` (#1035)
|
||||
- Set `align` attribute of `<th>` elements with `CssToAttributeConverter` (#1008)
|
||||
- Set `align` attribute of `<th>` elements with `CssToAttributeConverter`
|
||||
(#1008)
|
||||
|
||||
### Changed
|
||||
|
||||
- Use `sabberworm/php-css-parser` to parse the CSS (#1015)
|
||||
- Also check the unit test code with Psalm (#1003)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 7.2 will be removed in Emogrifier 7.0.
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove a redundant CSS data cache (#1018)
|
||||
- Drop support for Symfony 5.1 and 5.2 (#972, #1104)
|
||||
- Drop support for PHP 7.1 (#967)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow `@import` after ignored invalid `@charset` (@1081)
|
||||
- Allow line feeds within `<html>` tag (#987)
|
||||
|
||||
## 5.0.1
|
||||
|
||||
### Changed
|
||||
|
||||
- Switch the default branch from `master` to `main` (#951)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Ignore `http-equiv` `Content-Type` in `<body>` (#961)
|
||||
- Allow "Content-Type" in content (#959)
|
||||
|
||||
## 5.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Add an `.editorconfig` file (#940)
|
||||
- Support PHP 8.0 (#926)
|
||||
- Run the CI build once a week (#933)
|
||||
- Move more development tools to PHIVE (#894, #907)
|
||||
|
||||
### Changed
|
||||
|
||||
- Automatically add a backslash for global functions (#909)
|
||||
- Update the development tools (#898, #895)
|
||||
- Upgrade to PHPUnit 7.5 (#888)
|
||||
@@ -101,14 +143,17 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- Make use of PHP 7.1 language features (#883)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 7.1 will be removed in Emogrifier 6.0.
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop support for Symfony 4.3 and 5.0 (#936)
|
||||
- Stop checking `tests/` with Psalm (#885)
|
||||
- Drop support for PHP 7.0 (#880)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix a nonsensical code example in the README (#920, #935)
|
||||
- Remove `!important` from `style` attributes also when uppercase, mixed case or
|
||||
having whitespace after `!` (#911)
|
||||
@@ -122,35 +167,43 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## 4.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Extract and inject `@font-face` rules into head (#870)
|
||||
- Test tag omission in conformant supplied HTML (#868)
|
||||
- Check for missing return type hint annotations in the code sniffs (#860)
|
||||
- Support `:only-of-type` (with a type) (#849, #856)
|
||||
- Configuration setting methods now all return `$this` to allow chaining (#824, #854)
|
||||
- Configuration setting methods now all return `$this` to allow chaining
|
||||
(#824, #854)
|
||||
- Disable php-cs-fixer Yoda conditions (#791, #794)
|
||||
- Check the code with psalm (#537, #779)
|
||||
- Composer script to run tests with `--stop-on-failure` (#782)
|
||||
- Test universal selector with combinators (#776)
|
||||
|
||||
### Changed
|
||||
- Normalize DOCTYPE declaration according to polyglot markup recommendation (#866)
|
||||
|
||||
- Normalize DOCTYPE declaration according to polyglot markup recommendation
|
||||
(#866)
|
||||
- Upgrade to V2 of the PHP setup GitHub action (#861)
|
||||
- Move the development tools to PHIVE (#850, #851)
|
||||
- Switch the parallel linting to a maintained fork (#842)
|
||||
- Move continuous integration from Travis CI to GitHub actions (#832, #834, #838, #839, #840, #841, #843, #846, #849)
|
||||
- Move continuous integration from Travis CI to GitHub actions
|
||||
(#832, #834, #838, #839, #840, #841, #843, #846, #849)
|
||||
- Clean up the folder structure and autoloading configuration (#529, #785)
|
||||
- Use `self` as the return type for `fromHtml` (#784)
|
||||
- Make use of PHP 7.0 language features (#777)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 7.0 will be removed in Emogrifier 5.0.
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop support for Symfony versions that have reached their end of life (#847)
|
||||
- Drop the `Emogrifier` class (#774)
|
||||
- Drop support for PHP 5.6 (#773)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow `:last-of-type` etc. without type, without causing exception (#875)
|
||||
- Make sure to use the Composer-installed development tools (#862, #865)
|
||||
- Add missing `<head>` element when there's a `<header>` element (#844, #853)
|
||||
@@ -161,22 +214,28 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## 3.1.0
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for PHP 7.4 (#821, #829)
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade to Symfony 5.0 (#820)
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### Added
|
||||
- Test and document excluding entire subtree with `addExcludedSelector()` (#347, #768)
|
||||
|
||||
- Test and document excluding entire subtree with `addExcludedSelector()`
|
||||
(#347, #768)
|
||||
- Test that rules with `:optional` or `:required` are copied to the `<style>`
|
||||
element (#748, #765)
|
||||
- Test that rules with `:only-of-type` are copied to the `<style>` element (#748, #760)
|
||||
- Test that rules with `:only-of-type` are copied to the `<style>` element
|
||||
(#748, #760)
|
||||
- Support `:last-of-type` (#748, #758)
|
||||
- Support `:first-of-type` (#748, #757)
|
||||
- Support `:empty` (#748, #756)
|
||||
- Test that rules with `:any-link` are copied to the `<style>` element (#748, #755)
|
||||
- Test that rules with `:any-link` are copied to the `<style>` element
|
||||
(#748, #755)
|
||||
- Support and test `:only-child` (#747, #754)
|
||||
- Support and test `:nth-last-of-type` (#747, #751)
|
||||
- Support and test `:nth-last-child` (#747, #750)
|
||||
@@ -195,6 +254,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- Add tests for `:nth-child` and `:nth-of-type` (#71, #698)
|
||||
|
||||
### Changed
|
||||
|
||||
- Relax the dependency on `symfony/css-selector` (#762)
|
||||
- Rename `HtmlPruner::removeInvisibleNodes` to
|
||||
`HtmlPruner::removeElementsWithDisplayNone` (#717, #718)
|
||||
@@ -204,14 +264,17 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- Update the development dependencies (#691)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 5.6 will be removed in Emogrifier 4.0.
|
||||
- Deprecate the `Emogrifier` class (#701)
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop `enableCssToHtmlMapping` and `disableInvisibleNodeRemoval` (#692)
|
||||
- Drop support for PHP 5.5 (#690)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix PhpStorm code inspection warnings (#729, #770)
|
||||
- Uppercase type combined with class or ID in selector (#590, #769)
|
||||
- Dynamic pseudo-class combined with static one (rules copied to `<style>`
|
||||
@@ -226,22 +289,27 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## 2.2.0
|
||||
|
||||
### Added
|
||||
|
||||
- Add a `HtmlPruner` class (#679)
|
||||
- Add `AbstractHtmlProcessor::fromDomDocument` (#676)
|
||||
- Add `AbstractHtmlProcessor::fromHtml` (#675)
|
||||
|
||||
### Changed
|
||||
|
||||
- Make the closures static (#674)
|
||||
- Keep `<wbr>` elements by default with `CssInliner` (#665)
|
||||
- Make the `CssInliner` inherit `AbstractHtmlProcessor` (#660)
|
||||
- Separate `CssInliner::inlineCss` and the rendering (#654)
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop the removal of unprocessable tags from `CssInliner` (#685)
|
||||
- Drop the removal of invisible nodes from `CssInliner` (#684)
|
||||
|
||||
### Fixed
|
||||
- Remove opening `<body>` tag from `body` content when element has attribute(s) (#677, #683)
|
||||
|
||||
- Remove opening `<body>` tag from `body` content when element has attribute(s)
|
||||
(#677, #683)
|
||||
- Keep development files out of the Composer packages (#678)
|
||||
- Call all static methods statically in `CssConcatenator` (#670)
|
||||
- Support all HTML5 self-closing tags, including `<embed>`, `<source>`,
|
||||
@@ -252,17 +320,20 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## 2.1.1
|
||||
|
||||
### Changed
|
||||
|
||||
- Add a test that a missing document type gets added (#641)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Keep the `style` element the `head` (#642)
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### Added
|
||||
|
||||
- PHP 7.3 support (#638)
|
||||
- Allow PHP 7.3 in `composer.json`
|
||||
- Test in Travis for PHP 7.3
|
||||
- Allow PHP 7.3 in `composer.json`
|
||||
- Test in Travis for PHP 7.3
|
||||
- Add a `renderBodyContent()` method (#633)
|
||||
- Add a `getDomDocument()` method (#630)
|
||||
- Add a Composer script for PHP CS Fixer (#607)
|
||||
@@ -276,17 +347,20 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- Validate the composer.json on Travis (#476)
|
||||
|
||||
### Changed
|
||||
|
||||
- Mark the work-in-progress classes as `@internal` (#640)
|
||||
- Remove the unprocessable tags from the DOM, not from the raw HTML (#627)
|
||||
- Reject empty HTML in `setHtml()` (#622)
|
||||
- Stop passing the DOM document around (#618)
|
||||
- Improve performance by using explicit namespaces for PHP functions (#573, #576)
|
||||
- Improve performance by using explicit namespaces for PHP functions
|
||||
(#573, #576)
|
||||
- Add type hint checking to the code sniffs (#566)
|
||||
- Check the code with PHPMD (#561)
|
||||
- Add the cyclomatic complexity to the checked code sniffs (#558)
|
||||
- Use the Symfony CSS selector component (#540)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 5.5 will be removed in Emogrifier 3.0.
|
||||
- Support for PHP 5.6 will be removed in Emogrifier 4.0.
|
||||
- The removal of invisible nodes will be removed in Emogrifier 3.0. (#473)
|
||||
@@ -300,16 +374,19 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
version 3.0 and removed for version 4.0.
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop the `@version` PHPDoc annotations (#637)
|
||||
- Drop the destructors (#619)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add required XML PHP extension to `composer.json` (#614)
|
||||
- Add required DOM PHP extension to `composer.json` (#595)
|
||||
- Escape hyphens in regular expressions (#588)
|
||||
- Fix Travis for PHP 5.x (#589)
|
||||
- Allow CSS between empty `@media` rule and another `@media` rule (#534)
|
||||
- Allow additional whitespace in media-query-list of disallowed `@media` rules (#532)
|
||||
- Allow additional whitespace in media-query-list of disallowed `@media` rules (
|
||||
#532)
|
||||
- Allow multiple minified `@import` rules in the CSS without error (note:
|
||||
`@import`s are currently ignored, #527)
|
||||
- Style property ordering when multiple mixed individual and shorthand
|
||||
@@ -325,28 +402,34 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## 2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Support for CSS :not() selector (#431)
|
||||
- Automatically remove !important annotations from final inline style declarations (#420)
|
||||
- Automatically remove !important annotations from final inline style
|
||||
declarations (#420)
|
||||
- Automatically move `<style>` block from `<head>` to `<body>` (#396)
|
||||
- PHP 7.2 support (#398)
|
||||
- Allow PHP 7.2 in `composer.json`, cleaner PHP version constraint
|
||||
- Test in Travis for PHP 7.2
|
||||
- Allow PHP 7.2 in `composer.json`, cleaner PHP version constraint
|
||||
- Test in Travis for PHP 7.2
|
||||
- Debug mode. Throw debug exceptions only if debug is active. (#392)
|
||||
|
||||
### Changed
|
||||
|
||||
- Test with latest and oldest dependencies on Travis (#463)
|
||||
- Always enable the debug mode in the tests (#448)
|
||||
- Optimize the string operations (#430)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 5.5 will be removed in Emogrifier 3.0.
|
||||
- Support for PHP 5.6 will be removed in Emogrifier 4.0.
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop support for PHP 5.4 (#422)
|
||||
- Drop support for HHVM (#386)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Handle invalid/unrecognized selectors in media query blocks (#442)
|
||||
- Throw (the correct) exception for invalid excluded selectors (#437)
|
||||
- emogrifyBody must not encode umlaut entities (#414)
|
||||
@@ -359,19 +442,23 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## 1.2.0 (2017-03-02)
|
||||
|
||||
### Added
|
||||
|
||||
- Handling invalid xPath expression warnings (#361)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 5.5 will be removed in Emogrifier 3.0.
|
||||
- Support for PHP 5.4 will be removed in Emogrifier 2.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow colon (`:`) and semi-colon (`;`) when using the `*=` selector (#371)
|
||||
- Ignore "auto" width and height (#365)
|
||||
|
||||
## 1.1.0 (2016-09-18)
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for PHP 7.1 (#342)
|
||||
- Support the attr|=value selector (#337)
|
||||
- Support the attr*=value selector (#330)
|
||||
@@ -381,13 +468,17 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- Add CSS to HTML attribute mapper (#288)
|
||||
|
||||
### Changed
|
||||
- Remove composer dependency from PHP mbstring extension (Actual code dependency were removed a lot of time ago) (#295)
|
||||
|
||||
- Remove composer dependency from PHP mbstring extension (Actual code dependency
|
||||
were removed a lot of time ago) (#295)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 5.5 will be removed in Emogrifier 3.0.
|
||||
- Support for PHP 5.4 will be removed in Emogrifier 2.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Method emogrifyBodyContent() doesn't keeps utf8 umlauts (#349)
|
||||
- Ignore value with words more than one in the attribute selector (#327)
|
||||
- Ignore spaces around the > in the direct child selector (#322)
|
||||
@@ -399,6 +490,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
## 1.0.0 (2015-10-15)
|
||||
|
||||
### Added
|
||||
|
||||
- Add branch alias (#231)
|
||||
- Remove media queries which do not impact the document (#217)
|
||||
- Allow elements to be excluded from emogrification (#215)
|
||||
@@ -415,19 +507,23 @@ This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
and nth-of-type)
|
||||
|
||||
### Changed
|
||||
|
||||
- Make HTML5 the default document type (#245)
|
||||
- Make copyCssWithMediaToStyleNode private (#218)
|
||||
- Stop encoding umlauts and dollar signs (#170)
|
||||
- Convert the classes to namespaces (#41)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Support for PHP 5.4 will be removed in Emogrifier 2.0.
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop support for PHP 5.3 (#114)
|
||||
- Support for character sets other than UTF-8 was removed.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix failing tests on Windows due to line endings (#263)
|
||||
- Parsing CSS declaration blocks (#261)
|
||||
- Fix first-child and last-child selectors (#257)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Emogrifier
|
||||
|
||||
[](https://github.com/MyIntervals/emogrifier/actions/)
|
||||
[](https://github.com/MyIntervals/emogrifier/actions/)
|
||||
[](https://packagist.org/packages/pelago/emogrifier)
|
||||
[](https://packagist.org/packages/pelago/emogrifier)
|
||||
[](https://packagist.org/packages/pelago/emogrifier)
|
||||
[](https://coveralls.io/github/MyIntervals/emogrifier?branch=main)
|
||||
|
||||
_n. e•mog•ri•fi•er [\ē-'mä-grƏ-,fī-Ər\] - a utility for changing completely the
|
||||
nature or appearance of HTML email, esp. in a particularly fantastic or bizarre
|
||||
@@ -31,6 +32,7 @@ into inline style attributes in your HTML code.
|
||||
- [Usage](#usage)
|
||||
- [Supported CSS selectors](#supported-css-selectors)
|
||||
- [Caveats](#caveats)
|
||||
- [Contributing](#contributing)
|
||||
- [Steps to release a new version](#steps-to-release-a-new-version)
|
||||
- [Maintainers](#maintainers)
|
||||
|
||||
@@ -157,6 +159,58 @@ $visualHtml = CssToAttributeConverter::fromDomDocument($domDocument)
|
||||
->convertCssToVisualAttributes()->render();
|
||||
```
|
||||
|
||||
### Evaluating CSS custom properties (variables)
|
||||
|
||||
The `CssVariableEvaluator` class can be used to apply the values of CSS
|
||||
variables defined in inline style attributes to inline style properties which
|
||||
use them.
|
||||
|
||||
For example, the following CSS defines and uses a custom property:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--text-color: green;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-color);
|
||||
}
|
||||
```
|
||||
|
||||
After `CssInliner` has inlined that CSS on the (contrived) HTML
|
||||
`<html><body><p></p></body></html>`, it will look like this:
|
||||
|
||||
```html
|
||||
|
||||
<html style="--text-color: green;">
|
||||
<body>
|
||||
<p style="color: var(--text-color);">
|
||||
<p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The `CssVariableEvaluator` method `evaluateVariables` will apply the value of
|
||||
`--text-color` so that the paragraph `style` attribute becomes `color: green;`.
|
||||
|
||||
It can be used like this:
|
||||
|
||||
```php
|
||||
use Pelago\Emogrifier\HtmlProcessor\CssVariableEvaluator;
|
||||
|
||||
…
|
||||
|
||||
$evaluatedHtml = CssVariableEvaluator::fromHtml($html)
|
||||
->evaluateVariables()->render();
|
||||
```
|
||||
|
||||
You can also have the ` CssVariableEvaluator ` work on a `DOMDocument`:
|
||||
|
||||
```php
|
||||
$evaluatedHtml = CssVariableEvaluator::fromDomDocument($domDocument)
|
||||
->evaluateVariables()->render();
|
||||
```
|
||||
|
||||
### Removing redundant content and attributes from the HTML
|
||||
|
||||
The `HtmlPruner` class can reduce the size of the HTML by removing elements with
|
||||
@@ -174,11 +228,11 @@ $prunedHtml = HtmlPruner::fromHtml($html)->removeElementsWithDisplayNone()
|
||||
->removeRedundantClasses($classesToKeep)->render();
|
||||
```
|
||||
|
||||
The `removeRedundantClasses` method accepts a whitelist of names of classes that
|
||||
should be retained. If this is a post-processing step after inlining CSS, you
|
||||
can alternatively use `removeRedundantClassesAfterCssInlined`, passing it the
|
||||
`CssInliner` instance that has inlined the CSS (and having the `HtmlPruner` work
|
||||
on the `DOMDocument`). This will use information from the `CssInliner` to
|
||||
The `removeRedundantClasses` method accepts an allowlist of names of classes
|
||||
that should be retained. If this is a post-processing step after inlining CSS,
|
||||
you can alternatively use `removeRedundantClassesAfterCssInlined`, passing it
|
||||
the `CssInliner` instance that has inlined the CSS (and having the `HtmlPruner`
|
||||
work on the `DOMDocument`). This will use information from the `CssInliner` to
|
||||
determine which classes are still required (namely, those used in uninlinable
|
||||
rules that have been copied to a `<style>` element):
|
||||
|
||||
@@ -189,16 +243,16 @@ $prunedHtml = HtmlPruner::fromDomDocument($cssInliner->getDomDocument())
|
||||
```
|
||||
|
||||
The `removeElementsWithDisplayNone` method will not remove any elements which
|
||||
have the class `-emogrifier-keep`. So if, for example, there are elements which
|
||||
have the class `-emogrifier-keep`. So if, for example, there are elements which
|
||||
by default have `display: none` but are revealed by an `@media` rule, or which
|
||||
are intended as a preheader, you can add that class to those elements. The
|
||||
are intended as a preheader, you can add that class to those elements. The
|
||||
paragraph in this HTML snippet will not be removed even though it has
|
||||
`display: none` (which has presumably been applied by `CssInliner::inlineCss()`
|
||||
from a CSS rule `.preheader { display: none; }`):
|
||||
|
||||
```html
|
||||
<p class="preheader -emogrifier-keep" style="display: none;">
|
||||
Hello World!
|
||||
Hello World!
|
||||
</p>
|
||||
```
|
||||
|
||||
@@ -229,15 +283,32 @@ calling the `inlineCss` method:
|
||||
* `->removeAllowedMediaType(string $mediaName)` - You can use this
|
||||
method to remove media types that Emogrifier keeps.
|
||||
* `->addExcludedSelector(string $selector)` - Keeps elements from
|
||||
being affected by CSS inlining. Note that only elements matching the supplied
|
||||
being affected by CSS inlining. Note that only elements matching the supplied
|
||||
selector(s) will be excluded from CSS inlining, not necessarily their
|
||||
descendants. If you wish to exclude an entire subtree, you should provide
|
||||
descendants. If you wish to exclude an entire subtree, you should provide
|
||||
selector(s) which will match all elements in the subtree, for example by using
|
||||
the universal selector:
|
||||
```php
|
||||
$cssInliner->addExcludedSelector('.message-preview');
|
||||
$cssInliner->addExcludedSelector('.message-preview *');
|
||||
```
|
||||
* `->addExcludedCssSelector(string $selector)` - Contrary to
|
||||
`addExcludedSelector`, which excludes HTML nodes, this method excludes CSS
|
||||
selectors from being inlined. This is for example useful if you don't want
|
||||
your CSS reset rules to be inlined on each HTML node (e.g.
|
||||
`* { margin: 0; padding: 0; font-size: 100% }`).
|
||||
Note that these selectors must precisely match the selectors you wish to
|
||||
exclude.
|
||||
Meaning that excluding `.example` will not exclude `p .example`.
|
||||
```php
|
||||
$cssInliner->addExcludedCssSelector('*');
|
||||
$cssInliner->addExcludedCssSelector('form');
|
||||
```
|
||||
* `->removeExcludedCssSelector(string $selector)` - Removes previously added
|
||||
excluded selectors, if any.
|
||||
```php
|
||||
$cssInliner->removeExcludedCssSelector('form');
|
||||
```
|
||||
|
||||
### Migrating from the dropped `Emogrifier` class to the `CssInliner` class
|
||||
|
||||
@@ -286,11 +357,11 @@ $html = CssToAttributeConverter::fromDomDocument($domDocument)
|
||||
Emogrifier currently supports the following
|
||||
[CSS selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors):
|
||||
|
||||
* [type](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors)
|
||||
* [class](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors)
|
||||
* [ID](https://developer.mozilla.org/en-US/docs/Web/CSS/ID_selectors)
|
||||
* [universal](https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors)
|
||||
* [attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors):
|
||||
* [type](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors)
|
||||
* [class](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors)
|
||||
* [ID](https://developer.mozilla.org/en-US/docs/Web/CSS/ID_selectors)
|
||||
* [universal](https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors)
|
||||
* [attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors):
|
||||
* presence
|
||||
* exact value match
|
||||
* value with `~` (one word within a whitespace-separated list of words)
|
||||
@@ -298,68 +369,71 @@ Emogrifier currently supports the following
|
||||
* value with `^` (prefix match)
|
||||
* value with `$` (suffix match)
|
||||
* value with `*` (substring match)
|
||||
* [adjacent](https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_selectors)
|
||||
* [general sibling](https://developer.mozilla.org/en-US/docs/Web/CSS/General_sibling_combinator)
|
||||
* [child](https://developer.mozilla.org/en-US/docs/Web/CSS/Child_selectors)
|
||||
* [descendant](https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_selectors)
|
||||
* [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes):
|
||||
* [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`)
|
||||
* [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)
|
||||
* [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)
|
||||
* [nth-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type)
|
||||
(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)
|
||||
* [adjacent](https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_selectors)
|
||||
* [general sibling](https://developer.mozilla.org/en-US/docs/Web/CSS/General_sibling_combinator)
|
||||
* [child](https://developer.mozilla.org/en-US/docs/Web/CSS/Child_selectors)
|
||||
* [descendant](https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_selectors)
|
||||
* [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes):
|
||||
* [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`)
|
||||
* [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)
|
||||
* [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)
|
||||
* [nth-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type)
|
||||
(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)
|
||||
* [root](https://developer.mozilla.org/en-US/docs/Web/CSS/:root)
|
||||
|
||||
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)
|
||||
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
|
||||
* [last-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type)
|
||||
without a type
|
||||
* [nth-last-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type)
|
||||
without a type
|
||||
* [nth-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type)
|
||||
without a type
|
||||
* [only-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type)
|
||||
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)
|
||||
* [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)
|
||||
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
|
||||
* [last-of-type](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-of-type)
|
||||
without a type
|
||||
* [nth-last-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-last-of-type)
|
||||
without a type
|
||||
* [nth-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-of-type)
|
||||
without a type
|
||||
* [only-of-type()](https://developer.mozilla.org/en-US/docs/Web/CSS/:only-of-type)
|
||||
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:
|
||||
|
||||
* dynamic [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes)
|
||||
(such as `:hover`)
|
||||
* [pseudo-elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements)
|
||||
(such as `::after`)
|
||||
* dynamic
|
||||
[pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes)
|
||||
(such as `:hover`)
|
||||
* [pseudo-elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements)
|
||||
(such as `::after`)
|
||||
|
||||
## Caveats
|
||||
|
||||
* Emogrifier requires the HTML and the CSS to be UTF-8. Encodings like
|
||||
ISO8859-1 or ISO8859-15 are not supported.
|
||||
* Emogrifier preserves all valuable `@media` rules. Media queries can be very
|
||||
useful in responsive email design. See
|
||||
* Emogrifier preserves all applicable `@media` rules. Media queries can be very
|
||||
useful in responsive email design. See
|
||||
[media query support](https://litmus.com/help/email-clients/media-query-support/).
|
||||
However, in order for them to be effective, you may need to add `!important`
|
||||
to some of the declarations within them so that they will override CSS styles
|
||||
that have been inlined. For example, with the following CSS, the `font-size`
|
||||
that have been inlined. For example, with the following CSS, the `font-size`
|
||||
declaration in the `@media` rule would not override the font size for `p`
|
||||
elements from the preceding rule after that has been inlined as
|
||||
`<p style="font-size: 16px;">` in the HTML, without the `!important` directive
|
||||
@@ -374,11 +448,15 @@ They will, however, be preserved and copied to a `<style>` element in the HTML:
|
||||
}
|
||||
}
|
||||
```
|
||||
Any CSS custom properties (variables) defined in `@media` rules cannot be
|
||||
applied to CSS property values that have been inlined and evaluated. However,
|
||||
`@media` rules using custom properties (with `var()`) would still be able to
|
||||
obtain their values (from the inlined definitions or `@media` rules) in email
|
||||
clients that support custom properties.
|
||||
* Emogrifier cannot inline CSS rules involving selectors with pseudo-elements
|
||||
(such as `::after`) or dynamic pseudo-classes (such as `:hover`) – it is
|
||||
impossible. However, such rules will be preserved and copied to a `<style>`
|
||||
element, as for `@media` rules. The same caveat about the possible need for
|
||||
the `!important` directive also applies with pseudo-classes.
|
||||
impossible. However, such rules will be preserved and copied to a `<style>`
|
||||
element, as for `@media` rules, with the same caveats applying.
|
||||
* Emogrifier will grab existing inline style attributes _and_ will
|
||||
grab `<style>` blocks from your HTML, but it will not grab CSS files
|
||||
referenced in `<link>` elements or `@import` rules (though it will leave them
|
||||
@@ -402,20 +480,32 @@ They will, however, be preserved and copied to a `<style>` element in the HTML:
|
||||
self-closing tags will lose their slash. To keep your HTML valid, it is
|
||||
recommended to use HTML5 instead of one of the XHTML variants.
|
||||
|
||||
## API and deprecation policy
|
||||
|
||||
Please have a look at our
|
||||
[API and deprecation policy](docs/API-and-deprecation-policy.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions in the form of bug reports, feature requests, or pull requests are
|
||||
more than welcome. :pray: Please have a look at our
|
||||
[contribution guidelines](CONTRIBUTING.md) to learn more about how to
|
||||
contribute to Emogrifier.
|
||||
|
||||
## Steps to release a new version
|
||||
|
||||
1. In the [composer.json](composer.json), update the `branch-alias` entry to
|
||||
point to the release _after_ the upcoming release.
|
||||
2. In the [CHANGELOG.md](CHANGELOG.md), create a new section with subheadings
|
||||
1. In the [CHANGELOG.md](CHANGELOG.md), create a new section with subheadings
|
||||
for changes _after_ the upcoming release, set the version number for the
|
||||
upcoming release, and remove any empty sections.
|
||||
3. Create a pull request "Prepare release of version x.y.z" with those
|
||||
changes.
|
||||
4. Have the pull request reviewed and merged.
|
||||
5. Tag the new release.
|
||||
6. In the [Releases tab](https://github.com/MyIntervals/emogrifier/releases),
|
||||
1. Update the target milestone in the Dependabot configuration.
|
||||
1. Create a pull request "Prepare release of version x.y.z" with those changes.
|
||||
1. Have the pull request reviewed and merged.
|
||||
1. Tag the new release.
|
||||
1. In the [Releases tab](https://github.com/MyIntervals/emogrifier/releases),
|
||||
create a new release and copy the change log entries to the new release.
|
||||
7. Post about the new release on social media.
|
||||
1. Post about the new release on social media.
|
||||
|
||||
## Maintainers
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Jake Hotson",
|
||||
"email": "jake@qzdesign.co.uk"
|
||||
"email": "jake.github@qzdesign.co.uk"
|
||||
},
|
||||
{
|
||||
"name": "Cameron Brooks"
|
||||
@@ -37,15 +37,19 @@
|
||||
"source": "https://github.com/MyIntervals/emogrifier"
|
||||
},
|
||||
"require": {
|
||||
"php": "~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0",
|
||||
"php": "~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"sabberworm/php-css-parser": "^8.4.0",
|
||||
"sabberworm/php-css-parser": "^8.7.0",
|
||||
"symfony/css-selector": "^4.4.23 || ^5.4.0 || ^6.0.0 || ^7.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "1.3.2",
|
||||
"phpunit/phpunit": "9.6.11",
|
||||
"php-parallel-lint/php-parallel-lint": "1.4.0",
|
||||
"phpstan/extension-installer": "1.4.3",
|
||||
"phpstan/phpstan": "1.12.7",
|
||||
"phpstan/phpstan-phpunit": "1.4.0",
|
||||
"phpstan/phpstan-strict-rules": "1.6.1",
|
||||
"phpunit/phpunit": "9.6.21",
|
||||
"rawr/cross-data-providers": "2.4.0"
|
||||
},
|
||||
"prefer-stable": true,
|
||||
@@ -60,6 +64,9 @@
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"phpstan/extension-installer": true
|
||||
},
|
||||
"preferred-install": {
|
||||
"*": "dist"
|
||||
},
|
||||
@@ -80,28 +87,26 @@
|
||||
"@ci:tests"
|
||||
],
|
||||
"ci:php:fixer": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix --dry-run -v --show-progress=dots config/ src/ tests/",
|
||||
"ci:php:lint": "\"./vendor/bin/parallel-lint\" config src tests",
|
||||
"ci:php:lint": "parallel-lint config src tests",
|
||||
"ci:php:md": "\"./.phive/phpmd\" src text config/phpmd.xml",
|
||||
"ci:php:psalm": "\"./.phive/psalm\" --show-info=false",
|
||||
"ci:php:sniff": "\"./.phive/phpcs\" config src tests",
|
||||
"ci:php:stan": "phpstan --no-progress --error-format=github",
|
||||
"ci:static": [
|
||||
"@ci:composer:normalize",
|
||||
"@ci:php:lint",
|
||||
"@ci:php:sniff",
|
||||
"@ci:php:fixer",
|
||||
"@ci:php:md",
|
||||
"@ci:php:psalm"
|
||||
"@ci:php:stan"
|
||||
],
|
||||
"ci:tests": [
|
||||
"@ci:tests:unit"
|
||||
],
|
||||
"ci:tests:sof": "@php \"./vendor/bin/phpunit\" --stop-on-failure --do-not-cache-result",
|
||||
"ci:tests:unit": "@php \"./vendor/bin/phpunit\" --do-not-cache-result",
|
||||
"ci:tests:coverage": "phpunit --do-not-cache-result --coverage-clover=coverage.xml",
|
||||
"ci:tests:sof": "phpunit --stop-on-failure --do-not-cache-result",
|
||||
"ci:tests:unit": "phpunit --do-not-cache-result",
|
||||
"composer:normalize": "\"./.phive/composer-normalize\" --no-check-lock",
|
||||
"php:fix": "\"./.phive/php-cs-fixer\" --config=config/php-cs-fixer.php fix config/ src/ tests/",
|
||||
"php:version": "@php -v | grep -Po 'PHP\\s++\\K(?:\\d++\\.)*+\\d++(?:-\\w++)?+'",
|
||||
"psalm:baseline": "\"./.phive/psalm\" --set-baseline=psalm.baseline.xml",
|
||||
"psalm:cc": "\"./.phive/psalm\" --clear-cache"
|
||||
"phpstan:baseline": "phpstan --generate-baseline --allow-empty-baseline"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"ci": "Runs all dynamic and static code checks.",
|
||||
@@ -110,16 +115,15 @@
|
||||
"ci:php:fixer": "Checks the code style with PHP CS Fixer.",
|
||||
"ci:php:lint": "Lints the PHP files for syntax errors.",
|
||||
"ci:php:md": "Checks the code complexity with PHPMD.",
|
||||
"ci:php:psalm": "Checks the types with Psalm.",
|
||||
"ci:php:sniff": "Checks the code style with PHP_CodeSniffer.",
|
||||
"ci:php:stan": "Checks the PHP types using PHPStan.",
|
||||
"ci:static": "Runs all static code analysis checks for the code and the composer.json.",
|
||||
"ci:tests": "Runs all dynamic tests (i.e., currently, the unit tests).",
|
||||
"ci:tests:coverage": "Runs the unit tests with code coverage.",
|
||||
"ci:tests:sof": "Runs the unit tests and stops at the first failure.",
|
||||
"ci:tests:unit": "Runs all unit tests.",
|
||||
"composer:normalize": "Reformats and sorts the composer.json file.",
|
||||
"php:fix": "Reformats the code with php-cs-fixer.",
|
||||
"php:version": "Outputs the installed PHP version.",
|
||||
"psalm:baseline": "Updates the Psalm baseline file to match the code.",
|
||||
"psalm:cc": "Clears the Psalm cache."
|
||||
"phpstan:baseline": "Updates the PHPStan baseline file to match the code."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Pelago\Emogrifier\Caching;
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class SimpleStringCache
|
||||
final class SimpleStringCache
|
||||
{
|
||||
/**
|
||||
* @var array<string, string>
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\Css;
|
||||
|
||||
use Pelago\Emogrifier\Utilities\Preg;
|
||||
use Sabberworm\CSS\CSSList\AtRuleBlockList as CssAtRuleBlockList;
|
||||
use Sabberworm\CSS\CSSList\Document as SabberwormCssDocument;
|
||||
use Sabberworm\CSS\Parser as CssParser;
|
||||
@@ -21,7 +22,7 @@ use Sabberworm\CSS\Settings as ParserSettings;
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CssDocument
|
||||
final class CssDocument
|
||||
{
|
||||
/**
|
||||
* @var SabberwormCssDocument
|
||||
@@ -61,7 +62,8 @@ class CssDocument
|
||||
*/
|
||||
private function hasNestedAtRule(string $css): bool
|
||||
{
|
||||
return \preg_match('/@(?:media|supports|(?:-webkit-|-moz-|-ms-|-o-)?+(keyframes|document))\\b/', $css) === 1;
|
||||
return (new Preg())
|
||||
->match('/@(?:media|supports|(?:-webkit-|-moz-|-ms-|-o-)?+(keyframes|document))\\b/', $css) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +142,8 @@ class CssDocument
|
||||
$allowedMediaTypes
|
||||
);
|
||||
$mediaTypesMatcher = \implode('|', $escapedAllowedMediaTypes);
|
||||
$isAllowed = \preg_match('/^\\s*+(?:only\\s++)?+(?:' . $mediaTypesMatcher . ')/i', $mediaType) > 0;
|
||||
$isAllowed
|
||||
= (new Preg())->match('/^\\s*+(?:only\\s++)?+(?:' . $mediaTypesMatcher . ')/i', $mediaType) !== 0;
|
||||
} else {
|
||||
$isAllowed = true;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use Sabberworm\CSS\RuleSet\DeclarationBlock;
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class StyleRule
|
||||
final class StyleRule
|
||||
{
|
||||
/**
|
||||
* @var DeclarationBlock
|
||||
@@ -43,7 +43,7 @@ class StyleRule
|
||||
$selectors = $this->declarationBlock->getSelectors();
|
||||
return \array_map(
|
||||
static function (Selector $selector): string {
|
||||
return (string)$selector;
|
||||
return (string) $selector;
|
||||
},
|
||||
$selectors
|
||||
);
|
||||
|
||||
@@ -7,13 +7,15 @@ namespace Pelago\Emogrifier;
|
||||
use Pelago\Emogrifier\Css\CssDocument;
|
||||
use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
|
||||
use Pelago\Emogrifier\Utilities\CssConcatenator;
|
||||
use Pelago\Emogrifier\Utilities\DeclarationBlockParser;
|
||||
use Pelago\Emogrifier\Utilities\Preg;
|
||||
use Symfony\Component\CssSelector\CssSelectorConverter;
|
||||
use Symfony\Component\CssSelector\Exception\ParseException;
|
||||
|
||||
/**
|
||||
* This class provides functions for converting CSS styles into inline style attributes in your HTML code.
|
||||
*/
|
||||
class CssInliner extends AbstractHtmlProcessor
|
||||
final class CssInliner extends AbstractHtmlProcessor
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
@@ -23,12 +25,7 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private const CACHE_KEY_CSS_DECLARATIONS_BLOCK = 1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private const CACHE_KEY_COMBINED_STYLES = 2;
|
||||
private const CACHE_KEY_COMBINED_STYLES = 1;
|
||||
|
||||
/**
|
||||
* Regular expression component matching a static pseudo class in a selector, without the preceding ":",
|
||||
@@ -39,7 +36,7 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
* @var string
|
||||
*/
|
||||
private const PSEUDO_CLASS_MATCHER
|
||||
= 'empty|(?:first|last|nth(?:-last)?+|only)-(?:child|of-type)|not\\([[:ascii:]]*\\)';
|
||||
= 'empty|(?:first|last|nth(?:-last)?+|only)-(?:child|of-type)|not\\([[:ascii:]]*\\)|root';
|
||||
|
||||
/**
|
||||
* This regular expression componenet matches an `...of-type` pseudo class name, without the preceding ":". These
|
||||
@@ -56,11 +53,23 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private const COMBINATOR_MATCHER = '(?:\\s++|\\s*+[>+~]\\s*+)(?=[[:alpha:]_\\-.#*:\\[])';
|
||||
|
||||
/**
|
||||
* options array key for `querySelectorAll`
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const QSA_ALWAYS_THROW_PARSE_EXCEPTION = 'alwaysThrowParseException';
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $excludedSelectors = [];
|
||||
|
||||
/**
|
||||
* @var array<non-empty-string, bool>
|
||||
*/
|
||||
private $excludedCssSelectors = [];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
@@ -69,13 +78,11 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
/**
|
||||
* @var array{
|
||||
* 0: array<string, int>,
|
||||
* 1: array<string, array<string, string>>,
|
||||
* 2: array<string, string>
|
||||
* 1: array<string, string>
|
||||
* }
|
||||
*/
|
||||
private $caches = [
|
||||
self::CACHE_KEY_SELECTOR => [],
|
||||
self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [],
|
||||
self::CACHE_KEY_COMBINED_STYLES => [],
|
||||
];
|
||||
|
||||
@@ -164,7 +171,11 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
* @return $this
|
||||
*
|
||||
* @throws ParseException in debug mode, if an invalid selector is encountered
|
||||
* @throws \RuntimeException in debug mode, if an internal PCRE error occurs
|
||||
* @throws \RuntimeException
|
||||
* in debug mode, if an internal PCRE error occurs
|
||||
* or `CssSelectorConverter::toXPath` returns an invalid XPath expression
|
||||
* @throws \UnexpectedValueException
|
||||
* if a selector query result includes a node which is not a `DOMElement`
|
||||
*/
|
||||
public function inlineCss(string $css = ''): self
|
||||
{
|
||||
@@ -183,23 +194,12 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
|
||||
$excludedNodes = $this->getNodesToExclude();
|
||||
$cssRules = $this->collateCssRules($parsedCss);
|
||||
$cssSelectorConverter = $this->getCssSelectorConverter();
|
||||
foreach ($cssRules['inlinable'] as $cssRule) {
|
||||
try {
|
||||
$nodesMatchingCssSelectors = $this->getXPath()
|
||||
->query($cssSelectorConverter->toXPath($cssRule['selector']));
|
||||
|
||||
/** @var \DOMElement $node */
|
||||
foreach ($nodesMatchingCssSelectors as $node) {
|
||||
if (\in_array($node, $excludedNodes, true)) {
|
||||
continue;
|
||||
}
|
||||
$this->copyInlinableCssToStyleAttribute($node, $cssRule);
|
||||
}
|
||||
} catch (ParseException $e) {
|
||||
if ($this->debug) {
|
||||
throw $e;
|
||||
foreach ($this->querySelectorAll($cssRule['selector']) as $node) {
|
||||
if (\in_array($node, $excludedNodes, true)) {
|
||||
continue;
|
||||
}
|
||||
$this->copyInlinableCssToStyleAttribute($this->ensureNodeIsElement($node), $cssRule);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +301,36 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a selector to exclude CSS selector from emogrification.
|
||||
*
|
||||
* @param non-empty-string $selector the selector to exclude, e.g., `.editor`
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addExcludedCssSelector(string $selector): self
|
||||
{
|
||||
$this->excludedCssSelectors[$selector] = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* No longer excludes the CSS selector from emogrification.
|
||||
*
|
||||
* @param non-empty-string $selector the selector to no longer exclude, e.g., `.editor`
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeExcludedCssSelector(string $selector): self
|
||||
{
|
||||
if (isset($this->excludedCssSelectors[$selector])) {
|
||||
unset($this->excludedCssSelectors[$selector]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the debug mode.
|
||||
*
|
||||
@@ -357,7 +387,6 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
{
|
||||
$this->caches = [
|
||||
self::CACHE_KEY_SELECTOR => [],
|
||||
self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [],
|
||||
self::CACHE_KEY_COMBINED_STYLES => [],
|
||||
];
|
||||
}
|
||||
@@ -417,11 +446,13 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function normalizeStyleAttributes(\DOMElement $node): void
|
||||
{
|
||||
$normalizedOriginalStyle = \preg_replace_callback(
|
||||
'/-?+[_a-zA-Z][\\w\\-]*+(?=:)/S',
|
||||
$declarationBlockParser = new DeclarationBlockParser();
|
||||
|
||||
$normalizedOriginalStyle = (new Preg())->throwExceptions($this->debug)->replaceCallback(
|
||||
'/-{0,2}+[_a-zA-Z][\\w\\-]*+(?=:)/S',
|
||||
/** @param array<array-key, string> $propertyNameMatches */
|
||||
static function (array $propertyNameMatches): string {
|
||||
return \strtolower($propertyNameMatches[0]);
|
||||
static function (array $propertyNameMatches) use ($declarationBlockParser): string {
|
||||
return $declarationBlockParser->normalizePropertyName($propertyNameMatches[0]);
|
||||
},
|
||||
$node->getAttribute('style')
|
||||
);
|
||||
@@ -429,55 +460,13 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
// In order to not overwrite existing style attributes in the HTML, we have to save the original HTML styles.
|
||||
$nodePath = $node->getNodePath();
|
||||
if (\is_string($nodePath) && !isset($this->styleAttributesForNodes[$nodePath])) {
|
||||
$this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationsBlock($normalizedOriginalStyle);
|
||||
$this->styleAttributesForNodes[$nodePath] = $declarationBlockParser->parse($normalizedOriginalStyle);
|
||||
$this->visitedNodes[$nodePath] = $node;
|
||||
}
|
||||
|
||||
$node->setAttribute('style', $normalizedOriginalStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a CSS declaration block into property name/value pairs.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* The declaration block
|
||||
*
|
||||
* "color: #000; font-weight: bold;"
|
||||
*
|
||||
* will be parsed into the following array:
|
||||
*
|
||||
* "color" => "#000"
|
||||
* "font-weight" => "bold"
|
||||
*
|
||||
* @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
|
||||
*
|
||||
* @return array<string, string>
|
||||
* the CSS declarations with the property names as array keys and the property values as array values
|
||||
*/
|
||||
private function parseCssDeclarationsBlock(string $cssDeclarationsBlock): array
|
||||
{
|
||||
if (isset($this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock])) {
|
||||
return $this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock];
|
||||
}
|
||||
|
||||
$properties = [];
|
||||
foreach (\preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock) as $declaration) {
|
||||
/** @var list<string> $matches */
|
||||
$matches = [];
|
||||
if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$propertyName = \strtolower($matches[1]);
|
||||
$propertyValue = $matches[2];
|
||||
$properties[$propertyName] = $propertyValue;
|
||||
}
|
||||
$this->caches[self::CACHE_KEY_CSS_DECLARATIONS_BLOCK][$cssDeclarationsBlock] = $properties;
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CSS content.
|
||||
*
|
||||
@@ -509,34 +498,74 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*
|
||||
* @return list<\DOMElement>
|
||||
*
|
||||
* @throws ParseException
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws ParseException in debug mode, if an invalid selector is encountered
|
||||
* @throws \RuntimeException in debug mode, if `CssSelectorConverter::toXPath` returns an invalid XPath expression
|
||||
* @throws \UnexpectedValueException if the selector query result includes a node which is not a `DOMElement`
|
||||
*/
|
||||
private function getNodesToExclude(): array
|
||||
{
|
||||
$excludedNodes = [];
|
||||
foreach (\array_keys($this->excludedSelectors) as $selectorToExclude) {
|
||||
try {
|
||||
$matchingNodes = $this->getXPath()
|
||||
->query($this->getCssSelectorConverter()->toXPath($selectorToExclude));
|
||||
|
||||
foreach ($matchingNodes as $node) {
|
||||
if (!$node instanceof \DOMElement) {
|
||||
$path = $node->getNodePath() ?? '$node';
|
||||
throw new \UnexpectedValueException($path . ' is not a DOMElement.', 1617975914);
|
||||
}
|
||||
$excludedNodes[] = $node;
|
||||
}
|
||||
} catch (ParseException $e) {
|
||||
if ($this->debug) {
|
||||
throw $e;
|
||||
}
|
||||
foreach ($this->querySelectorAll($selectorToExclude) as $node) {
|
||||
$excludedNodes[] = $this->ensureNodeIsElement($node);
|
||||
}
|
||||
}
|
||||
|
||||
return $excludedNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{}|array{alwaysThrowParseException: bool} $options
|
||||
* This is an array of option values to control behaviour:
|
||||
* - `QSA_ALWAYS_THROW_PARSE_EXCEPTION` - `bool` - throw any `ParseException` regardless of debug setting.
|
||||
*
|
||||
* @return \DOMNodeList<\DOMNode> the HTML elements that match the provided CSS `$selectors`
|
||||
*
|
||||
* @throws ParseException
|
||||
* in debug mode (or with `QSA_ALWAYS_THROW_PARSE_EXCEPTION` option), if an invalid selector is encountered
|
||||
* @throws \RuntimeException in debug mode, if `CssSelectorConverter::toXPath` returns an invalid XPath expression
|
||||
*/
|
||||
private function querySelectorAll(string $selectors, array $options = []): \DOMNodeList
|
||||
{
|
||||
try {
|
||||
$result = $this->getXPath()->query($this->getCssSelectorConverter()->toXPath($selectors));
|
||||
|
||||
if ($result === false) {
|
||||
throw new \RuntimeException('query failed with selector \'' . $selectors . '\'', 1726533051);
|
||||
}
|
||||
|
||||
return $result;
|
||||
} catch (ParseException $exception) {
|
||||
$alwaysThrowParseException = $options[self::QSA_ALWAYS_THROW_PARSE_EXCEPTION] ?? false;
|
||||
if ($this->debug || $alwaysThrowParseException) {
|
||||
throw $exception;
|
||||
}
|
||||
return new \DOMNodeList();
|
||||
} catch (\RuntimeException $exception) {
|
||||
if (
|
||||
$this->debug
|
||||
) {
|
||||
throw $exception;
|
||||
}
|
||||
// `RuntimeException` indicates a bug in CssSelector so pass the message to the error handler.
|
||||
\trigger_error($exception->getMessage());
|
||||
return new \DOMNodeList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \UnexpectedValueException if `$node` is not a `DOMElement`
|
||||
*/
|
||||
private function ensureNodeIsElement(\DOMNode $node): \DOMElement
|
||||
{
|
||||
if (!$node instanceof \DOMElement) {
|
||||
$path = $node->getNodePath() ?? '$node';
|
||||
throw new \UnexpectedValueException($path . ' is not a DOMElement.', 1617975914);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CssSelectorConverter
|
||||
*/
|
||||
@@ -577,6 +606,7 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
{
|
||||
$matches = $parsedCss->getStyleRulesData(\array_keys($this->allowedMediaTypes));
|
||||
|
||||
$preg = (new Preg())->throwExceptions($this->debug);
|
||||
$cssRules = [
|
||||
'inlinable' => [],
|
||||
'uninlinable' => [],
|
||||
@@ -588,7 +618,21 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
|
||||
$mediaQuery = $cssRule->getContainingAtRule();
|
||||
$declarationsBlock = $cssRule->getDeclarationAsText();
|
||||
foreach ($cssRule->getSelectors() as $selector) {
|
||||
$selectors = $cssRule->getSelectors();
|
||||
|
||||
// Maybe exclude CSS selectors
|
||||
if (\count($this->excludedCssSelectors) > 0) {
|
||||
// Normalize spaces, line breaks & tabs
|
||||
$selectorsNormalized = \array_map(static function (string $selector) use ($preg): string {
|
||||
return $preg->replace('@\\s++@u', ' ', $selector);
|
||||
}, $selectors);
|
||||
|
||||
$selectors = \array_filter($selectorsNormalized, function (string $selector): bool {
|
||||
return !isset($this->excludedCssSelectors[$selector]);
|
||||
});
|
||||
}
|
||||
|
||||
foreach ($selectors as $selector) {
|
||||
// don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
|
||||
// only allow structural pseudo-classes
|
||||
$hasPseudoElement = \strpos($selector, '::') !== false;
|
||||
@@ -634,15 +678,17 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function hasUnsupportedPseudoClass(string $selector): bool
|
||||
{
|
||||
if (\preg_match('/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i', $selector)) {
|
||||
$preg = (new Preg())->throwExceptions($this->debug);
|
||||
|
||||
if ($preg->match('/:(?!' . self::PSEUDO_CLASS_MATCHER . ')[\\w\\-]/i', $selector) !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!\preg_match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selector)) {
|
||||
if ($preg->match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selector) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (\preg_split('/' . self::COMBINATOR_MATCHER . '/', $selector) as $selectorPart) {
|
||||
foreach ($preg->split('/' . self::COMBINATOR_MATCHER . '/', $selector) as $selectorPart) {
|
||||
if ($this->selectorPartHasUnsupportedOfTypePseudoClass($selectorPart)) {
|
||||
return true;
|
||||
}
|
||||
@@ -661,11 +707,13 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function selectorPartHasUnsupportedOfTypePseudoClass(string $selectorPart): bool
|
||||
{
|
||||
if (\preg_match('/^[\\w\\-]/', $selectorPart)) {
|
||||
$preg = (new Preg())->throwExceptions($this->debug);
|
||||
|
||||
if ($preg->match('/^[\\w\\-]/', $selectorPart) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool)\preg_match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selectorPart);
|
||||
return $preg->match('/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i', $selectorPart) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -693,19 +741,20 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function getCssSelectorPrecedence(string $selector): int
|
||||
{
|
||||
$selectorKey = \md5($selector);
|
||||
$selectorKey = $selector;
|
||||
if (isset($this->caches[self::CACHE_KEY_SELECTOR][$selectorKey])) {
|
||||
return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
|
||||
}
|
||||
|
||||
$preg = (new Preg())->throwExceptions($this->debug);
|
||||
$precedence = 0;
|
||||
foreach ($this->selectorPrecedenceMatchers as $matcher => $value) {
|
||||
if (\trim($selector) === '') {
|
||||
break;
|
||||
}
|
||||
$number = 0;
|
||||
$selector = \preg_replace('/' . $matcher . '\\w+/', '', $selector, -1, $number);
|
||||
$precedence += ($value * (int)$number);
|
||||
$count = 0;
|
||||
$selector = $preg->replace('/' . $matcher . '\\w+/', '', $selector, -1, $count);
|
||||
$precedence += ($value * $count);
|
||||
}
|
||||
$this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
|
||||
|
||||
@@ -729,7 +778,8 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
private function copyInlinableCssToStyleAttribute(\DOMElement $node, array $cssRule): void
|
||||
{
|
||||
$declarationsBlock = $cssRule['declarationsBlock'];
|
||||
$newStyleDeclarations = $this->parseCssDeclarationsBlock($declarationsBlock);
|
||||
$declarationBlockParser = new DeclarationBlockParser();
|
||||
$newStyleDeclarations = $declarationBlockParser->parse($declarationsBlock);
|
||||
if ($newStyleDeclarations === []) {
|
||||
return;
|
||||
}
|
||||
@@ -737,7 +787,7 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
// if it has a style attribute, get it, process it, and append (overwrite) new stuff
|
||||
if ($node->hasAttribute('style')) {
|
||||
// break it up into an associative array
|
||||
$oldStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
|
||||
$oldStyleDeclarations = $declarationBlockParser->parse($node->getAttribute('style'));
|
||||
} else {
|
||||
$oldStyleDeclarations = [];
|
||||
}
|
||||
@@ -757,6 +807,8 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
* @param array<string, string> $newStyles
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \UnexpectedValueException if an empty property name is encountered (which should not happen)
|
||||
*/
|
||||
private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles): string
|
||||
{
|
||||
@@ -784,9 +836,16 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
|
||||
$combinedStyles = \array_merge($oldStyles, $newStyles);
|
||||
|
||||
$declarationBlockParser = new DeclarationBlockParser();
|
||||
$style = '';
|
||||
foreach ($combinedStyles as $attributeName => $attributeValue) {
|
||||
$style .= \strtolower(\trim($attributeName)) . ': ' . \trim($attributeValue) . '; ';
|
||||
$trimmedAttributeName = \trim($attributeName);
|
||||
if ($trimmedAttributeName === '') {
|
||||
throw new \UnexpectedValueException('An empty property name was encountered.', 1727046078);
|
||||
}
|
||||
$propertyName = $declarationBlockParser->normalizePropertyName($trimmedAttributeName);
|
||||
$propertyValue = \trim($attributeValue);
|
||||
$style .= $propertyName . ': ' . $propertyValue . '; ';
|
||||
}
|
||||
$trimmedStyle = \rtrim($style);
|
||||
|
||||
@@ -804,7 +863,7 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function attributeValueIsImportant(string $attributeValue): bool
|
||||
{
|
||||
return (bool)\preg_match('/!\\s*+important$/i', $attributeValue);
|
||||
return (new Preg())->throwExceptions($this->debug)->match('/!\\s*+important$/i', $attributeValue) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -812,9 +871,10 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function fillStyleAttributesWithMergedStyles(): void
|
||||
{
|
||||
$declarationBlockParser = new DeclarationBlockParser();
|
||||
foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
|
||||
$node = $this->visitedNodes[$nodePath];
|
||||
$currentStyleAttributes = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
|
||||
$currentStyleAttributes = $declarationBlockParser->parse($node->getAttribute('style'));
|
||||
$node->setAttribute(
|
||||
'style',
|
||||
$this->generateStyleStringFromDeclarationsArrays(
|
||||
@@ -853,14 +913,16 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function removeImportantAnnotationFromNodeInlineStyle(\DOMElement $node): void
|
||||
{
|
||||
$inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
|
||||
$style = $node->getAttribute('style');
|
||||
$inlineStyleDeclarations = (new DeclarationBlockParser())->parse((bool) $style ? $style : '');
|
||||
/** @var array<string, string> $regularStyleDeclarations */
|
||||
$regularStyleDeclarations = [];
|
||||
/** @var array<string, string> $importantStyleDeclarations */
|
||||
$importantStyleDeclarations = [];
|
||||
foreach ($inlineStyleDeclarations as $property => $value) {
|
||||
if ($this->attributeValueIsImportant($value)) {
|
||||
$importantStyleDeclarations[$property] = $this->pregReplace('/\\s*+!\\s*+important$/i', '', $value);
|
||||
$importantStyleDeclarations[$property]
|
||||
= (new Preg())->throwExceptions($this->debug)->replace('/\\s*+!\\s*+important$/i', '', $value);
|
||||
} else {
|
||||
$regularStyleDeclarations[$property] = $value;
|
||||
}
|
||||
@@ -944,12 +1006,14 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws ParseException
|
||||
* @throws ParseException in debug mode, if an invalid selector is encountered
|
||||
* @throws \RuntimeException in debug mode, if `CssSelectorConverter::toXPath` returns an invalid XPath expression
|
||||
*/
|
||||
private function existsMatchForCssSelector(string $cssSelector): bool
|
||||
{
|
||||
try {
|
||||
$nodesMatchingSelector = $this->getXPath()->query($this->getCssSelectorConverter()->toXPath($cssSelector));
|
||||
$nodesMatchingSelector
|
||||
= $this->querySelectorAll($cssSelector, [self::QSA_ALWAYS_THROW_PARSE_EXCEPTION => true]);
|
||||
} catch (ParseException $e) {
|
||||
if ($this->debug) {
|
||||
throw $e;
|
||||
@@ -957,7 +1021,7 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
return true;
|
||||
}
|
||||
|
||||
return $nodesMatchingSelector !== false && $nodesMatchingSelector->length !== 0;
|
||||
return $nodesMatchingSelector->length !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -972,9 +1036,11 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function removeUnmatchablePseudoComponents(string $selector): string
|
||||
{
|
||||
$preg = (new Preg())->throwExceptions($this->debug);
|
||||
|
||||
// The regex allows nested brackets via `(?2)`.
|
||||
// A space is temporarily prepended because the callback can't determine if the match was at the very start.
|
||||
$selectorWithoutNots = \ltrim(\preg_replace_callback(
|
||||
$selectorWithoutNots = \ltrim((new Preg())->throwExceptions($this->debug)->replaceCallback(
|
||||
'/([\\s>+~]?+):not(\\([^()]*+(?:(?2)[^()]*+)*+\\))/i',
|
||||
/** @param array<array-key, string> $matches */
|
||||
function (array $matches): string {
|
||||
@@ -989,10 +1055,11 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
);
|
||||
|
||||
if (
|
||||
!\preg_match(
|
||||
$preg->match(
|
||||
'/:(?:' . self::OF_TYPE_PSEUDO_CLASS_MATCHER . ')/i',
|
||||
$selectorWithoutUnmatchablePseudoComponents
|
||||
)
|
||||
=== 0
|
||||
) {
|
||||
return $selectorWithoutUnmatchablePseudoComponents;
|
||||
}
|
||||
@@ -1000,7 +1067,7 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
function (string $selectorPart): string {
|
||||
return $this->removeUnsupportedOfTypePseudoClasses($selectorPart);
|
||||
},
|
||||
\preg_split(
|
||||
$preg->split(
|
||||
'/(' . self::COMBINATOR_MATCHER . ')/',
|
||||
$selectorWithoutUnmatchablePseudoComponents,
|
||||
-1,
|
||||
@@ -1041,7 +1108,7 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function removeSelectorComponents(string $matcher, string $selector): string
|
||||
{
|
||||
return \preg_replace(
|
||||
return (new Preg())->throwExceptions($this->debug)->replace(
|
||||
['/([\\s>+~]|^)' . $matcher . '/i', '/' . $matcher . '/i'],
|
||||
['$1*', ''],
|
||||
$selector
|
||||
@@ -1141,58 +1208,4 @@ class CssInliner extends AbstractHtmlProcessor
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps `preg_replace`. If an error occurs (which is highly unlikely), either it is logged and the original
|
||||
* `$subject` is returned, or in debug mode an exception is thrown.
|
||||
*
|
||||
* This method only supports strings, not arrays of strings.
|
||||
*
|
||||
* @param non-empty-string $pattern
|
||||
* @param string $replacement
|
||||
* @param string $subject
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function pregReplace(string $pattern, string $replacement, string $subject): string
|
||||
{
|
||||
$result = \preg_replace($pattern, $replacement, $subject);
|
||||
|
||||
if (!\is_string($result)) {
|
||||
$this->logOrThrowPregLastError();
|
||||
$result = $subject;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the name of the error constant for `preg_last_error` (based on code posted at
|
||||
* {@see https://www.php.net/manual/en/function.preg-last-error.php#124124}) and puts it into an error message
|
||||
* which is either passed to `trigger_error` (in non-debug mode) or an exception which is thrown (in debug mode).
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function logOrThrowPregLastError(): void
|
||||
{
|
||||
$pcreConstants = \get_defined_constants(true)['pcre'];
|
||||
$pcreErrorConstantNames = \array_flip(\array_filter(
|
||||
$pcreConstants,
|
||||
static function (string $key): bool {
|
||||
return \substr($key, -6) === '_ERROR';
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
));
|
||||
|
||||
$pregLastError = \preg_last_error();
|
||||
$message = 'PCRE regex execution error `' . (string)($pcreErrorConstantNames[$pregLastError] ?? $pregLastError)
|
||||
. '`';
|
||||
|
||||
if ($this->debug) {
|
||||
throw new \RuntimeException($message, 1592870147);
|
||||
}
|
||||
\trigger_error($message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\HtmlProcessor;
|
||||
|
||||
use Pelago\Emogrifier\Utilities\Preg;
|
||||
|
||||
/**
|
||||
* Base class for HTML processor that e.g., can remove, add or modify nodes or attributes.
|
||||
*
|
||||
* The "vanilla" subclass is the HtmlNormalizer.
|
||||
*
|
||||
* @psalm-consistent-constructor
|
||||
*/
|
||||
abstract class AbstractHtmlProcessor
|
||||
{
|
||||
@@ -71,9 +71,7 @@ abstract class AbstractHtmlProcessor
|
||||
*
|
||||
* Please use `::fromHtml` or `::fromDomDocument` instead.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
private function __construct() {}
|
||||
|
||||
/**
|
||||
* Builds a new instance from the given HTML.
|
||||
@@ -184,7 +182,7 @@ abstract class AbstractHtmlProcessor
|
||||
$htmlWithPossibleErroneousClosingTags = $this->getDomDocument()->saveHTML($this->getBodyElement());
|
||||
$bodyNodeHtml = $this->removeSelfClosingTagsClosingTags($htmlWithPossibleErroneousClosingTags);
|
||||
|
||||
return \preg_replace('%</?+body(?:\\s[^>]*+)?+>%', '', $bodyNodeHtml);
|
||||
return (new Preg())->replace('%</?+body(?:\\s[^>]*+)?+>%', '', $bodyNodeHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +194,24 @@ abstract class AbstractHtmlProcessor
|
||||
*/
|
||||
private function removeSelfClosingTagsClosingTags(string $html): string
|
||||
{
|
||||
return \preg_replace('%</' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '>%', '', $html);
|
||||
return (new Preg())->replace('%</' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '>%', '', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML element.
|
||||
*
|
||||
* This method assumes that there always is an HTML element, throwing an exception otherwise.
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
protected function getHtmlElement(): \DOMElement
|
||||
{
|
||||
$htmlElement = $this->getDomDocument()->getElementsByTagName('html')->item(0);
|
||||
if (!$htmlElement instanceof \DOMElement) {
|
||||
throw new \UnexpectedValueException('There is no HTML element although there should be one.', 1569930853);
|
||||
}
|
||||
|
||||
return $htmlElement;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,7 +307,7 @@ abstract class AbstractHtmlProcessor
|
||||
private function normalizeDocumentType(string $html): string
|
||||
{
|
||||
// Limit to replacing the first occurrence: as an optimization; and in case an example exists as unescaped text.
|
||||
return \preg_replace(
|
||||
return (new Preg())->replace(
|
||||
'/<!DOCTYPE\\s++html(?=[\\s>])/i',
|
||||
'<!DOCTYPE html',
|
||||
$html,
|
||||
@@ -317,17 +332,17 @@ abstract class AbstractHtmlProcessor
|
||||
|
||||
// We are trying to insert the meta tag to the right spot in the DOM.
|
||||
// If we just prepended it to the HTML, we would lose attributes set to the HTML tag.
|
||||
$hasHeadTag = \preg_match('/<head[\\s>]/i', $html);
|
||||
$hasHeadTag = (new Preg())->match('/<head[\\s>]/i', $html) !== 0;
|
||||
$hasHtmlTag = \stripos($html, '<html') !== false;
|
||||
|
||||
if ($hasHeadTag) {
|
||||
$reworkedHtml = \preg_replace(
|
||||
$reworkedHtml = (new Preg())->replace(
|
||||
'/<head(?=[\\s>])([^>]*+)>/i',
|
||||
'<head$1>' . self::CONTENT_TYPE_META_TAG,
|
||||
$html
|
||||
);
|
||||
} elseif ($hasHtmlTag) {
|
||||
$reworkedHtml = \preg_replace(
|
||||
$reworkedHtml = (new Preg())->replace(
|
||||
'/<html(.*?)>/is',
|
||||
'<html$1><head>' . self::CONTENT_TYPE_META_TAG . '</head>',
|
||||
$html
|
||||
@@ -350,7 +365,11 @@ abstract class AbstractHtmlProcessor
|
||||
*/
|
||||
private function hasContentTypeMetaTagInHead(string $html): bool
|
||||
{
|
||||
\preg_match('%^.*?(?=<meta(?=\\s)[^>]*\\shttp-equiv=(["\']?+)Content-Type\\g{-1}[\\s/>])%is', $html, $matches);
|
||||
(new Preg())->match(
|
||||
'%^.*?(?=<meta(?=\\s)[^>]*\\shttp-equiv=(["\']?+)Content-Type\\g{-1}[\\s/>])%is',
|
||||
$html,
|
||||
$matches
|
||||
);
|
||||
if (isset($matches[0])) {
|
||||
$htmlBefore = $matches[0];
|
||||
try {
|
||||
@@ -380,9 +399,10 @@ abstract class AbstractHtmlProcessor
|
||||
*/
|
||||
private function hasEndOfHeadElement(string $html): bool
|
||||
{
|
||||
$headEndTagMatchCount
|
||||
= \preg_match('%<(?!' . self::TAGNAME_ALLOWED_BEFORE_BODY_MATCHER . '[\\s/>])\\w|</head>%i', $html);
|
||||
if (\is_int($headEndTagMatchCount) && $headEndTagMatchCount > 0) {
|
||||
if (
|
||||
(new Preg())->match('%<(?!' . self::TAGNAME_ALLOWED_BEFORE_BODY_MATCHER . '[\\s/>])\\w|</head>%i', $html)
|
||||
!== 0
|
||||
) {
|
||||
// An exception to the implicit end of the `<head>` is any content within a `<template>` element, as well in
|
||||
// comments. As an optimization, this is only checked for if a potential `<head>` end tag is found.
|
||||
$htmlWithoutCommentsOrTemplates = $this->removeHtmlTemplateElements($this->removeHtmlComments($html));
|
||||
@@ -407,12 +427,7 @@ abstract class AbstractHtmlProcessor
|
||||
*/
|
||||
private function removeHtmlComments(string $html): string
|
||||
{
|
||||
$result = \preg_replace(self::HTML_COMMENT_PATTERN, '', $html);
|
||||
if (!\is_string($result)) {
|
||||
throw new \RuntimeException('Internal PCRE error', 1616521475);
|
||||
}
|
||||
|
||||
return $result;
|
||||
return (new Preg())->throwExceptions(true)->replace(self::HTML_COMMENT_PATTERN, '', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -427,12 +442,7 @@ abstract class AbstractHtmlProcessor
|
||||
*/
|
||||
private function removeHtmlTemplateElements(string $html): string
|
||||
{
|
||||
$result = \preg_replace(self::HTML_TEMPLATE_ELEMENT_PATTERN, '', $html);
|
||||
if (!\is_string($result)) {
|
||||
throw new \RuntimeException('Internal PCRE error', 1616519652);
|
||||
}
|
||||
|
||||
return $result;
|
||||
return (new Preg())->throwExceptions(true)->replace(self::HTML_TEMPLATE_ELEMENT_PATTERN, '', $html);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -445,7 +455,7 @@ abstract class AbstractHtmlProcessor
|
||||
*/
|
||||
private function ensurePhpUnrecognizedSelfClosingTagsAreXml(string $html): string
|
||||
{
|
||||
return \preg_replace(
|
||||
return (new Preg())->replace(
|
||||
'%<' . self::PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER . '\\b[^>]*+(?<!/)(?=>)%',
|
||||
'$0/',
|
||||
$html
|
||||
@@ -463,10 +473,6 @@ abstract class AbstractHtmlProcessor
|
||||
return;
|
||||
}
|
||||
|
||||
$htmlElement = $this->getDomDocument()->getElementsByTagName('html')->item(0);
|
||||
if (!$htmlElement instanceof \DOMElement) {
|
||||
throw new \UnexpectedValueException('There is no HTML element although there should be one.', 1569930853);
|
||||
}
|
||||
$htmlElement->appendChild($this->getDomDocument()->createElement('body'));
|
||||
$this->getHtmlElement()->appendChild($this->getDomDocument()->createElement('body'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Pelago\Emogrifier\HtmlProcessor;
|
||||
|
||||
use Pelago\Emogrifier\Utilities\DeclarationBlockParser;
|
||||
use Pelago\Emogrifier\Utilities\Preg;
|
||||
|
||||
/**
|
||||
* This HtmlProcessor can convert style HTML attributes to the corresponding other visual HTML attributes,
|
||||
* e.g. it converts style="width: 100px" to width="100".
|
||||
@@ -12,12 +15,12 @@ namespace Pelago\Emogrifier\HtmlProcessor;
|
||||
*
|
||||
* To trigger the conversion, call the convertCssToVisualAttributes method.
|
||||
*/
|
||||
class CssToAttributeConverter extends AbstractHtmlProcessor
|
||||
final class CssToAttributeConverter extends AbstractHtmlProcessor
|
||||
{
|
||||
/**
|
||||
* This multi-level array contains simple mappings of CSS properties to
|
||||
* HTML attributes. If a mapping only applies to certain HTML nodes or
|
||||
* only for certain values, the mapping is an object with a whitelist
|
||||
* only for certain values, the mapping is an object with an allowlist
|
||||
* of nodes and values.
|
||||
*
|
||||
* @var array<string, array{attribute: string, nodes?: array<int, string>, values?: array<int, string>}>
|
||||
@@ -42,11 +45,6 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
private static $parsedCssCache = [];
|
||||
|
||||
/**
|
||||
* Maps the CSS from the style nodes to visual HTML attributes.
|
||||
*
|
||||
@@ -54,9 +52,10 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
|
||||
*/
|
||||
public function convertCssToVisualAttributes(): self
|
||||
{
|
||||
$declarationBlockParser = new DeclarationBlockParser();
|
||||
/** @var \DOMElement $node */
|
||||
foreach ($this->getAllNodesWithStyleAttribute() as $node) {
|
||||
$inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
|
||||
$inlineStyleDeclarations = $declarationBlockParser->parse($node->getAttribute('style'));
|
||||
$this->mapCssToHtmlAttributes($inlineStyleDeclarations, $node);
|
||||
}
|
||||
|
||||
@@ -73,48 +72,6 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
|
||||
return $this->getXPath()->query('//*[@style]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a CSS declaration block into property name/value pairs.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* The declaration block
|
||||
*
|
||||
* "color: #000; font-weight: bold;"
|
||||
*
|
||||
* will be parsed into the following array:
|
||||
*
|
||||
* "color" => "#000"
|
||||
* "font-weight" => "bold"
|
||||
*
|
||||
* @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
|
||||
*
|
||||
* @return array<string, string>
|
||||
* the CSS declarations with the property names as array keys and the property values as array values
|
||||
*/
|
||||
private function parseCssDeclarationsBlock(string $cssDeclarationsBlock): array
|
||||
{
|
||||
if (isset(self::$parsedCssCache[$cssDeclarationsBlock])) {
|
||||
return self::$parsedCssCache[$cssDeclarationsBlock];
|
||||
}
|
||||
|
||||
$properties = [];
|
||||
foreach (\preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock) as $declaration) {
|
||||
/** @var array<int, string> $matches */
|
||||
$matches = [];
|
||||
if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$propertyName = \strtolower($matches[1]);
|
||||
$propertyValue = $matches[2];
|
||||
$properties[$propertyName] = $propertyValue;
|
||||
}
|
||||
self::$parsedCssCache[$cssDeclarationsBlock] = $properties;
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies $styles to $node.
|
||||
*
|
||||
@@ -228,12 +185,14 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function mapWidthOrHeightProperty(\DOMElement $node, string $value, string $property): void
|
||||
{
|
||||
$preg = new Preg();
|
||||
|
||||
// only parse values in px and %, but not values like "auto"
|
||||
if (!\preg_match('/^(\\d+)(\\.(\\d+))?(px|%)$/', $value)) {
|
||||
if ($preg->match('/^(\\d+)(\\.(\\d+))?(px|%)$/', $value) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$number = \preg_replace('/[^0-9.%]/', '', $value);
|
||||
$number = $preg->replace('/[^0-9.%]/', '', $value);
|
||||
$node->setAttribute($property, $number);
|
||||
}
|
||||
|
||||
@@ -289,8 +248,7 @@ class CssToAttributeConverter extends AbstractHtmlProcessor
|
||||
*/
|
||||
private function parseCssShorthandValue(string $value): array
|
||||
{
|
||||
/** @var array<int, string> $values */
|
||||
$values = \preg_split('/\\s+/', $value);
|
||||
$values = (new Preg())->split('/\\s+/', $value);
|
||||
|
||||
$css = [];
|
||||
$css['top'] = $values[0];
|
||||
|
||||
@@ -11,6 +11,4 @@ namespace Pelago\Emogrifier\HtmlProcessor;
|
||||
* - add HEAD and BODY elements (if they are missing)
|
||||
* - reformat the HTML
|
||||
*/
|
||||
class HtmlNormalizer extends AbstractHtmlProcessor
|
||||
{
|
||||
}
|
||||
final class HtmlNormalizer extends AbstractHtmlProcessor {}
|
||||
|
||||
@@ -6,11 +6,12 @@ namespace Pelago\Emogrifier\HtmlProcessor;
|
||||
|
||||
use Pelago\Emogrifier\CssInliner;
|
||||
use Pelago\Emogrifier\Utilities\ArrayIntersector;
|
||||
use Pelago\Emogrifier\Utilities\Preg;
|
||||
|
||||
/**
|
||||
* This class can remove things from HTML.
|
||||
*/
|
||||
class HtmlPruner extends AbstractHtmlProcessor
|
||||
final class HtmlPruner extends AbstractHtmlProcessor
|
||||
{
|
||||
/**
|
||||
* We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only
|
||||
@@ -84,9 +85,10 @@ class HtmlPruner extends AbstractHtmlProcessor
|
||||
{
|
||||
$classesToKeepIntersector = new ArrayIntersector($classesToKeep);
|
||||
|
||||
$preg = new Preg();
|
||||
/** @var \DOMElement $element */
|
||||
foreach ($elements as $element) {
|
||||
$elementClasses = \preg_split('/\\s++/', \trim($element->getAttribute('class')));
|
||||
$elementClasses = $preg->split('/\\s++/', \trim($element->getAttribute('class')));
|
||||
$elementClassesToKeep = $classesToKeepIntersector->intersectWith($elementClasses);
|
||||
if ($elementClassesToKeep !== []) {
|
||||
$element->setAttribute('class', \implode(' ', $elementClassesToKeep));
|
||||
@@ -124,9 +126,11 @@ class HtmlPruner extends AbstractHtmlProcessor
|
||||
*/
|
||||
public function removeRedundantClassesAfterCssInlined(CssInliner $cssInliner): self
|
||||
{
|
||||
$preg = new Preg();
|
||||
|
||||
$classesToKeepAsKeys = [];
|
||||
foreach ($cssInliner->getMatchingUninlinableSelectors() as $selector) {
|
||||
\preg_match_all('/\\.(-?+[_a-zA-Z][\\w\\-]*+)/', $selector, $matches);
|
||||
$preg->matchAll('/\\.(-?+[_a-zA-Z][\\w\\-]*+)/', $selector, $matches);
|
||||
$classesToKeepAsKeys += \array_fill_keys($matches[1], true);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Pelago\Emogrifier\Utilities;
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ArrayIntersector
|
||||
final class ArrayIntersector
|
||||
{
|
||||
/**
|
||||
* the array with which the object was constructed, with all its keys exchanged with their associated values
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Pelago\Emogrifier\Utilities;
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class CssConcatenator
|
||||
final class CssConcatenator
|
||||
{
|
||||
/**
|
||||
* Array of media rules in order. Each element is an object with the following properties:
|
||||
@@ -89,7 +89,7 @@ class CssConcatenator
|
||||
$lastDeclarationsBlockWithoutSemicolon = \rtrim(\rtrim($lastRuleBlock->declarationsBlock), ';');
|
||||
$lastRuleBlock->declarationsBlock = $lastDeclarationsBlockWithoutSemicolon . ';' . $declarationsBlock;
|
||||
} else {
|
||||
$mediaRule->ruleBlocks[] = (object)\compact('selectorsAsKeys', 'declarationsBlock');
|
||||
$mediaRule->ruleBlocks[] = (object) \compact('selectorsAsKeys', 'declarationsBlock');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class CssConcatenator
|
||||
return $lastMediaRule;
|
||||
}
|
||||
|
||||
$newMediaRule = (object)[
|
||||
$newMediaRule = (object) [
|
||||
'media' => $media,
|
||||
'ruleBlocks' => [],
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user