├── LICENSE.md
├── composer.json
└── src
├── Css
├── Processor.php
├── Property
│ ├── Processor.php
│ └── Property.php
└── Rule
│ ├── Processor.php
│ └── Rule.php
└── CssToInlineStyles.php
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) Tijs Verkoyen. All rights reserved.
2 | Redistribution and use in source and binary forms, with or without
3 | modification, are permitted provided that the following conditions are met:
4 |
5 | 1. Redistributions of source code must retain the above copyright notice, this
6 | list of conditions and the following disclaimer.
7 | 2. Redistributions in binary form must reproduce the above copyright notice,
8 | this list of conditions and the following disclaimer in the documentation
9 | and/or other materials provided with the distribution.
10 | 3. Neither the name of the copyright holder nor the names of its contributors
11 | may be used to endorse or promote products derived from this software
12 | without specific prior written permission.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tijsverkoyen/css-to-inline-styles",
3 | "type": "library",
4 | "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.",
5 | "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
6 | "license": "BSD-3-Clause",
7 | "authors": [
8 | {
9 | "name": "Tijs Verkoyen",
10 | "email": "css_to_inline_styles@verkoyen.eu",
11 | "role": "Developer"
12 | }
13 | ],
14 | "require": {
15 | "php": "^7.4 || ^8.0",
16 | "ext-dom": "*",
17 | "ext-libxml": "*",
18 | "symfony/css-selector": "^5.4 || ^6.0 || ^7.0"
19 | },
20 | "require-dev": {
21 | "phpunit/phpunit": "^8.5.21 || ^9.5.10",
22 | "phpstan/phpstan": "^2.0",
23 | "phpstan/phpstan-phpunit": "^2.0"
24 | },
25 | "autoload": {
26 | "psr-4": {
27 | "TijsVerkoyen\\CssToInlineStyles\\": "src"
28 | }
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "TijsVerkoyen\\CssToInlineStyles\\Tests\\": "tests"
33 | }
34 | },
35 | "extra": {
36 | "branch-alias": {
37 | "dev-master": "2.x-dev"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Css/Processor.php:
--------------------------------------------------------------------------------
1 | doCleanup($css);
21 | $rulesProcessor = new RuleProcessor();
22 | $rules = $rulesProcessor->splitIntoSeparateRules($css);
23 |
24 | return $rulesProcessor->convertArrayToObjects($rules, $existingRules);
25 | }
26 |
27 | /**
28 | * Get the CSS from the style-tags in the given HTML-string
29 | *
30 | * @param string $html
31 | *
32 | * @return string
33 | */
34 | public function getCssFromStyleTags($html)
35 | {
36 | $css = '';
37 | $matches = array();
38 | $htmlNoComments = preg_replace('||s', '', $html) ?? $html;
39 | preg_match_all('||isU', $htmlNoComments, $matches);
40 |
41 | if (!empty($matches[1])) {
42 | foreach ($matches[1] as $match) {
43 | $css .= trim($match) . "\n";
44 | }
45 | }
46 |
47 | return $css;
48 | }
49 |
50 | /**
51 | * @param string $css
52 | *
53 | * @return string
54 | */
55 | private function doCleanup($css)
56 | {
57 | // remove charset
58 | $css = preg_replace('/@charset "[^"]++";/', '', $css) ?? $css;
59 | // remove media queries
60 | $css = preg_replace('/@media [^{]*+{([^{}]++|{[^{}]*+})*+}/', '', $css) ?? $css;
61 |
62 | $css = str_replace(array("\r", "\n"), '', $css);
63 | $css = str_replace(array("\t"), ' ', $css);
64 | $css = str_replace('"', '\'', $css);
65 | $css = preg_replace('|/\*.*?\*/|', '', $css) ?? $css;
66 | $css = preg_replace('/\s\s++/', ' ', $css) ?? $css;
67 | $css = trim($css);
68 |
69 | return $css;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Css/Property/Processor.php:
--------------------------------------------------------------------------------
1 | cleanup($propertiesString);
19 |
20 | $properties = (array) explode(';', $propertiesString);
21 | $keysToRemove = array();
22 | $numberOfProperties = count($properties);
23 |
24 | for ($i = 0; $i < $numberOfProperties; $i++) {
25 | $properties[$i] = trim($properties[$i]);
26 |
27 | // if the new property begins with base64 it is part of the current property
28 | if (isset($properties[$i + 1]) && strpos(trim($properties[$i + 1]), 'base64,') === 0) {
29 | $properties[$i] .= ';' . trim($properties[$i + 1]);
30 | $keysToRemove[] = $i + 1;
31 | }
32 | }
33 |
34 | if (!empty($keysToRemove)) {
35 | foreach ($keysToRemove as $key) {
36 | unset($properties[$key]);
37 | }
38 | }
39 |
40 | return array_values($properties);
41 | }
42 |
43 | /**
44 | * @param string $string
45 | *
46 | * @return string
47 | */
48 | private function cleanup($string)
49 | {
50 | $string = str_replace(array("\r", "\n"), '', $string);
51 | $string = str_replace(array("\t"), ' ', $string);
52 | $string = str_replace('"', '\'', $string);
53 | $string = preg_replace('|/\*.*?\*/|', '', $string) ?? $string;
54 | $string = preg_replace('/\s\s+/', ' ', $string) ?? $string;
55 |
56 | $string = trim($string);
57 | $string = rtrim($string, ';');
58 |
59 | return $string;
60 | }
61 |
62 | /**
63 | * Converts a property-string into an object
64 | *
65 | * @param string $property
66 | *
67 | * @return Property|null
68 | */
69 | public function convertToObject($property, ?Specificity $specificity = null)
70 | {
71 | if (strpos($property, ':') === false) {
72 | return null;
73 | }
74 |
75 | list($name, $value) = explode(':', $property, 2);
76 |
77 | $name = trim($name);
78 | $value = trim($value);
79 |
80 | if ($value === '') {
81 | return null;
82 | }
83 |
84 | return new Property($name, $value, $specificity);
85 | }
86 |
87 | /**
88 | * Converts an array of property-strings into objects
89 | *
90 | * @param string[] $properties
91 | *
92 | * @return Property[]
93 | */
94 | public function convertArrayToObjects(array $properties, ?Specificity $specificity = null)
95 | {
96 | $objects = array();
97 |
98 | foreach ($properties as $property) {
99 | $object = $this->convertToObject($property, $specificity);
100 | if ($object === null) {
101 | continue;
102 | }
103 |
104 | $objects[] = $object;
105 | }
106 |
107 | return $objects;
108 | }
109 |
110 | /**
111 | * Build the property-string for multiple properties
112 | *
113 | * @param Property[] $properties
114 | *
115 | * @return string
116 | */
117 | public function buildPropertiesString(array $properties)
118 | {
119 | $chunks = array();
120 |
121 | foreach ($properties as $property) {
122 | $chunks[] = $property->toString();
123 | }
124 |
125 | return implode(' ', $chunks);
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/src/Css/Property/Property.php:
--------------------------------------------------------------------------------
1 | name = $name;
33 | $this->value = $value;
34 | $this->originalSpecificity = $specificity;
35 | }
36 |
37 | /**
38 | * Get name
39 | *
40 | * @return string
41 | */
42 | public function getName()
43 | {
44 | return $this->name;
45 | }
46 |
47 | /**
48 | * Get value
49 | *
50 | * @return string
51 | */
52 | public function getValue()
53 | {
54 | return $this->value;
55 | }
56 |
57 | /**
58 | * Get originalSpecificity
59 | *
60 | * @return Specificity|null
61 | */
62 | public function getOriginalSpecificity()
63 | {
64 | return $this->originalSpecificity;
65 | }
66 |
67 | /**
68 | * Is this property important?
69 | *
70 | * @return bool
71 | */
72 | public function isImportant()
73 | {
74 | return (stripos($this->value, '!important') !== false);
75 | }
76 |
77 | /**
78 | * Get the textual representation of the property
79 | *
80 | * @return string
81 | */
82 | public function toString()
83 | {
84 | return sprintf(
85 | '%1$s: %2$s;',
86 | $this->name,
87 | $this->value
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/Css/Rule/Processor.php:
--------------------------------------------------------------------------------
1 | cleanup($rulesString);
20 |
21 | return (array) explode('}', $rulesString);
22 | }
23 |
24 | /**
25 | * @param string $string
26 | *
27 | * @return string
28 | */
29 | private function cleanup($string)
30 | {
31 | $string = str_replace(array("\r", "\n"), '', $string);
32 | $string = str_replace(array("\t"), ' ', $string);
33 | $string = str_replace('"', '\'', $string);
34 | $string = preg_replace('|/\*.*?\*/|', '', $string) ?? $string;
35 | $string = preg_replace('/\s\s+/', ' ', $string) ?? $string;
36 |
37 | $string = trim($string);
38 | $string = rtrim($string, '}');
39 |
40 | return $string;
41 | }
42 |
43 | /**
44 | * Converts a rule-string into an object
45 | *
46 | * @param string $rule
47 | * @param int $originalOrder
48 | *
49 | * @return Rule[]
50 | */
51 | public function convertToObjects($rule, $originalOrder)
52 | {
53 | $rule = $this->cleanup($rule);
54 |
55 | $chunks = explode('{', $rule);
56 | if (!isset($chunks[1])) {
57 | return array();
58 | }
59 | $propertiesProcessor = new PropertyProcessor();
60 | $rules = array();
61 | $selectors = (array) explode(',', trim($chunks[0]));
62 | $properties = $propertiesProcessor->splitIntoSeparateProperties($chunks[1]);
63 |
64 | foreach ($selectors as $selector) {
65 | $selector = trim($selector);
66 | $specificity = $this->calculateSpecificityBasedOnASelector($selector);
67 |
68 | $rules[] = new Rule(
69 | $selector,
70 | $propertiesProcessor->convertArrayToObjects($properties, $specificity),
71 | $specificity,
72 | $originalOrder
73 | );
74 | }
75 |
76 | return $rules;
77 | }
78 |
79 | /**
80 | * Calculates the specificity based on a CSS Selector string,
81 | * Based on the patterns from premailer/css_parser by Alex Dunae
82 | *
83 | * @see https://github.com/premailer/css_parser/blob/master/lib/css_parser/regexps.rb
84 | *
85 | * @param string $selector
86 | *
87 | * @return Specificity
88 | */
89 | public function calculateSpecificityBasedOnASelector($selector)
90 | {
91 | $idSelectorCount = preg_match_all("/ \#/ix", $selector, $matches);
92 | $classAttributesPseudoClassesSelectorsPattern = " (\.[\w]+) # classes
93 | |
94 | \[(\w+) # attributes
95 | |
96 | (\:( # pseudo classes
97 | link|visited|active
98 | |hover|focus
99 | |lang
100 | |target
101 | |enabled|disabled|checked|indeterminate
102 | |root
103 | |nth-child|nth-last-child|nth-of-type|nth-last-of-type
104 | |first-child|last-child|first-of-type|last-of-type
105 | |only-child|only-of-type
106 | |empty|contains
107 | ))";
108 | $classAttributesPseudoClassesSelectorCount = preg_match_all("/{$classAttributesPseudoClassesSelectorsPattern}/ix", $selector, $matches);
109 |
110 | $typePseudoElementsSelectorPattern = " ((^|[\s\+\>\~]+)[\w]+ # elements
111 | |
112 | \:{1,2}( # pseudo-elements
113 | after|before
114 | |first-letter|first-line
115 | |selection
116 | )
117 | )";
118 | $typePseudoElementsSelectorCount = preg_match_all("/{$typePseudoElementsSelectorPattern}/ix", $selector, $matches);
119 |
120 | if ($idSelectorCount === false || $classAttributesPseudoClassesSelectorCount === false || $typePseudoElementsSelectorCount === false) {
121 | throw new \RuntimeException('Failed to calculate specificity based on selector.');
122 | }
123 |
124 | return new Specificity(
125 | $idSelectorCount,
126 | $classAttributesPseudoClassesSelectorCount,
127 | $typePseudoElementsSelectorCount
128 | );
129 | }
130 |
131 | /**
132 | * @param string[] $rules
133 | * @param Rule[] $objects
134 | *
135 | * @return Rule[]
136 | */
137 | public function convertArrayToObjects(array $rules, array $objects = array())
138 | {
139 | $order = 1;
140 | foreach ($rules as $rule) {
141 | $objects = array_merge($objects, $this->convertToObjects($rule, $order));
142 | $order++;
143 | }
144 |
145 | return $objects;
146 | }
147 |
148 | /**
149 | * Sorts an array on the specificity element in an ascending way
150 | * Lower specificity will be sorted to the beginning of the array
151 | *
152 | * @param Rule $e1 The first element.
153 | * @param Rule $e2 The second element.
154 | *
155 | * @return int
156 | */
157 | public static function sortOnSpecificity(Rule $e1, Rule $e2)
158 | {
159 | $e1Specificity = $e1->getSpecificity();
160 | $value = $e1Specificity->compareTo($e2->getSpecificity());
161 |
162 | // if the specificity is the same, use the order in which the element appeared
163 | if ($value === 0) {
164 | $value = $e1->getOrder() - $e2->getOrder();
165 | }
166 |
167 | return $value;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/Css/Rule/Rule.php:
--------------------------------------------------------------------------------
1 | selector = $selector;
41 | $this->properties = $properties;
42 | $this->specificity = $specificity;
43 | $this->order = $order;
44 | }
45 |
46 | /**
47 | * Get selector
48 | *
49 | * @return string
50 | */
51 | public function getSelector()
52 | {
53 | return $this->selector;
54 | }
55 |
56 | /**
57 | * Get properties
58 | *
59 | * @return Property[]
60 | */
61 | public function getProperties()
62 | {
63 | return $this->properties;
64 | }
65 |
66 | /**
67 | * Get specificity
68 | *
69 | * @return Specificity
70 | */
71 | public function getSpecificity()
72 | {
73 | return $this->specificity;
74 | }
75 |
76 | /**
77 | * Get order
78 | *
79 | * @return int
80 | */
81 | public function getOrder()
82 | {
83 | return $this->order;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/CssToInlineStyles.php:
--------------------------------------------------------------------------------
1 | cssConverter = new CssSelectorConverter();
22 | }
23 |
24 | /**
25 | * Will inline the $css into the given $html
26 | *
27 | * Remark: if the html contains