├── 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