From bc9c82443bea26ffe70f27dd7f330695f339c8cc Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 17:09:55 +0100 Subject: [PATCH 01/39] Beter .travis.yml file --- .travis.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1fd67f5..2ee4b33 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,20 @@ language: php -php: - - 5.3 - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - hhvm - matrix: + include: + - php: 5.4 + - php: 5.5 + - php: 5.6 + - php: 7 + - php: nightly + - php: hhvm allow_failures: - - php: 7.0 + - php: nightly + - php: hhvm before_script: - - travis_retry composer self-update - - travis_retry composer install --no-interaction --prefer-source --dev + - composer self-update + - composer install --prefer-source --no-interaction --no-scripts script: - - vendor/bin/phpunit --verbose --coverage-text + - bin/phpunit --verbose --coverage-text From 2e811ee07fc979064d3d10357cc6bc300eda1348 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 17:10:53 +0100 Subject: [PATCH 02/39] use .dist-file --- phpunit.xml => phpunit.xml.dist | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename phpunit.xml => phpunit.xml.dist (86%) diff --git a/phpunit.xml b/phpunit.xml.dist similarity index 86% rename from phpunit.xml rename to phpunit.xml.dist index b28724e..b08d7f6 100644 --- a/phpunit.xml +++ b/phpunit.xml.dist @@ -1,9 +1,7 @@ - - + src + tests vendor From d914bfc7b66f2b3acbb8ba21c64b5ca0e2924778 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 17:12:39 +0100 Subject: [PATCH 03/39] Gemfile isn't relevant --- .gitignore | 1 - Gemfile | 3 --- 2 files changed, 4 deletions(-) delete mode 100644 Gemfile diff --git a/.gitignore b/.gitignore index 0ba6972..8b7ef35 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /vendor composer.lock -Gemfile.lock diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 54a7b0b..0000000 --- a/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source 'http://rubygems.org' - -gem 'travis-lint' From 8834d44c70e52949b35df6ed98a64e29bd71b7eb Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 17:27:12 +0100 Subject: [PATCH 04/39] The 2.0 version is a major overhaul, which is *not* backwards compatible. * From now on you can re-use the class for multiple mails. * A lot less complicated options, as in: no more options at all. * More separate classes which handle their own (tested) methods. * A lot more tests The reason why I did this was to made the class more usable. --- CHANGELOG.md | 11 + README.md | 29 +- composer.json | 28 +- example/config.php | 8 - example/index.php | 9 +- src/Css/Processor.php | 42 ++ src/Css/Property/Processor.php | 107 ++++ src/Css/Property/Property.php | 72 +++ src/Css/Rule/Processor.php | 117 ++++ src/Css/Rule/Rule.php | 78 +++ src/Css/Specificity/Specificity.php | 135 +++++ src/CssToInlineStyles.php | 699 +++------------------- src/Exception.php | 7 +- src/Specificity.php | 133 ---- tests/Css/ProcessorTest.php | 71 +++ tests/Css/Property/ProcessorTest.php | 74 +++ tests/Css/Property/PropertyTest.php | 35 ++ tests/Css/Rule/ProcessorTest.php | 45 ++ tests/Css/Rule/RuleTest.php | 28 + tests/Css/Specificity/SpecificityTest.php | 76 +++ tests/CssToInlineStylesTest.php | 241 -------- tests/CssToOnlineStylesTest.php | 110 ++++ tests/SpecificityTest.php | 83 --- 23 files changed, 1100 insertions(+), 1138 deletions(-) delete mode 100644 example/config.php create mode 100644 src/Css/Processor.php create mode 100644 src/Css/Property/Processor.php create mode 100644 src/Css/Property/Property.php create mode 100644 src/Css/Rule/Processor.php create mode 100644 src/Css/Rule/Rule.php create mode 100644 src/Css/Specificity/Specificity.php delete mode 100644 src/Specificity.php create mode 100644 tests/Css/ProcessorTest.php create mode 100644 tests/Css/Property/ProcessorTest.php create mode 100644 tests/Css/Property/PropertyTest.php create mode 100644 tests/Css/Rule/ProcessorTest.php create mode 100644 tests/Css/Rule/RuleTest.php create mode 100644 tests/Css/Specificity/SpecificityTest.php delete mode 100644 tests/CssToInlineStylesTest.php create mode 100644 tests/CssToOnlineStylesTest.php delete mode 100644 tests/SpecificityTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a22f9..59cb213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# Changelog since 1.5.4 + +The 2.0 version is a major overhaul, which is *not* backwards compatible. + +* From now on you can re-use the class for multiple mails. +* A lot less complicated options, as in: no more options at all. +* More separate classes which handle their own (tested) methods. +* A lot more tests + +The reason why I did this was to made the class more usable. + # Changelog since 1.5.3 * Fix properties split on base64 encoded url content, thx to [tguyard](https://github.com/Giga-gg), diff --git a/README.md b/README.md index d56f79a..00d226f 100755 --- a/README.md +++ b/README.md @@ -22,28 +22,17 @@ $ composer require tijsverkoyen/css-to-inline-styles use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles; - // Convert HTML + CSS to HTML with inlined CSS - $cssToInlineStyles= new CssToInlineStyles(); - $cssToInlineStyles->setHTML($html); - $cssToInlineStyles->setCSS($css); - $html = $cssToInlineStyles->convert(); + // create instance + $cssToInlineStyles = new CssToInlineStyles(); - // Or use inline-styles blocks from the HTML as CSS - $cssToInlineStyles = new CssToInlineStyles($html); - $cssToInlineStyles->setUseInlineStylesBlock(true); - $html = $cssToInlineStyles->convert(); + $html = file_get_contents(__DIR__ . '/examples/sumo/index.htm'); + $css = file_get_contents(__DIR__ . '/examples/sumo/style.css'); - -## Documentation - -The following properties exists and have get/set methods available: - -Property | Default | Description --------|---------|------------ -cleanup|false|Should the generated HTML be cleaned? -useInlineStylesBlock |false|Use inline-styles block as CSS. -stripOriginalStyleTags |false|Strip original style tags. -excludeMediaQueries |true|Exclude the media queries from the inlined styles. + // output + echo $cssToInlineStyles->convert( + $html, + $css + ); ## Known issues diff --git a/composer.json b/composer.json index 4e6b92b..6f6d1b2 100644 --- a/composer.json +++ b/composer.json @@ -1,31 +1,25 @@ { - "name": "tijsverkoyen/css-to-inline-styles", - "type": "library", + "name": "tijsverkoyen/css-to-inline-styles", + "type": "library", "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.", - "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", - "license": "BSD", - "authors": [ + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "license": "BSD", + "authors": [ { - "name": "Tijs Verkoyen", + "name": "Tijs Verkoyen", "email": "css_to_inline_styles@verkoyen.eu", - "role": "Developer" + "role": "Developer" } ], - "require": { - "php": ">=5.3.0", - "symfony/css-selector": "~2.1" + "require": { + "symfony/css-selector": "^2.7" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "~5.0" }, - "autoload": { + "autoload": { "psr-4": { "TijsVerkoyen\\CssToInlineStyles\\": "src" } - }, - "extra": { - "branch-alias": { - "dev-master": "1.5.x-dev" - } } } diff --git a/example/config.php b/example/config.php deleted file mode 100644 index 8ca14dc..0000000 --- a/example/config.php +++ /dev/null @@ -1,8 +0,0 @@ -setHTML($html); -$cssToInlineStyles->setCSS($css); - // output -echo $cssToInlineStyles->convert(); +echo $cssToInlineStyles->convert( + $html, + $css +); diff --git a/src/Css/Processor.php b/src/Css/Processor.php new file mode 100644 index 0000000..9bb8e43 --- /dev/null +++ b/src/Css/Processor.php @@ -0,0 +1,42 @@ +doCleanup($css); + $rulesProcessor = new RuleProcessor(); + $rules = $rulesProcessor->splitIntoSeparateRules($css); + + return $rulesProcessor->convertArrayToObjects($rules); + } + + /** + * @param $css + * @return mixed|string + */ + protected function doCleanup($css) + { + // remove media queries + $css = preg_replace('/@media [^{]*{([^{}]|{[^{}]*})*}/', '', $css); + + $css = str_replace(array("\r", "\n"), '', $css); + $css = str_replace(array("\t"), ' ', $css); + $css = str_replace('"', '\'', $css); + $css = preg_replace('|/\*.*?\*/|', '', $css); + $css = preg_replace('/\s\s+/', ' ', $css); + $css = trim($css); + + return $css; + } +} diff --git a/src/Css/Property/Processor.php b/src/Css/Property/Processor.php new file mode 100644 index 0000000..dcbd733 --- /dev/null +++ b/src/Css/Property/Processor.php @@ -0,0 +1,107 @@ +cleanup($propertiesString); + + $properties = (array) explode(';', $propertiesString); + $keysToRemove = array(); + + for ($i = 0; $i < count($properties); $i++) { + $properties[$i] = trim($properties[$i]); + + // if the new property begins with base64 it is part of the current property + if (isset($properties[$i + 1]) && strpos(trim($properties[$i + 1]), 'base64,') === 0) { + $properties[$i] .= ';' . trim($properties[$i + 1]); + $keysToRemove[] = $i + 1; + } + } + + if (!empty($keysToRemove)) { + foreach ($keysToRemove as $key) { + unset($properties[$key]); + } + } + + return array_values($properties); + } + + /** + * @param $string + * @return mixed|string + */ + protected function cleanup($string) + { + $string = str_replace(array("\r", "\n"), '', $string); + $string = str_replace(array("\t"), ' ', $string); + $string = str_replace('"', '\'', $string); + $string = preg_replace('|/\*.*?\*/|', '', $string); + $string = preg_replace('/\s\s+/', ' ', $string); + + $string = trim($string); + $string = rtrim($string, ';'); + + return $string; + } + + /** + * Convert a property-string into an object + * + * @param string $property + * @return Property|null + */ + public function convertToObject($property) + { + $chunks = (array) explode(':', $property, 2); + + if (!isset($chunks[1]) || $chunks[1] == '') { + return null; + } + + return new Property(trim($chunks[0]), trim($chunks[1])); + } + + /** + * Convert an array of property-strings into objects + * + * @param array $properties + * @return array + */ + public function convertArrayToObjects(array $properties) + { + $objects = array(); + + foreach ($properties as $property) { + $objects[] = $this->convertToObject($property); + } + + return $objects; + } + + /** + * Build the property-string for multiple properties + * + * @param array $properties + * @return string + */ + public function buildPropertiesString(array $properties) + { + $chunks = array(); + + foreach ($properties as $property) { + $chunks[] = $property->toString(); + } + + return implode(' ', $chunks); + } +} diff --git a/src/Css/Property/Property.php b/src/Css/Property/Property.php new file mode 100644 index 0000000..1e83f35 --- /dev/null +++ b/src/Css/Property/Property.php @@ -0,0 +1,72 @@ +name = $name; + $this->value = $value; + } + + /** + * Get name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get value + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * Is this property important? + * + * @return bool + */ + public function isImportant() + { + return (stristr($this->value, '!important') !== false); + } + + /** + * Get the textual representation of the property + * + * @return string + */ + public function toString() + { + return sprintf( + '%1$s: %2$s;', + $this->name, + $this->value + ); + } +} diff --git a/src/Css/Rule/Processor.php b/src/Css/Rule/Processor.php new file mode 100644 index 0000000..a7309ef --- /dev/null +++ b/src/Css/Rule/Processor.php @@ -0,0 +1,117 @@ +cleanup($rulesString); + + return (array) explode('}', $rulesString); + } + + /** + * @param $string + * @return mixed|string + */ + protected function cleanup($string) + { + $string = str_replace(array("\r", "\n"), '', $string); + $string = str_replace(array("\t"), ' ', $string); + $string = str_replace('"', '\'', $string); + $string = preg_replace('|/\*.*?\*/|', '', $string); + $string = preg_replace('/\s\s+/', ' ', $string); + + $string = trim($string); + $string = rtrim($string, '}'); + + return $string; + } + + /** + * Convert a rule-string into an object + * + * @param string $rule + * @param int $orginalOrder + * @return array + */ + public function convertToObjects($rule, $orginalOrder) + { + $rule = $this->cleanup($rule); + + $chunks = explode('{', $rule); + if (!isset($chunks[1])) { + return array(); + } + $propertiesProcessor = new PropertyProcessor(); + $rules = array(); + $selectors = (array) explode(',', trim($chunks[0])); + $properties = $propertiesProcessor->splitIntoSeparateProperties($chunks[1]); + + foreach ($selectors as $selector) { + $selector = trim($selector); + +// $ruleSet['specificity'] = Specificity::fromSelector($selector); + + $rules[] = new Rule( + $selector, + $propertiesProcessor->convertArrayToObjects($properties), + Specificity::fromSelector($selector), + $orginalOrder + ); + } + + return $rules; + } + + /** + * @param array $rules + * @return array + */ + public function convertArrayToObjects(array $rules) + { + $objects = array(); + + $order = 1; + foreach ($rules as $rule) { + $objects = array_merge($objects, $this->convertToObjects($rule, $order)); + $order++; + } + + if (!empty($objects)) { + usort($objects, array(__CLASS__, 'sortOnSpecificity')); + } + + return $objects; + } + + /** + * Sort an array on the specificity element + * + * @return int + * @param array $e1 The first element. + * @param array $e2 The second element. + */ + private static function sortOnSpecificity($e1, $e2) + { + // Compare the specificity + $value = $e1['specificity']->compareTo($e2['specificity']); + + // if the specificity is the same, use the order in which the element appeared + if ($value === 0) { + $value = $e1['order'] - $e2['order']; + } + + return $value; + } +} diff --git a/src/Css/Rule/Rule.php b/src/Css/Rule/Rule.php new file mode 100644 index 0000000..2ddcccd --- /dev/null +++ b/src/Css/Rule/Rule.php @@ -0,0 +1,78 @@ +selector = $selector; + $this->properties = $properties; + $this->specificity = $specificity; + $this->order = $order; + } + + /** + * Get selector + * + * @return string + */ + public function getSelector() + { + return $this->selector; + } + + /** + * Get properties + * + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Get specificity + * + * @return mixed + */ + public function getSpecificity() + { + return $this->specificity; + } + + /** + * Get order + * + * @return int + */ + public function getOrder() + { + return $this->order; + } +} diff --git a/src/Css/Specificity/Specificity.php b/src/Css/Specificity/Specificity.php new file mode 100644 index 0000000..13e5f3b --- /dev/null +++ b/src/Css/Specificity/Specificity.php @@ -0,0 +1,135 @@ +numberOfIdSelectors = $numberOfIdSelectors; + $this->numberOfClassAttributesPseudoClassSelectors = $numberOfClassAttributesPseudoClassSelectors; + $this->numberOfTypePseudoElementSelectors = $numberOfTypePseudoElementSelectors; + } + + /** + * Increase the current specificity by adding the three values + * + * @param int $numberOfIdSelectors The number of ID selectors in the selector + * @param int $numberOfClassAttributesPseudoClassSelectors The number of class selectors, attributes selectors, and pseudo-classes in the selector + * @param int $numberOfTypePseudoElementSelectors The number of type selectors and pseudo-elements in the selector + */ + public function increase( + $numberOfIdSelectors, + $numberOfClassAttributesPseudoClassSelectors, + $numberOfTypePseudoElementSelectors + ) { + $this->numberOfIdSelectors += $numberOfIdSelectors; + $this->numberOfClassAttributesPseudoClassSelectors += $numberOfClassAttributesPseudoClassSelectors; + $this->numberOfTypePseudoElementSelectors += $numberOfTypePseudoElementSelectors; + } + + /** + * Get the specificity values as an array + * + * @return array + */ + public function getValues() + { + return array( + $this->numberOfIdSelectors, + $this->numberOfClassAttributesPseudoClassSelectors, + $this->numberOfTypePseudoElementSelectors + ); + } + + /** + * Calculate the specificity based on a CSS Selector string, + * Based on the patterns from premailer/css_parser by Alex Dunae + * + * @see https://github.com/premailer/css_parser/blob/master/lib/css_parser/regexps.rb + * @param string $selector + * @return static + */ + public static function fromSelector($selector) + { + $idSelectorsPattern = " \#"; + $classAttributesPseudoClassesSelectorsPattern = " (\.[\w]+) # classes + | + \[(\w+) # attributes + | + (\:( # pseudo classes + link|visited|active + |hover|focus + |lang + |target + |enabled|disabled|checked|indeterminate + |root + |nth-child|nth-last-child|nth-of-type|nth-last-of-type + |first-child|last-child|first-of-type|last-of-type + |only-child|only-of-type + |empty|contains + ))"; + + $typePseudoElementsSelectorPattern = " ((^|[\s\+\>\~]+)[\w]+ # elements + | + \:{1,2}( # pseudo-elements + after|before + |first-letter|first-line + |selection + ) + )"; + + return new static( + preg_match_all("/{$idSelectorsPattern}/ix", $selector, $matches), + preg_match_all("/{$classAttributesPseudoClassesSelectorsPattern}/ix", $selector, $matches), + preg_match_all("/{$typePseudoElementsSelectorPattern}/ix", $selector, $matches) + ); + } + + /** + * Returns <0 when $specificity is greater, 0 when equal, >0 when smaller + * + * @param Specificity $specificity + * @return int + */ + public function compareTo(Specificity $specificity) + { + if ($this->numberOfIdSelectors !== $specificity->numberOfIdSelectors) { + return $this->numberOfIdSelectors - $specificity->numberOfIdSelectors; + } elseif ($this->numberOfClassAttributesPseudoClassSelectors !== $specificity->numberOfClassAttributesPseudoClassSelectors) { + return $this->numberOfClassAttributesPseudoClassSelectors - $specificity->numberOfClassAttributesPseudoClassSelectors; + } else { + return $this->numberOfTypePseudoElementSelectors - $specificity->numberOfTypePseudoElementSelectors; + } + } +} diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index acf610a..927a305 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -1,682 +1,129 @@ - * @version 1.5.4 - * @copyright Copyright (c), Tijs Verkoyen. All rights reserved. - * @license Revised BSD License - */ class CssToInlineStyles { - /** - * The CSS to use - * - * @var string - */ - private $css; - - /** - * The processed CSS rules - * - * @var array - */ - private $cssRules; - - /** - * Should the generated HTML be cleaned - * - * @var bool - */ - private $cleanup = false; - - /** - * The encoding to use. - * - * @var string - */ - private $encoding = 'UTF-8'; - - /** - * The HTML to process - * - * @var string - */ - private $html; - - /** - * Use inline-styles block as CSS - * - * @var bool - */ - private $useInlineStylesBlock = false; - - /** - * Strip original style tags - * - * @var bool - */ - private $stripOriginalStyleTags = false; - - /** - * Exclude the media queries from the inlined styles - * - * @var bool - */ - private $excludeMediaQueries = true; - - /** - * Creates an instance, you could set the HTML and CSS here, or load it - * later. - * - * @return void - * @param string [optional] $html The HTML to process. - * @param string [optional] $css The CSS to use. - */ - public function __construct($html = null, $css = null) - { - if ($html !== null) { - $this->setHTML($html); - } - if ($css !== null) { - $this->setCSS($css); - } - } - - /** - * Remove id and class attributes. - * - * @return string - * @param \DOMXPath $xPath The DOMXPath for the entire document. - */ - private function cleanupHTML(\DOMXPath $xPath) + public function convert($html, $css) { - $nodes = $xPath->query('//@class | //@id'); + $document = $this->createDomDocumentFromHtml($html); + $rules = (new Processor())->getRules($css); + $document = $this->inline($document, $rules); - foreach ($nodes as $node) { - $node->ownerElement->removeAttributeNode($node); - } + return $this->getHtmlFromDocument($document); } /** - * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS + * Inline the given properties on a DOMElement * - * @return string - * @param bool [optional] $outputXHTML Should we output valid XHTML? + * @param \DOMElement $element + * @param array $properties + * @return \DOMElement */ - public function convert($outputXHTML = false) + public function inlineCssOnElement(\DOMElement $element, array $properties) { - // redefine - $outputXHTML = (bool) $outputXHTML; - - // validate - if ($this->html == null) { - throw new Exception('No HTML provided.'); + if (empty($properties)) { + return $element; } - // should we use inline style-block - if ($this->useInlineStylesBlock) { - // init var - $matches = array(); + $propertyProcessor = new PropertyProcessor(); + $cssProperties = array(); + $currentStyles = $element->attributes->getNamedItem('style'); - // match the style blocks - preg_match_all('|(.*)|isU', $this->html, $matches); + if ($currentStyles !== null) { + $currentProperties = $propertyProcessor->convertArrayToObjects( + $propertyProcessor->splitIntoSeparateProperties($currentStyles->value) + ); - // any style-blocks found? - if (!empty($matches[2])) { - // add - foreach ($matches[2] as $match) { - $this->css .= trim($match) . "\n"; - } + foreach ($currentProperties as $property) { + $cssProperties[$property->getName()] = $property; } } - // process css - $this->processCSS(); - - // create new DOMDocument - $document = new \DOMDocument('1.0', $this->getEncoding()); - - // set error level - $internalErrors = libxml_use_internal_errors(true); - - // load HTML - $document->loadHTML($this->html); - - // Restore error level - libxml_use_internal_errors($internalErrors); - - // create new XPath - $xPath = new \DOMXPath($document); - - // any rules? - if (!empty($this->cssRules)) { - // loop rules - foreach ($this->cssRules as $rule) { - try { - $query = CssSelector::toXPath($rule['selector']); - } catch (ExceptionInterface $e) { - continue; - } - - // search elements - $elements = $xPath->query($query); - - // validate elements - if ($elements === false) { - continue; - } - - // loop found elements - foreach ($elements as $element) { - // no styles stored? - if ($element->attributes->getNamedItem( - 'data-css-to-inline-styles-original-styles' - ) == null - ) { - // init var - $originalStyle = ''; - if ($element->attributes->getNamedItem('style') !== null) { - $originalStyle = $element->attributes->getNamedItem('style')->value; - } - - // store original styles - $element->setAttribute( - 'data-css-to-inline-styles-original-styles', - $originalStyle - ); - - // clear the styles - $element->setAttribute('style', ''); - } - - // init var - $properties = array(); - - // get current styles - $stylesAttribute = $element->attributes->getNamedItem('style'); - - // any styles defined before? - if ($stylesAttribute !== null) { - // get value for the styles attribute - $definedStyles = (string) $stylesAttribute->value; - - // split into properties - $definedProperties = $this->splitIntoProperties($definedStyles); - // loop properties - foreach ($definedProperties as $property) { - // validate property - if ($property == '') { - continue; - } - - // split into chunks - $chunks = (array) explode(':', trim($property), 2); - - // validate - if (!isset($chunks[1])) { - continue; - } - - // loop chunks - $properties[$chunks[0]] = trim($chunks[1]); - } - } - - // add new properties into the list - foreach ($rule['properties'] as $key => $value) { - // If one of the rules is already set and is !important, don't apply it, - // except if the new rule is also important. - if ( - !isset($properties[$key]) - || stristr($properties[$key], '!important') === false - || (stristr(implode('', $value), '!important') !== false) - ) { - $properties[$key] = $value; - } - } - - // build string - $propertyChunks = array(); - - // build chunks - foreach ($properties as $key => $values) { - foreach ((array) $values as $value) { - $propertyChunks[] = $key . ': ' . $value . ';'; - } - } - - // build properties string - $propertiesString = implode(' ', $propertyChunks); - - // set attribute - if ($propertiesString != '') { - $element->setAttribute('style', $propertiesString); - } - } - } - - // reapply original styles - // search elements - $elements = $xPath->query('//*[@data-css-to-inline-styles-original-styles]'); - - // loop found elements - foreach ($elements as $element) { - // get the original styles - $originalStyle = $element->attributes->getNamedItem( - 'data-css-to-inline-styles-original-styles' - )->value; - - if ($originalStyle != '') { - $originalProperties = array(); - $originalStyles = $this->splitIntoProperties($originalStyle); - - foreach ($originalStyles as $property) { - // validate property - if ($property == '') { - continue; - } - - // split into chunks - $chunks = (array) explode(':', trim($property), 2); - - // validate - if (!isset($chunks[1])) { - continue; - } - - // loop chunks - $originalProperties[$chunks[0]] = trim($chunks[1]); - } - - // get current styles - $stylesAttribute = $element->attributes->getNamedItem('style'); - $properties = array(); - - // any styles defined before? - if ($stylesAttribute !== null) { - // get value for the styles attribute - $definedStyles = (string) $stylesAttribute->value; - - // split into properties - $definedProperties = $this->splitIntoProperties($definedStyles); - - // loop properties - foreach ($definedProperties as $property) { - // validate property - if ($property == '') { - continue; - } - - // split into chunks - $chunks = (array) explode(':', trim($property), 2); - - // validate - if (!isset($chunks[1])) { - continue; - } - - // loop chunks - $properties[$chunks[0]] = trim($chunks[1]); - } - } - - // add new properties into the list - foreach ($originalProperties as $key => $value) { - $properties[$key] = $value; - } - - // build string - $propertyChunks = array(); - - // build chunks - foreach ($properties as $key => $values) { - foreach ((array) $values as $value) { - $propertyChunks[] = $key . ': ' . $value . ';'; - } - } - - // build properties string - $propertiesString = implode(' ', $propertyChunks); - - // set attribute - if ($propertiesString != '') { - $element->setAttribute( - 'style', - $propertiesString - ); - } + foreach ($properties as $property) { + if (isset($cssProperties[$property->getName()])) { + // only overrule it if the the not-inline-style property is important + if ($property->isImportant()) { + $cssProperties[$property->getName()] = $property; } - - // remove placeholder - $element->removeAttribute( - 'data-css-to-inline-styles-original-styles' - ); + } else { + $cssProperties[$property->getName()] = $property; } } - // strip original style tags if we need to - if ($this->stripOriginalStyleTags) { - $this->stripOriginalStyleTags($xPath); - } - - // cleanup the HTML if we need to - if ($this->cleanup) { - $this->cleanupHTML($xPath); - } + $element->setAttribute( + 'style', + $propertyProcessor->buildPropertiesString(array_values($cssProperties)) + ); - // should we output XHTML? - if ($outputXHTML) { - // set formating - $document->formatOutput = true; - - // get the HTML as XML - $html = $document->saveXML(null, LIBXML_NOEMPTYTAG); - - // remove the XML-header - $html = ltrim(preg_replace('/<\?xml (.*)\?>/', '', $html)); - } // just regular HTML 4.01 as it should be used in newsletters - else { - // get the HTML - $html = $document->saveHTML(); - } - - // return - return $html; + return $element; } /** - * Split a style string into an array of properties. - * The returned array can contain empty strings. - * - * @param string $styles ex: 'color:blue;font-size:12px;' - * @return array an array of strings containing css property ex: array('color:blue','font-size:12px') + * @param string $html + * @return \DOMDocument */ - private function splitIntoProperties($styles) { - $properties = (array) explode(';', $styles); + protected function createDomDocumentFromHtml($html) + { + $document = new \DOMDocument('1.0', 'UTF-8'); + $internalErrors = libxml_use_internal_errors(true); + $document->loadHTML($html); + libxml_use_internal_errors($internalErrors); + $document->formatOutput = true; - for ($i = 0; $i < count($properties); $i++) { - // If next property begins with base64, - // Then the ';' was part of this property (and we should not have split on it). - if (isset($properties[$i + 1]) && strpos($properties[$i + 1], 'base64,') === 0) { - $properties[$i] .= ';' . $properties[$i + 1]; - $properties[$i + 1] = ''; - $i += 1; - } - } - return $properties; + return $document; } /** - * Get the encoding to use - * + * @param \DOMDocument $document * @return string */ - private function getEncoding() - { - return $this->encoding; - } - - /** - * Process the loaded CSS - * - * @return void - */ - private function processCSS() + protected function getHtmlFromDocument(\DOMDocument $document) { - // init vars - $css = (string) $this->css; - - // remove newlines - $css = str_replace(array("\r", "\n"), '', $css); - - // replace double quotes by single quotes - $css = str_replace('"', '\'', $css); - - // remove comments - $css = preg_replace('|/\*.*?\*/|', '', $css); - - // remove spaces - $css = preg_replace('/\s\s+/', ' ', $css); - - if ($this->excludeMediaQueries) { - $css = preg_replace('/@media [^{]*{([^{}]|{[^{}]*})*}/', '', $css); - } - - // rules are splitted by } - $rules = (array) explode('}', $css); - - // init var - $i = 1; - - // loop rules - foreach ($rules as $rule) { - // split into chunks - $chunks = explode('{', $rule); - - // invalid rule? - if (!isset($chunks[1])) { - continue; - } - - // set the selectors - $selectors = trim($chunks[0]); - - // get cssProperties - $cssProperties = trim($chunks[1]); - - // split multiple selectors - $selectors = (array) explode(',', $selectors); - - // loop selectors - foreach ($selectors as $selector) { - // cleanup - $selector = trim($selector); + $xml = $document->saveXML(null, LIBXML_NOEMPTYTAG); - // build an array for each selector - $ruleSet = array(); + $html = preg_replace( + '|<\?xml (.*)\?>|', + '', + $xml + ); - // store selector - $ruleSet['selector'] = $selector; - - // process the properties - $ruleSet['properties'] = $this->processCSSProperties( - $cssProperties - ); - - // calculate specificity - $ruleSet['specificity'] = Specificity::fromSelector($selector); - - // remember the order in which the rules appear - $ruleSet['order'] = $i; - - // add into global rules - $this->cssRules[] = $ruleSet; - } - - // increment - $i++; - } - - // sort based on specificity - if (!empty($this->cssRules)) { - usort($this->cssRules, array(__CLASS__, 'sortOnSpecificity')); - } + return ltrim($html); } /** - * Process the CSS-properties - * - * @return array - * @param string $propertyString The CSS-properties. + * @param \DOMDocument $document + * @param array $rules + * @return \DOMDocument */ - private function processCSSProperties($propertyString) + protected function inline(\DOMDocument $document, array $rules) { - // split into chunks - $properties = $this->splitIntoProperties($propertyString); + if (empty($rules)) { + return $document; + } - // init var - $pairs = array(); + $xPath = new \DOMXPath($document); + foreach ($rules as $rule) { + /** @var Rule $rule */ - // loop properties - foreach ($properties as $property) { - // split into chunks - $chunks = (array) explode(':', $property, 2); + $elements = $xPath->query( + CssSelector::toXPath($rule->getSelector()) + ); - // validate - if (!isset($chunks[1])) { + if ($elements === false) { continue; } - // cleanup - $chunks[0] = trim($chunks[0]); - $chunks[1] = trim($chunks[1]); - - // add to pairs array - if (!isset($pairs[$chunks[0]]) || - !in_array($chunks[1], $pairs[$chunks[0]]) - ) { - $pairs[$chunks[0]][] = $chunks[1]; - } - } - - // sort the pairs - ksort($pairs); - - // return - return $pairs; - } - - /** - * Should the IDs and classes be removed? - * - * @return void - * @param bool [optional] $on Should we enable cleanup? - */ - public function setCleanup($on = true) - { - $this->cleanup = (bool) $on; - } - - /** - * Set CSS to use - * - * @return void - * @param string $css The CSS to use. - */ - public function setCSS($css) - { - $this->css = (string) $css; - } - - /** - * Set the encoding to use with the DOMDocument - * - * @return void - * @param string $encoding The encoding to use. - * - * @deprecated Doesn't have any effect - */ - public function setEncoding($encoding) - { - $this->encoding = (string) $encoding; - } - - /** - * Set HTML to process - * - * @return void - * @param string $html The HTML to process. - */ - public function setHTML($html) - { - $this->html = (string) $html; - } - - /** - * Set use of inline styles block - * If this is enabled the class will use the style-block in the HTML. - * - * @return void - * @param bool [optional] $on Should we process inline styles? - */ - public function setUseInlineStylesBlock($on = true) - { - $this->useInlineStylesBlock = (bool) $on; - } - - /** - * Set strip original style tags - * If this is enabled the class will remove all style tags in the HTML. - * - * @return void - * @param bool [optional] $on Should we process inline styles? - */ - public function setStripOriginalStyleTags($on = true) - { - $this->stripOriginalStyleTags = (bool) $on; - } - - /** - * Set exclude media queries - * - * If this is enabled the media queries will be removed before inlining the rules - * - * @return void - * @param bool [optional] $on - */ - public function setExcludeMediaQueries($on = true) - { - $this->excludeMediaQueries = (bool) $on; - } - - /** - * Strip style tags into the generated HTML - * - * @return string - * @param \DOMXPath $xPath The DOMXPath for the entire document. - */ - private function stripOriginalStyleTags(\DOMXPath $xPath) - { - // Get all style tags - $nodes = $xPath->query('descendant-or-self::style'); - - foreach ($nodes as $node) { - if ($this->excludeMediaQueries) { - //Search for Media Queries - preg_match_all('/@media [^{]*{([^{}]|{[^{}]*})*}/', $node->nodeValue, $mqs); - - // Replace the nodeValue with just the Media Queries - $node->nodeValue = implode("\n", $mqs[0]); - } else { - // Remove the entire style tag - $node->parentNode->removeChild($node); + foreach ($elements as $element) { + $this->inlineCssOnElement($element, $rule->getProperties()); } } - } - - /** - * Sort an array on the specificity element - * - * @return int - * @param array $e1 The first element. - * @param array $e2 The second element. - */ - private static function sortOnSpecificity($e1, $e2) - { - // Compare the specificity - $value = $e1['specificity']->compareTo($e2['specificity']); - - // if the specificity is the same, use the order in which the element appeared - if ($value === 0) { - $value = $e1['order'] - $e2['order']; - } - return $value; + return $document; } } diff --git a/src/Exception.php b/src/Exception.php index 709b055..52f2dc4 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -1,11 +1,8 @@ - */ class Exception extends \Exception { + } diff --git a/src/Specificity.php b/src/Specificity.php deleted file mode 100644 index 8e5a2fd..0000000 --- a/src/Specificity.php +++ /dev/null @@ -1,133 +0,0 @@ -a = $a; - $this->b = $b; - $this->c = $c; - } - - /** - * Increase the current specificity by adding the three values - * - * @param int $a The number of ID selectors in the selector - * @param int $b The number of class selectors, attributes selectors, and pseudo-classes in the selector - * @param int $c The number of type selectors and pseudo-elements in the selector - */ - public function increase($a, $b, $c) - { - $this->a += $a; - $this->b += $b; - $this->c += $c; - } - - /** - * Get the specificity values as an array - * - * @return array - */ - public function getValues() - { - return array($this->a, $this->b, $this->c); - } - - /** - * Calculate the specificity based on a CSS Selector string, - * Based on the patterns from premailer/css_parser by Alex Dunae - * - * @see https://github.com/premailer/css_parser/blob/master/lib/css_parser/regexps.rb - * @param string $selector - * @return static - */ - public static function fromSelector($selector) - { - $pattern_a = " \#"; - $pattern_b = " (\.[\w]+) # classes - | - \[(\w+) # attributes - | - (\:( # pseudo classes - link|visited|active - |hover|focus - |lang - |target - |enabled|disabled|checked|indeterminate - |root - |nth-child|nth-last-child|nth-of-type|nth-last-of-type - |first-child|last-child|first-of-type|last-of-type - |only-child|only-of-type - |empty|contains - ))"; - - $pattern_c = " ((^|[\s\+\>\~]+)[\w]+ # elements - | - \:{1,2}( # pseudo-elements - after|before - |first-letter|first-line - |selection - ) - )"; - - return new static( - preg_match_all("/{$pattern_a}/ix", $selector, $matches), - preg_match_all("/{$pattern_b}/ix", $selector, $matches), - preg_match_all("/{$pattern_c}/ix", $selector, $matches) - ); - } - - /** - * Returns <0 when $specificity is greater, 0 when equal, >0 when smaller - * - * @param Specificity $specificity - * @return int - */ - public function compareTo(Specificity $specificity) - { - if ($this->a !== $specificity->a) { - return $this->a - $specificity->a; - } elseif ($this->b !== $specificity->b) { - return $this->b - $specificity->b; - } else { - return $this->c - $specificity->c; - } - } -} diff --git a/tests/Css/ProcessorTest.php b/tests/Css/ProcessorTest.php new file mode 100644 index 0000000..50a0669 --- /dev/null +++ b/tests/Css/ProcessorTest.php @@ -0,0 +1,71 @@ +processor = new Processor(); + } + + public function tearDown() + { + $this->processor = null; + } + + public function testCssWithOneRule() + { + $css = <<processor->getRules($css); + + $this->assertCount(1, $rules); + $this->assertInstanceOf('TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule', $rules[0]); + $this->assertEquals('a', $rules[0]->getSelector()); + $this->assertCount(2, $rules[0]->getProperties()); + $this->assertEquals('padding', $rules[0]->getProperties()[0]->getName()); + $this->assertEquals('5px', $rules[0]->getProperties()[0]->getValue()); + $this->assertEquals('display', $rules[0]->getProperties()[1]->getName()); + $this->assertEquals('block', $rules[0]->getProperties()[1]->getValue()); + $this->assertEquals(1, $rules[0]->getOrder()); + } + + public function testCssWithMediaQueries() + { + $css = <<processor->getRules($css); + + $this->assertCount(1, $rules); + $this->assertInstanceOf('TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule', $rules[0]); + $this->assertEquals('a', $rules[0]->getSelector()); + $this->assertCount(1, $rules[0]->getProperties()); + $this->assertEquals('color', $rules[0]->getProperties()[0]->getName()); + $this->assertEquals('red', $rules[0]->getProperties()[0]->getValue()); + $this->assertEquals(1, $rules[0]->getOrder()); + } +} diff --git a/tests/Css/Property/ProcessorTest.php b/tests/Css/Property/ProcessorTest.php new file mode 100644 index 0000000..052e5f9 --- /dev/null +++ b/tests/Css/Property/ProcessorTest.php @@ -0,0 +1,74 @@ +processor = new Processor(); + } + + public function tearDown() + { + $this->processor = null; + } + + public function testMostBasicProperty() + { + $propertiesString = 'padding: 0;'; + $this->assertEquals( + array( + 'padding: 0', + ), + $this->processor->splitIntoSeparateProperties($propertiesString) + ); + } + + public function testInvalidProperty() + { + $this->assertNull( + $this->processor->convertToObject('foo:') + ); + } + + public function testBase64ContainsSemiColon() + { + $propertiesString = <<assertEquals( + array( + 'background: url(data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7) no-repeat left center', + 'padding: 5px 0 5px 25px', + ), + $this->processor->splitIntoSeparateProperties($propertiesString) + ); + } + + public function testBuildingPropertiesString() + { + $properties = array( + new Property('padding', '5px'), + new Property('display', 'block'), + ); + + $this->assertEquals( + 'padding: 5px; display: block;', + $this->processor->buildPropertiesString($properties) + ); + } +} diff --git a/tests/Css/Property/PropertyTest.php b/tests/Css/Property/PropertyTest.php new file mode 100644 index 0000000..4bd0773 --- /dev/null +++ b/tests/Css/Property/PropertyTest.php @@ -0,0 +1,35 @@ +assertEquals('padding', $property->getName()); + $this->assertEquals('5px', $property->getValue()); + } + + public function testSimplePropertyToString() + { + $property = new Property('padding', '5px'); + + $this->assertEquals( + 'padding: 5px;', + $property->toString() + ); + } + + public function testIfImportantIsDetected() + { + $property = new Property('padding', '5px !important'); + $this->assertTrue($property->isImportant()); + + $property = new Property('padding', '5px'); + $this->assertFalse($property->isImportant()); + } +} diff --git a/tests/Css/Rule/ProcessorTest.php b/tests/Css/Rule/ProcessorTest.php new file mode 100644 index 0000000..76abee8 --- /dev/null +++ b/tests/Css/Rule/ProcessorTest.php @@ -0,0 +1,45 @@ +processor = new Processor(); + } + + public function tearDown() + { + $this->processor = null; + } + + public function testMostBasicRule() + { + $css = <<processor->convertToObjects($css, 1); + + $this->assertCount(1, $rules); + $this->assertInstanceOf('TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule', $rules[0]); + $this->assertEquals('a', $rules[0]->getSelector()); + $this->assertCount(2, $rules[0]->getProperties()); + $this->assertEquals('padding', $rules[0]->getProperties()[0]->getName()); + $this->assertEquals('5px', $rules[0]->getProperties()[0]->getValue()); + $this->assertEquals('display', $rules[0]->getProperties()[1]->getName()); + $this->assertEquals('block', $rules[0]->getProperties()[1]->getValue()); + $this->assertEquals(1, $rules[0]->getOrder()); + } +} diff --git a/tests/Css/Rule/RuleTest.php b/tests/Css/Rule/RuleTest.php new file mode 100644 index 0000000..555a035 --- /dev/null +++ b/tests/Css/Rule/RuleTest.php @@ -0,0 +1,28 @@ +assertEquals('a', $rule->getSelector()); + $this->assertEquals(array($property), $rule->getProperties()); + $this->assertEquals($specificity, $rule->getSpecificity()); + $this->assertEquals(1, $rule->getOrder()); + } +} diff --git a/tests/Css/Specificity/SpecificityTest.php b/tests/Css/Specificity/SpecificityTest.php new file mode 100644 index 0000000..1fe57db --- /dev/null +++ b/tests/Css/Specificity/SpecificityTest.php @@ -0,0 +1,76 @@ +increase(1, 1, 1); + + $this->assertEquals( + array(1, 1, 1), + $instance->getValues() + ); + } + + public function testIdBeforeClass() + { + $idInstance = new Specificity(1, 0, 0); + $classInstance = new Specificity(0, 1, 0); + + $this->assertEquals( + 1, + $idInstance->compareTo($classInstance) + ); + } + + public function testClassBeforeElement() + { + $idInstance = new Specificity(0, 1, 0); + $classInstance = new Specificity(0, 0, 1); + + $this->assertEquals( + 1, + $idInstance->compareTo($classInstance) + ); + } + + public function testCompareEqualItems() + { + $instance1 = new Specificity(1, 0, 0); + $instance2 = new Specificity(1, 0, 0); + + $this->assertEquals( + 0, + $instance1->compareTo($instance2) + ); + } + + public function testSingleIdSelector() + { + $this->assertEquals( + array(1, 0, 0), + Specificity::fromSelector('#foo')->getValues() + ); + } + + public function testSingleClassSelector() + { + $this->assertEquals( + array(0, 1, 0), + Specificity::fromSelector('.foo')->getValues() + ); + } + + public function testSingleElementSelector() + { + $this->assertEquals( + array(0, 0, 1), + Specificity::fromSelector('a')->getValues() + ); + } +} diff --git a/tests/CssToInlineStylesTest.php b/tests/CssToInlineStylesTest.php deleted file mode 100644 index 61a7e1a..0000000 --- a/tests/CssToInlineStylesTest.php +++ /dev/null @@ -1,241 +0,0 @@ -cssToInlineStyles = new CssToInlineStyles(); - } - - public function teardown() - { - $this->cssToInlineStyles = null; - } - - public function testSimpleElementSelector() - { - $html = '
'; - $css = 'div { display: none; }'; - $expected = '
'; - $this->runHTMLToCSS($html, $css, $expected); - } - - public function testSimpleCssSelector() - { - $html = 'nodeContent'; - $css = '.test-class { background-color: #aaa; text-decoration: none; }'; - $expected = 'nodeContent'; - $this->runHTMLToCSS($html, $css, $expected); - } - - public function testSimpleIdSelector() - { - $html = ''; - $css = '#IMG1 { border: 1px solid red; }'; - $expected = ''; - $this->runHTMLToCSS($html, $css, $expected); - } - - public function testInlineStylesBlock() - { - $html = << - a { - padding: 10px; - margin: 0; - } - - -EOF; - $expected = ''; - $this->cssToInlineStyles->setUseInlineStylesBlock(); - $this->cssToInlineStyles->setHTML($html); - $actual = $this->findAndSaveNode($this->cssToInlineStyles->convert(), '//a'); - $this->assertEquals($expected, $actual); - } - - public function testStripOriginalStyleTags() - { - $html = << - a { - padding: 10px; - margin: 0; - } - - -EOF; - $expected = ''; - $this->cssToInlineStyles->setStripOriginalStyleTags(); - $this->cssToInlineStyles->setHTML($html); - $actual = $this->findAndSaveNode($this->cssToInlineStyles->convert(), '//a'); - $this->assertEquals($expected, $actual); - - $this->assertNull($this->findAndSaveNode($actual, '//style')); - } - - public function testSpecificity() - { - $html = ''; - $css = <<runHTMLToCSS($html, $css, $expected); - } - - public function testMergeOriginalStyles() - { - $html = '

text

'; - $css = <<runHTMLToCSS($html, $css, $expected); - } - - public function testXHTMLOutput() - { - $html = ''; - $css = 'a { display: block; }'; - - $this->cssToInlineStyles->setHTML($html); - $this->cssToInlineStyles->setCSS($css); - $actual = $this->cssToInlineStyles->convert(true); - - $this->assertContains('', $actual); - } - - public function testCleanup() - { - $html = '
id="foo" class="bar"
'; - $css = ' #id { display: inline; } .className { margin-right: 10px; }'; - $expected = '
id="foo" class="bar"
'; - $this->cssToInlineStyles->setCleanup(); - $this->runHTMLToCSS($html, $css, $expected); - } - - public function testEqualSpecificity() - { - $html = ''; - $css = ' .one { display: inline; } a > strong {} a {} a {} a {} a {} a {} a {}a {} img { display: block; }'; - $expected = ''; - $this->runHTMLToCSS($html, $css, $expected); - } - - public function testInvalidSelector() - { - $html = "

"; - $css = ' p&@*$%& { display: inline; }'; - $expected = $html; - $this->runHTMLToCSS($html, $css, $expected); - } - - public function testEncoding() - { - $html = "

" . html_entity_decode('’', 0, 'UTF-8') . "

"; - $css = ''; - $expected = '

' . chr(0xc3) . chr(0xa2) . chr(0xc2) . chr(0x80) . chr(0xc2) . chr(0x99) . '

'; - - $this->cssToInlineStyles->setEncoding('ISO-8859-1'); - $this->runHTMLToCSS($html, $css, $expected); - } - - public function testXMLHeaderIsRemoved() - { - $html = '

Foo

'; - $expected = << - - -

Foo

- - - -EOF; - $this->cssToInlineStyles->setHTML($html); - $this->cssToInlineStyles->setCSS(''); - $actual = $this->cssToInlineStyles->convert(true); - - $this->assertEquals($expected, $actual); - } - - private function runHTMLToCSS($html, $css, $expected, $asXHTML = false) - { - $cssToInlineStyles = $this->cssToInlineStyles; - $cssToInlineStyles->setHTML($html); - $cssToInlineStyles->setCSS($css); - $output = $cssToInlineStyles->convert($asXHTML); - $actual = $this->stripBody($output, $asXHTML); - $this->assertEquals($expected, $actual); - } - - private function stripBody($html, $asXHTML = false) - { - $dom = new \DOMDocument(); - /*if ($asXHTML) { - $dom->loadXML($html); - } else {*/ - $dom->loadHTML($html); - /*}*/ - $xpath = new \DOMXPath($dom); - $nodelist = $xpath->query('//body/*'); - $result = ''; - for ($i = 0; $i < $nodelist->length; $i++) { - $node = $nodelist->item($i); - if ($asXHTML) { - $result .= $dom->saveXML($node); - } else { - $result .= $dom->saveHTML($node); - } - } - - return $result; - } - - private function findAndSaveNode($html, $query) - { - $dom = new \DOMDocument(); - $dom->loadHTML($html); - $xpath = new \DOMXPath($dom); - $nodelist = $xpath->query($query); - if ($nodelist->length > 0) { - $node = $nodelist->item(0); - - return $dom->saveHTML($node); - } else { - return null; - } - } -} diff --git a/tests/CssToOnlineStylesTest.php b/tests/CssToOnlineStylesTest.php new file mode 100644 index 0000000..aa557d0 --- /dev/null +++ b/tests/CssToOnlineStylesTest.php @@ -0,0 +1,110 @@ +cssToInlineStyles = new CssToInlineStyles(); + } + + public function tearDown() + { + $this->cssToInlineStyles = null; + } + + public function testNoXMLHeaderPresent() + { + $this->assertNotContains( + 'cssToInlineStyles->convert( + '

foo

', + '' + ) + ); + } + + public function testApplyNoStylesOnElement() + { + $document = new \DOMDocument(); + $element = $document->createElement('a', 'foo'); + $inlineElement = $this->cssToInlineStyles->inlineCssOnElement( + $element, + array() + ); + + $document->appendChild($inlineElement); + $this->assertEquals('foo', trim($document->saveHTML())); + } + + public function testApplyBasicStylesOnElement() + { + $document = new \DOMDocument(); + $element = $document->createElement('a', 'foo'); + $inlineElement = $this->cssToInlineStyles->inlineCssOnElement( + $element, + array( + new Property('padding', '5px'), + ) + ); + + $document->appendChild($inlineElement); + + $this->assertEquals('foo', trim($document->saveHTML())); + } + + public function testApplyBasicStylesOnElementWithInlineStyles() + { + $document = new \DOMDocument(); + $element = $document->createElement('a', 'foo'); + $element->setAttribute('style', 'color: green;'); + $inlineElement = $this->cssToInlineStyles->inlineCssOnElement( + $element, + array( + new Property('padding', '5px'), + ) + ); + + $document->appendChild($inlineElement); + + $this->assertEquals( + 'foo', + trim($document->saveHTML()) + ); + } + + public function testBasicRealHTMLExample() + { + $html = '

foo

'; + $css = 'p { color: red; }'; + + $this->assertEquals( + '

foo

', + $this->stripAllStuff( + $this->cssToInlineStyles->convert($html, $css) + ) + ); + } + + private function stripAllStuff($content) + { + $content = str_replace( + array("\n", "\t"), + '', + $content + ); + $content = preg_replace('|(\s)+<|', '<', $content); + $content = preg_replace('|>(\s)+|', '>', $content); + + return $content; + } +} diff --git a/tests/SpecificityTest.php b/tests/SpecificityTest.php deleted file mode 100644 index 44d0644..0000000 --- a/tests/SpecificityTest.php +++ /dev/null @@ -1,83 +0,0 @@ -assertEquals(array(1,2,3), $specificity->getValues()); - } - - public function testIncreaseValue() - { - $specificity = new Specificity(1, 2, 3); - $specificity->increase(1,2,3); - $this->assertEquals(array(2,4,6), $specificity->getValues()); - } - - - /** @dataProvider getCompareTestData */ - public function testCompare(Specificity $a, Specificity $b, $result) - { - $this->assertEquals($result, $a->compareTo($b)); - } - - public function getCompareTestData() - { - return array( - array(new Specificity(0, 0, 0), new Specificity(0, 0, 0), 0), - array(new Specificity(0, 0, 1), new Specificity(0, 0, 1), 0), - array(new Specificity(0, 0, 2), new Specificity(0, 0, 1), 1), - array(new Specificity(0, 0, 2), new Specificity(0, 0, 3), -1), - array(new Specificity(0, 4, 0), new Specificity(0, 4, 0), 0), - array(new Specificity(0, 6, 0), new Specificity(0, 5, 11), 1), - array(new Specificity(0, 7, 0), new Specificity(0, 8, 0), -1), - array(new Specificity(9, 0, 0), new Specificity(9, 0, 0), 0), - array(new Specificity(11, 0, 0), new Specificity(10, 11, 0), 1), - array(new Specificity(12, 11, 0), new Specificity(13, 0, 0), -1), - ); - } - - /** @dataProvider getSelectorData */ - public function testFromSelector($selector, $result) - { - $specificity = Specificity::fromSelector($selector); - $this->assertEquals($result, $specificity->getValues()); - } - - public function getSelectorData() - { - return array( - array("*", array(0,0,0)), - array("li", array(0,0,1)), - array("ul li", array(0,0,2)), - array("ul ol+li", array(0,0,3)), - array("h1 + *[rel=up]", array(0,1,1)), - array("ul ol li.red", array(0,1,3)), - array("li.red.level", array(0,2,1)), - array("#x34y", array(1,0,0)), - ); - } - - /** @dataProvider getSkippedSelectorData */ - public function testSkippedFromSelector($selector, $result) - { - $this->markTestSkipped( - 'Skipping edge cases in CSS' - ); - - $specificity = Specificity::fromSelector($selector); - $this->assertEquals($result, $specificity->getValues()); - } - - public function getSkippedSelectorData() - { - return array( - array("#s12:not(FOO)", array(1,0,1)), - ); - } -} From 658805342ec0fbdd050b676b095c62f9f7f781d8 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 17:34:14 +0100 Subject: [PATCH 05/39] Trying to fix coverage --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2ee4b33..356ad3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,4 +17,8 @@ before_script: - composer install --prefer-source --no-interaction --no-scripts script: - - bin/phpunit --verbose --coverage-text + - vendor/bin/phpunit --verbose --coverage-clover=coverage.clover + +after_success: + - if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]] && [[ "$TRAVIS_PHP_VERSION" != "nightly" ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi + - if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]] && [[ "$TRAVIS_PHP_VERSION" != "nightly" ]]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi From 4c91066f74b28fbb9497b29aec71d2f7e2b622ec Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 17:47:25 +0100 Subject: [PATCH 06/39] Use PHPUnit 4.8 instead of 5.0 as it requires php 5.6 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6f6d1b2..0a27922 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "symfony/css-selector": "^2.7" }, "require-dev": { - "phpunit/phpunit": "~5.0" + "phpunit/phpunit": "~4.8" }, "autoload": { "psr-4": { From 8ab951983aef7ce4f4de78e1f20981fb63e69457 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 17:51:36 +0100 Subject: [PATCH 07/39] Added some badges --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) mode change 100755 => 100644 README.md diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 00d226f..31b0a70 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # CssToInlineStyles class -[![Build Status](https://travis-ci.org/tijsverkoyen/CssToInlineStyles.svg?branch=master)](https://travis-ci.org/tijsverkoyen/CssToInlineStyles) +[![Build Status](https://travis-ci.org/tijsverkoyen/CssToInlineStyles.svg?branch=master)](https://travis-ci.org/tijsverkoyen/CssToInlineStyles) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/tijsverkoyen/CssToInlineStyles/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/tijsverkoyen/CssToInlineStyles/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/tijsverkoyen/CssToInlineStyles/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/tijsverkoyen/CssToInlineStyles/?branch=master) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/5c0ce94f-de6d-403e-9e0a-431268deb75c/mini.png)](https://insight.sensiolabs.com/projects/5c0ce94f-de6d-403e-9e0a-431268deb75c) + +## Installation > CssToInlineStyles is a class that enables you to convert HTML-pages/files into > HTML-pages/files with inline styles. This is very usefull when you're sending From acff8170e4d9df11a724345dc71281cffca24119 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 18:09:23 +0100 Subject: [PATCH 08/39] Fixed some issues detected with SensioLabsInsight --- LICENSE.md | 25 +++++++++++++------------ composer.json | 2 +- src/Css/Property/Processor.php | 3 ++- src/Css/Rule/Processor.php | 2 -- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 81adb07..5f40959 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -7,16 +7,17 @@ modification, are permitted provided that the following conditions are met: 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. -This software is provided by the author "as is" and any express or implied -warranties, including, but not limited to, the implied warranties of -merchantability and fitness for a particular purpose are disclaimed. In no event -shall the author be liable for any direct, indirect, incidental, special, -exemplary, or consequential damages (including, but not limited to, procurement -of substitute goods or services; loss of use, data, or profits; or business -interruption) however caused and on any theory of liability, whether in -contract, strict liability, or tort (including negligence or otherwise) arising -in any way out of the use of this software, even if advised of the possibility -of such damage. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/composer.json b/composer.json index 0a27922..c3de758 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type": "library", "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.", "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", - "license": "BSD", + "license": "BSD-3-Clause", "authors": [ { "name": "Tijs Verkoyen", diff --git a/src/Css/Property/Processor.php b/src/Css/Property/Processor.php index dcbd733..546d9ef 100644 --- a/src/Css/Property/Processor.php +++ b/src/Css/Property/Processor.php @@ -16,8 +16,9 @@ public function splitIntoSeparateProperties($propertiesString) $properties = (array) explode(';', $propertiesString); $keysToRemove = array(); + $numberOfProperties = count($properties); - for ($i = 0; $i < count($properties); $i++) { + for ($i = 0; $i < $numberOfProperties; $i++) { $properties[$i] = trim($properties[$i]); // if the new property begins with base64 it is part of the current property diff --git a/src/Css/Rule/Processor.php b/src/Css/Rule/Processor.php index a7309ef..f347667 100644 --- a/src/Css/Rule/Processor.php +++ b/src/Css/Rule/Processor.php @@ -61,8 +61,6 @@ public function convertToObjects($rule, $orginalOrder) foreach ($selectors as $selector) { $selector = trim($selector); -// $ruleSet['specificity'] = Specificity::fromSelector($selector); - $rules[] = new Rule( $selector, $propertiesProcessor->convertArrayToObjects($properties), From 7fd71521aa9ee590550014d398cdd6e84207aa93 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 18:36:32 +0100 Subject: [PATCH 09/39] Added a test to check if the order is maintained based on #83 --- tests/Css/Rule/ProcessorTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Css/Rule/ProcessorTest.php b/tests/Css/Rule/ProcessorTest.php index 76abee8..b687440 100644 --- a/tests/Css/Rule/ProcessorTest.php +++ b/tests/Css/Rule/ProcessorTest.php @@ -42,4 +42,25 @@ public function testMostBasicRule() $this->assertEquals('block', $rules[0]->getProperties()[1]->getValue()); $this->assertEquals(1, $rules[0]->getOrder()); } + + public function testMaintainOrderOfProperties() + { + $css = <<processor->convertToObjects($css, 1); + + $this->assertCount(1, $rules); + $this->assertInstanceOf('TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule', $rules[0]); + $this->assertEquals('div', $rules[0]->getSelector()); + $this->assertCount(2, $rules[0]->getProperties()); + $this->assertEquals('width', $rules[0]->getProperties()[0]->getName()); + $this->assertEquals('200px', $rules[0]->getProperties()[0]->getValue()); + $this->assertEquals('_width', $rules[0]->getProperties()[1]->getName()); + $this->assertEquals('211px', $rules[0]->getProperties()[1]->getValue()); + $this->assertEquals(1, $rules[0]->getOrder()); + } } From 4e7c36c20f96fcfd62d69f46ce6e567b4b65681d Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 20 Nov 2015 18:43:53 +0100 Subject: [PATCH 10/39] Added some more test to check if all media-queries are stripped. --- tests/Css/ProcessorTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Css/ProcessorTest.php b/tests/Css/ProcessorTest.php index 50a0669..b0c2fd2 100644 --- a/tests/Css/ProcessorTest.php +++ b/tests/Css/ProcessorTest.php @@ -68,4 +68,22 @@ public function testCssWithMediaQueries() $this->assertEquals('red', $rules[0]->getProperties()[0]->getValue()); $this->assertEquals(1, $rules[0]->getOrder()); } + + public function testMakeSureMediaQueriesAreRemoved() + { + $css = '@media tv and (min-width: 700px) and (orientation: landscape) {.foo {display: none;}}'; + $this->assertEmpty($this->processor->getRules($css)); + + $css = '@media (min-width: 700px), handheld and (orientation: landscape) {.foo {display: none;}}'; + $this->assertEmpty($this->processor->getRules($css)); + + $css = '@media not screen and (color), print and (color)'; + $this->assertEmpty($this->processor->getRules($css)); + + $css = '@media screen and (min-aspect-ratio: 1/1) {.foo {display: none;}}'; + $this->assertEmpty($this->processor->getRules($css)); + + $css = '@media screen and (device-aspect-ratio: 16/9), screen and (device-aspect-ratio: 16/10) {.foo {display: none;}}'; + $this->assertEmpty($this->processor->getRules($css)); + } } From 8e2010633f8ddbe7f46b954e85964370cc5f9731 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Mon, 23 Nov 2015 10:58:05 +0100 Subject: [PATCH 11/39] Use the container based infrastructure --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 356ad3b..5020e92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: php +sudo: false + matrix: include: - php: 5.4 From c6359dc626fa3a3f4c752755273ebca711220865 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Mon, 23 Nov 2015 11:03:47 +0100 Subject: [PATCH 12/39] More readable code for converting a string into a property --- src/Css/Property/Processor.php | 13 ++++++++++--- tests/Css/Property/ProcessorTest.php | 6 ++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Css/Property/Processor.php b/src/Css/Property/Processor.php index 546d9ef..ae64f13 100644 --- a/src/Css/Property/Processor.php +++ b/src/Css/Property/Processor.php @@ -63,13 +63,20 @@ protected function cleanup($string) */ public function convertToObject($property) { - $chunks = (array) explode(':', $property, 2); + if (stripos($property, ':') === false) { + return null; + } + + list($name, $value) = explode(':', $property, 2); + + $name = trim($name); + $value = trim($value); - if (!isset($chunks[1]) || $chunks[1] == '') { + if ($value === '') { return null; } - return new Property(trim($chunks[0]), trim($chunks[1])); + return new Property($name, $value); } /** diff --git a/tests/Css/Property/ProcessorTest.php b/tests/Css/Property/ProcessorTest.php index 052e5f9..980cf20 100644 --- a/tests/Css/Property/ProcessorTest.php +++ b/tests/Css/Property/ProcessorTest.php @@ -71,4 +71,10 @@ public function testBuildingPropertiesString() $this->processor->buildPropertiesString($properties) ); } + + public function testFaultyProperties() + { + $this->assertNull($this->processor->convertToObject('foo')); + $this->assertNull($this->processor->convertToObject('foo:')); + } } From ea182c6b03ebf6af74ba458c369384bbf2490416 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Mon, 23 Nov 2015 11:13:08 +0100 Subject: [PATCH 13/39] Fixed issue with comparing Specificity --- src/Css/Rule/Processor.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Css/Rule/Processor.php b/src/Css/Rule/Processor.php index f347667..e45473a 100644 --- a/src/Css/Rule/Processor.php +++ b/src/Css/Rule/Processor.php @@ -97,17 +97,17 @@ public function convertArrayToObjects(array $rules) * Sort an array on the specificity element * * @return int - * @param array $e1 The first element. - * @param array $e2 The second element. + * @param Rule $e1 The first element. + * @param Rule $e2 The second element. */ - private static function sortOnSpecificity($e1, $e2) + private static function sortOnSpecificity(Rule $e1, Rule $e2) { - // Compare the specificity - $value = $e1['specificity']->compareTo($e2['specificity']); + $e1Specificity = ($e1->getSpecificity()); + $value = $e1Specificity->compareTo($e2->getSpecificity()); // if the specificity is the same, use the order in which the element appeared if ($value === 0) { - $value = $e1['order'] - $e2['order']; + $value = $e1->getOrder() - $e2->getOrder(); } return $value; From e3b73228610e4e5782f432361efae8508fcf935f Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Mon, 23 Nov 2015 11:20:53 +0100 Subject: [PATCH 14/39] Fixed some PHPDoc --- src/Css/Processor.php | 6 +++--- src/Css/Property/Processor.php | 4 ++-- src/Css/Property/Property.php | 2 +- src/Css/Rule/Processor.php | 4 ++-- src/Css/Rule/Rule.php | 18 ++++++++++++------ 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/Css/Processor.php b/src/Css/Processor.php index 9bb8e43..d6f949a 100644 --- a/src/Css/Processor.php +++ b/src/Css/Processor.php @@ -22,10 +22,10 @@ public function getRules($css) } /** - * @param $css - * @return mixed|string + * @param string $css + * @return string */ - protected function doCleanup($css) + private function doCleanup($css) { // remove media queries $css = preg_replace('/@media [^{]*{([^{}]|{[^{}]*})*}/', '', $css); diff --git a/src/Css/Property/Processor.php b/src/Css/Property/Processor.php index ae64f13..4a8e0d4 100644 --- a/src/Css/Property/Processor.php +++ b/src/Css/Property/Processor.php @@ -41,7 +41,7 @@ public function splitIntoSeparateProperties($propertiesString) * @param $string * @return mixed|string */ - protected function cleanup($string) + private function cleanup($string) { $string = str_replace(array("\r", "\n"), '', $string); $string = str_replace(array("\t"), ' ', $string); @@ -83,7 +83,7 @@ public function convertToObject($property) * Convert an array of property-strings into objects * * @param array $properties - * @return array + * @return Property[] */ public function convertArrayToObjects(array $properties) { diff --git a/src/Css/Property/Property.php b/src/Css/Property/Property.php index 1e83f35..dbc94a6 100644 --- a/src/Css/Property/Property.php +++ b/src/Css/Property/Property.php @@ -2,7 +2,7 @@ namespace TijsVerkoyen\CssToInlineStyles\Css\Property; -class Property +final class Property { /** * @var string diff --git a/src/Css/Rule/Processor.php b/src/Css/Rule/Processor.php index e45473a..33f5335 100644 --- a/src/Css/Rule/Processor.php +++ b/src/Css/Rule/Processor.php @@ -24,7 +24,7 @@ public function splitIntoSeparateRules($rulesString) * @param $string * @return mixed|string */ - protected function cleanup($string) + private function cleanup($string) { $string = str_replace(array("\r", "\n"), '', $string); $string = str_replace(array("\t"), ' ', $string); @@ -74,7 +74,7 @@ public function convertToObjects($rule, $orginalOrder) /** * @param array $rules - * @return array + * @return Rule[] */ public function convertArrayToObjects(array $rules) { diff --git a/src/Css/Rule/Rule.php b/src/Css/Rule/Rule.php index 2ddcccd..4130991 100644 --- a/src/Css/Rule/Rule.php +++ b/src/Css/Rule/Rule.php @@ -2,6 +2,8 @@ namespace TijsVerkoyen\CssToInlineStyles\Css\Rule; +use TijsVerkoyen\CssToInlineStyles\Css\Specificity\Specificity; + final class Rule { /** @@ -14,6 +16,9 @@ final class Rule */ private $properties; + /** + * @var Specificity + */ private $specificity; /** @@ -23,12 +28,13 @@ final class Rule /** * Rule constructor. - * @param string $selector - * @param array $properties - * @param $specificity - * @param int $order + * + * @param string $selector + * @param array $properties + * @param Specificity $specificity + * @param int $order */ - public function __construct($selector, array $properties, $specificity, $order) + public function __construct($selector, array $properties, Specificity $specificity, $order) { $this->selector = $selector; $this->properties = $properties; @@ -59,7 +65,7 @@ public function getProperties() /** * Get specificity * - * @return mixed + * @return Specificity */ public function getSpecificity() { From d1452b451fcc1258ca0ed1b3dad72337787db646 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Mon, 23 Nov 2015 11:25:01 +0100 Subject: [PATCH 15/39] Skip empty properties --- src/Css/Property/Processor.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Css/Property/Processor.php b/src/Css/Property/Processor.php index 4a8e0d4..27cd3ad 100644 --- a/src/Css/Property/Processor.php +++ b/src/Css/Property/Processor.php @@ -90,6 +90,11 @@ public function convertArrayToObjects(array $properties) $objects = array(); foreach ($properties as $property) { + $object = $this->convertToObject($property); + if ($object === null) { + continue; + } + $objects[] = $this->convertToObject($property); } From d48d4f399f4416510ef70446244c2fe53eb62632 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Mon, 23 Nov 2015 11:26:36 +0100 Subject: [PATCH 16/39] More performant code --- src/Css/Property/Property.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Css/Property/Property.php b/src/Css/Property/Property.php index dbc94a6..320c3dc 100644 --- a/src/Css/Property/Property.php +++ b/src/Css/Property/Property.php @@ -53,7 +53,7 @@ public function getValue() */ public function isImportant() { - return (stristr($this->value, '!important') !== false); + return (stripos($this->value, '!important') !== false); } /** From b3834407de6be247fe23f22a8f40a188d547afaa Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Mon, 23 Nov 2015 11:26:44 +0100 Subject: [PATCH 17/39] Better method-name --- tests/CssToOnlineStylesTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CssToOnlineStylesTest.php b/tests/CssToOnlineStylesTest.php index aa557d0..8cdd8f9 100644 --- a/tests/CssToOnlineStylesTest.php +++ b/tests/CssToOnlineStylesTest.php @@ -89,13 +89,13 @@ public function testBasicRealHTMLExample() $this->assertEquals( '

foo

', - $this->stripAllStuff( + $this->stripAllWhitespaces( $this->cssToInlineStyles->convert($html, $css) ) ); } - private function stripAllStuff($content) + private function stripAllWhitespaces($content) { $content = str_replace( array("\n", "\t"), From fa040ce760b7c981b51209af6e24f44817d39899 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Mon, 23 Nov 2015 11:42:05 +0100 Subject: [PATCH 18/39] Fixed a stupid mistake where I called a method twice --- src/Css/Property/Processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Css/Property/Processor.php b/src/Css/Property/Processor.php index 27cd3ad..eee0eba 100644 --- a/src/Css/Property/Processor.php +++ b/src/Css/Property/Processor.php @@ -95,7 +95,7 @@ public function convertArrayToObjects(array $properties) continue; } - $objects[] = $this->convertToObject($property); + $objects[] = $object; } return $objects; From 06dd977cc5812f1f320b42d87911bb02f740d1bb Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 24 Nov 2015 14:22:00 +0100 Subject: [PATCH 19/39] : doesn't have a cased variant --- src/Css/Property/Processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Css/Property/Processor.php b/src/Css/Property/Processor.php index eee0eba..5f97de8 100644 --- a/src/Css/Property/Processor.php +++ b/src/Css/Property/Processor.php @@ -63,7 +63,7 @@ private function cleanup($string) */ public function convertToObject($property) { - if (stripos($property, ':') === false) { + if (strpos($property, ':') === false) { return null; } From 10ec2eb930c474aa4127ec2f503dfe0114098222 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 24 Nov 2015 14:40:05 +0100 Subject: [PATCH 20/39] Better PHPdoc --- src/Css/Processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Css/Processor.php b/src/Css/Processor.php index d6f949a..8abd870 100644 --- a/src/Css/Processor.php +++ b/src/Css/Processor.php @@ -10,7 +10,7 @@ class Processor * Get the rules from a given CSS-string * * @param string $css - * @return array + * @return Rule[] */ public function getRules($css) { From e1d0d5ed826e42c1eaf8dad451ae1cc4eae35536 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 24 Nov 2015 14:54:45 +0100 Subject: [PATCH 21/39] Use inline styles if they are present in $html --- src/Css/Processor.php | 21 +++++++++++++++++++++ src/CssToInlineStyles.php | 28 ++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/Css/Processor.php b/src/Css/Processor.php index 8abd870..2a52d07 100644 --- a/src/Css/Processor.php +++ b/src/Css/Processor.php @@ -21,6 +21,27 @@ public function getRules($css) return $rulesProcessor->convertArrayToObjects($rules); } + /** + * Get the CSS from the style-tags in the given HTML-string + * + * @param string $html + * @return string + */ + public function getCssFromStyleTags($html) + { + $css = ''; + $matches = array(); + preg_match_all('|(.*)|isU', $html, $matches); + + if (!empty($matches[2])) { + foreach ($matches[2] as $match) { + $css .= trim($match) . "\n"; + } + } + + return $css; + } + /** * @param string $css * @return string diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index 927a305..dbe6497 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -9,10 +9,34 @@ class CssToInlineStyles { - public function convert($html, $css) + /** + * Will inline the $css into the given $html + * + * Remark: if the html contains + + +

foo

+ + +EOF + )); + } + + public function testMultipleStyleTagsInHtml() + { + $expected = 'p { color: #F00; }' . "\n" . "p { color: #0F0; }"; + $this->assertEquals( + $expected, + $this->processor->getCssFromStyleTags( + << + + + + + +

foo

+ + +EOF + )); + } } From 22e6c63c22d404b60ea53ab701b8d077199a3365 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 24 Nov 2015 17:28:25 +0100 Subject: [PATCH 24/39] Tests --- tests/Css/ProcessorTest.php | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/Css/ProcessorTest.php b/tests/Css/ProcessorTest.php index fdc2e50..f286da5 100644 --- a/tests/Css/ProcessorTest.php +++ b/tests/Css/ProcessorTest.php @@ -93,42 +93,44 @@ public function testSimpleStyleTagsInHtml() $this->assertEquals( $expected, $this->processor->getCssFromStyleTags( -<< + << - +

foo

EOF - )); + ) + ); } public function testMultipleStyleTagsInHtml() { - $expected = 'p { color: #F00; }' . "\n" . "p { color: #0F0; }"; + $expected = 'p { color: #F00; }' . "\n" . 'p { color: #0F0; }' . "\n"; $this->assertEquals( $expected, $this->processor->getCssFromStyleTags( << - + - +

foo

EOF - )); + ) + ); } } From ec045cab0f2447485edb40effe2d8e2c1143f7ea Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Fri, 27 Nov 2015 15:29:05 +0100 Subject: [PATCH 25/39] Append to existing rules --- src/Css/Processor.php | 6 +++--- src/Css/Rule/Processor.php | 8 +------- src/CssToInlineStyles.php | 18 +++++++++--------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/Css/Processor.php b/src/Css/Processor.php index 2a52d07..f00b708 100644 --- a/src/Css/Processor.php +++ b/src/Css/Processor.php @@ -10,15 +10,15 @@ class Processor * Get the rules from a given CSS-string * * @param string $css + * @param array $existingRules * @return Rule[] */ - public function getRules($css) + public function getRules($css, $existingRules = array()) { $css = $this->doCleanup($css); $rulesProcessor = new RuleProcessor(); $rules = $rulesProcessor->splitIntoSeparateRules($css); - - return $rulesProcessor->convertArrayToObjects($rules); + return $rulesProcessor->convertArrayToObjects($rules, $existingRules); } /** diff --git a/src/Css/Rule/Processor.php b/src/Css/Rule/Processor.php index 33f5335..610fd7c 100644 --- a/src/Css/Rule/Processor.php +++ b/src/Css/Rule/Processor.php @@ -76,20 +76,14 @@ public function convertToObjects($rule, $orginalOrder) * @param array $rules * @return Rule[] */ - public function convertArrayToObjects(array $rules) + public function convertArrayToObjects(array $rules, array $objects = array()) { - $objects = array(); - $order = 1; foreach ($rules as $rule) { $objects = array_merge($objects, $this->convertToObjects($rule, $order)); $order++; } - if (!empty($objects)) { - usort($objects, array(__CLASS__, 'sortOnSpecificity')); - } - return $objects; } diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index dbe6497..1d9eef7 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -3,6 +3,7 @@ namespace TijsVerkoyen\CssToInlineStyles; use Symfony\Component\CssSelector\CssSelector; +use Symfony\Component\CssSelector\Exception\SyntaxErrorException; use TijsVerkoyen\CssToInlineStyles\Css\Processor; use TijsVerkoyen\CssToInlineStyles\Css\Property\Processor as PropertyProcessor; use TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule; @@ -30,13 +31,8 @@ public function convert($html, $css = null) ); if ($css !== null) { - // add the provided styles - $rules = array_merge( - $rules, - $processor->getRules($css) - ); + $rules = $processor->getRules($css, $rules); } - $document = $this->inline($document, $rules); return $this->getHtmlFromDocument($document); @@ -135,9 +131,13 @@ protected function inline(\DOMDocument $document, array $rules) foreach ($rules as $rule) { /** @var Rule $rule */ - $elements = $xPath->query( - CssSelector::toXPath($rule->getSelector()) - ); + try { + $expression = CssSelector::toXPath($rule->getSelector()); + } catch(SyntaxErrorException $e) { + continue; + } + + $elements = $xPath->query($expression); if ($elements === false) { continue; From e6d3d62b8271ccf5eb2967646b09cfce575bc069 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 12:04:33 +0100 Subject: [PATCH 26/39] Rewrote a big part to respect specificity * The Specificity-class looks a lot more like the Symfony one, this will be refactored in the future * The original Specificity is passed into the property, so it can be used for calculations * While inline everything is stored in the DOMDocument as base64 in a data-attribute. --- src/Css/Property/Processor.php | 10 +- src/Css/Property/Property.php | 26 +++- src/Css/Rule/Processor.php | 11 +- src/Css/Specificity/Specificity.php | 109 ++++++++--------- src/CssToInlineStyles.php | 137 ++++++++++++++++++---- tests/Css/Specificity/SpecificityTest.php | 22 ++-- tests/CssToInlineStylesTest.php | 131 +++++++++++++++++++-- 7 files changed, 331 insertions(+), 115 deletions(-) diff --git a/src/Css/Property/Processor.php b/src/Css/Property/Processor.php index 5f97de8..876d913 100644 --- a/src/Css/Property/Processor.php +++ b/src/Css/Property/Processor.php @@ -2,6 +2,8 @@ namespace TijsVerkoyen\CssToInlineStyles\Css\Property; +use TijsVerkoyen\CssToInlineStyles\Css\Specificity\Specificity; + class Processor { /** @@ -61,7 +63,7 @@ private function cleanup($string) * @param string $property * @return Property|null */ - public function convertToObject($property) + public function convertToObject($property, Specificity $specificity = null) { if (strpos($property, ':') === false) { return null; @@ -76,7 +78,7 @@ public function convertToObject($property) return null; } - return new Property($name, $value); + return new Property($name, $value, $specificity); } /** @@ -85,12 +87,12 @@ public function convertToObject($property) * @param array $properties * @return Property[] */ - public function convertArrayToObjects(array $properties) + public function convertArrayToObjects(array $properties, Specificity $specificity = null) { $objects = array(); foreach ($properties as $property) { - $object = $this->convertToObject($property); + $object = $this->convertToObject($property, $specificity); if ($object === null) { continue; } diff --git a/src/Css/Property/Property.php b/src/Css/Property/Property.php index 320c3dc..fa69a1a 100644 --- a/src/Css/Property/Property.php +++ b/src/Css/Property/Property.php @@ -2,6 +2,8 @@ namespace TijsVerkoyen\CssToInlineStyles\Css\Property; +use TijsVerkoyen\CssToInlineStyles\Css\Specificity\Specificity; + final class Property { /** @@ -14,16 +16,22 @@ final class Property */ private $value; + /** + * @var Specificity + */ + private $originalSpecificity; + /** * Property constructor. - * - * @param string $name - * @param string $value + * @param $name + * @param $value + * @param Specificity|null $specificity */ - public function __construct($name, $value) + public function __construct($name, $value, Specificity $specificity = null) { $this->name = $name; $this->value = $value; + $this->originalSpecificity = $specificity; } /** @@ -46,6 +54,16 @@ public function getValue() return $this->value; } + /** + * Get originalSpecificity + * + * @return Specificity + */ + public function getOriginalSpecificity() + { + return $this->originalSpecificity; + } + /** * Is this property important? * diff --git a/src/Css/Rule/Processor.php b/src/Css/Rule/Processor.php index 610fd7c..9b95ab3 100644 --- a/src/Css/Rule/Processor.php +++ b/src/Css/Rule/Processor.php @@ -42,10 +42,10 @@ private function cleanup($string) * Convert a rule-string into an object * * @param string $rule - * @param int $orginalOrder + * @param int $originalOrder * @return array */ - public function convertToObjects($rule, $orginalOrder) + public function convertToObjects($rule, $originalOrder) { $rule = $this->cleanup($rule); @@ -60,12 +60,13 @@ public function convertToObjects($rule, $orginalOrder) foreach ($selectors as $selector) { $selector = trim($selector); + $specificity = Specificity::fromSelector($selector); $rules[] = new Rule( $selector, - $propertiesProcessor->convertArrayToObjects($properties), - Specificity::fromSelector($selector), - $orginalOrder + $propertiesProcessor->convertArrayToObjects($properties, $specificity), + $specificity, + $originalOrder ); } diff --git a/src/Css/Specificity/Specificity.php b/src/Css/Specificity/Specificity.php index 13e5f3b..22d95ff 100644 --- a/src/Css/Specificity/Specificity.php +++ b/src/Css/Specificity/Specificity.php @@ -4,72 +4,82 @@ class Specificity { + const A_FACTOR = 100; + const B_FACTOR = 10; + const C_FACTOR = 1; + /** - * The number of ID selectors in the selector - * * @var int */ - private $numberOfIdSelectors; + private $a; /** - * - * The number of class selectors, attributes selectors, and pseudo-classes in the selector - * * @var int */ - private $numberOfClassAttributesPseudoClassSelectors; + private $b; /** - * The number of type selectors and pseudo-elements in the selector - * * @var int */ - private $numberOfTypePseudoElementSelectors; + private $c; /** - * @param int $numberOfIdSelectors The number of ID selectors in the selector - * @param int $numberOfClassAttributesPseudoClassSelectors The number of class selectors, attributes selectors, and pseudo-classes in the selector - * @param int $numberOfTypePseudoElementSelectors The number of type selectors and pseudo-elements in the selector + * Constructor. + * + * @param int $a + * @param int $b + * @param int $c */ - public function __construct( - $numberOfIdSelectors = 0, - $numberOfClassAttributesPseudoClassSelectors = 0, - $numberOfTypePseudoElementSelectors = 0 - ) { - $this->numberOfIdSelectors = $numberOfIdSelectors; - $this->numberOfClassAttributesPseudoClassSelectors = $numberOfClassAttributesPseudoClassSelectors; - $this->numberOfTypePseudoElementSelectors = $numberOfTypePseudoElementSelectors; + public function __construct($a, $b, $c) + { + $this->a = $a; + $this->b = $b; + $this->c = $c; } /** - * Increase the current specificity by adding the three values + * @param Specificity $specificity * - * @param int $numberOfIdSelectors The number of ID selectors in the selector - * @param int $numberOfClassAttributesPseudoClassSelectors The number of class selectors, attributes selectors, and pseudo-classes in the selector - * @param int $numberOfTypePseudoElementSelectors The number of type selectors and pseudo-elements in the selector + * @return Specificity */ - public function increase( - $numberOfIdSelectors, - $numberOfClassAttributesPseudoClassSelectors, - $numberOfTypePseudoElementSelectors - ) { - $this->numberOfIdSelectors += $numberOfIdSelectors; - $this->numberOfClassAttributesPseudoClassSelectors += $numberOfClassAttributesPseudoClassSelectors; - $this->numberOfTypePseudoElementSelectors += $numberOfTypePseudoElementSelectors; + public function plus(Specificity $specificity) + { + return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c); } /** - * Get the specificity values as an array + * Returns global specificity value. * - * @return array + * @return int */ - public function getValues() + public function getValue() { - return array( - $this->numberOfIdSelectors, - $this->numberOfClassAttributesPseudoClassSelectors, - $this->numberOfTypePseudoElementSelectors - ); + return $this->a * self::A_FACTOR + $this->b * self::B_FACTOR + $this->c * self::C_FACTOR; + } + + /** + * Returns -1 if the object specificity is lower than the argument, + * 0 if they are equal, and 1 if the argument is lower. + * + * @param Specificity $specificity + * + * @return int + */ + public function compareTo(Specificity $specificity) + { + if ($this->a !== $specificity->a) { + return $this->a > $specificity->a ? 1 : -1; + } + + if ($this->b !== $specificity->b) { + return $this->b > $specificity->b ? 1 : -1; + } + + if ($this->c !== $specificity->c) { + return $this->c > $specificity->c ? 1 : -1; + } + + return 0; } /** @@ -115,21 +125,4 @@ public static function fromSelector($selector) preg_match_all("/{$typePseudoElementsSelectorPattern}/ix", $selector, $matches) ); } - - /** - * Returns <0 when $specificity is greater, 0 when equal, >0 when smaller - * - * @param Specificity $specificity - * @return int - */ - public function compareTo(Specificity $specificity) - { - if ($this->numberOfIdSelectors !== $specificity->numberOfIdSelectors) { - return $this->numberOfIdSelectors - $specificity->numberOfIdSelectors; - } elseif ($this->numberOfClassAttributesPseudoClassSelectors !== $specificity->numberOfClassAttributesPseudoClassSelectors) { - return $this->numberOfClassAttributesPseudoClassSelectors - $specificity->numberOfClassAttributesPseudoClassSelectors; - } else { - return $this->numberOfTypePseudoElementSelectors - $specificity->numberOfTypePseudoElementSelectors; - } - } } diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index 1d9eef7..c13f352 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -33,16 +33,17 @@ public function convert($html, $css = null) if ($css !== null) { $rules = $processor->getRules($css, $rules); } + $document = $this->inline($document, $rules); return $this->getHtmlFromDocument($document); } /** - * Inline the given properties on a DOMElement + * Inle the given properties on an given DOMElement * - * @param \DOMElement $element - * @param array $properties + * @param \DOMElement $element + * @param Css\Property\Property[] $properties * @return \DOMElement */ public function inlineCssOnElement(\DOMElement $element, array $properties) @@ -51,39 +52,47 @@ public function inlineCssOnElement(\DOMElement $element, array $properties) return $element; } - $propertyProcessor = new PropertyProcessor(); $cssProperties = array(); - $currentStyles = $element->attributes->getNamedItem('style'); - - if ($currentStyles !== null) { - $currentProperties = $propertyProcessor->convertArrayToObjects( - $propertyProcessor->splitIntoSeparateProperties($currentStyles->value) - ); + $inlineProperties = $this->getInlineStyles($element); - foreach ($currentProperties as $property) { + if (!empty($inlineProperties)) { + foreach ($inlineProperties as $property) { $cssProperties[$property->getName()] = $property; } } foreach ($properties as $property) { - if (isset($cssProperties[$property->getName()])) { - // only overrule it if the the not-inline-style property is important - if ($property->isImportant()) { - $cssProperties[$property->getName()] = $property; - } - } else { + if (!isset($cssProperties[$property->getName()])) { $cssProperties[$property->getName()] = $property; } } - $element->setAttribute( - 'style', - $propertyProcessor->buildPropertiesString(array_values($cssProperties)) - ); + $rules = array(); + foreach ($cssProperties as $property) { + $rules[] = $property->toString(); + } + $element->setAttribute('style', implode(' ', $rules)); return $element; } + /** + * Get the current inline styles for a given DOMElement + * + * @param \DOMElement $element + * @return Css\Property\Property[] + */ + public function getInlineStyles(\DOMElement $element) + { + $processor = new PropertyProcessor(); + + return $processor->convertArrayToObjects( + $processor->splitIntoSeparateProperties( + $element->getAttribute('style') + ) + ); + } + /** * @param string $html * @return \DOMDocument @@ -130,10 +139,9 @@ protected function inline(\DOMDocument $document, array $rules) $xPath = new \DOMXPath($document); foreach ($rules as $rule) { /** @var Rule $rule */ - try { $expression = CssSelector::toXPath($rule->getSelector()); - } catch(SyntaxErrorException $e) { + } catch (SyntaxErrorException $e) { continue; } @@ -144,10 +152,91 @@ protected function inline(\DOMDocument $document, array $rules) } foreach ($elements as $element) { - $this->inlineCssOnElement($element, $rule->getProperties()); + $this->calculatePropertiesToBeApplied($element, $rule->getProperties()); + } + } + + $elements = $xPath->query('//*[@data-css-to-inline-styles]'); + + foreach ($elements as $element) { + $propertiesToBeApplied = $element->attributes->getNamedItem('data-css-to-inline-styles'); + $element->removeAttribute('data-css-to-inline-styles'); + + if ($propertiesToBeApplied !== null) { + $properties = unserialize(base64_decode($propertiesToBeApplied->value)); + $this->inlineCssOnElement($element, $properties); } } return $document; } + + /** + * Store the calculated values in a temporary data-attribute + * + * @param \DOMElement $element + * @param Property[] $properties + * @return \DOMElement + */ + protected function calculatePropertiesToBeApplied( + \DOMElement $element, + array $properties + ) { + if (empty($properties)) { + return $element; + } + + $cssProperties = array(); + $currentStyles = $element->attributes->getNamedItem('data-css-to-inline-styles'); + + if ($currentStyles !== null) { + $currentProperties = unserialize( + base64_decode( + $currentStyles->value + ) + ); + + foreach ($currentProperties as $property) { + $cssProperties[$property->getName()] = $property; + } + } + + foreach ($properties as $property) { + if (isset($cssProperties[$property->getName()])) { + $existingProperty = $cssProperties[$property->getName()]; + + if ( + ($existingProperty->isImportant() && $property->isImportant()) && + ($property->getOriginalSpecificity()->getValue() >= + $existingProperty->getOriginalSpecificity()->getValue()) + ) { + // if both the properties are important we should use the specificity + $cssProperties[$property->getName()] = $property; + } elseif (!$existingProperty->isImportant() && $property->isImportant()) { + // if the existing property is not important but the new one is, it should be overruled + $cssProperties[$property->getName()] = $property; + } elseif ( + !$existingProperty->isImportant() && + ($property->getOriginalSpecificity()->getValue() >= + $existingProperty->getOriginalSpecificity()->getValue()) + ) { + // if the existing propert is not important we should check the specificity + $cssProperties[$property->getName()] = $property; + } + } else { + $cssProperties[$property->getName()] = $property; + } + } + + $element->setAttribute( + 'data-css-to-inline-styles', + base64_encode( + serialize( + array_values($cssProperties) + ) + ) + ); + + return $element; + } } diff --git a/tests/Css/Specificity/SpecificityTest.php b/tests/Css/Specificity/SpecificityTest.php index 1fe57db..96a8140 100644 --- a/tests/Css/Specificity/SpecificityTest.php +++ b/tests/Css/Specificity/SpecificityTest.php @@ -8,12 +8,14 @@ class PropertyTest extends \PHPUnit_Framework_TestCase { public function testIncreaseMethodShouldIncreaseAllWithOne() { - $instance = new Specificity(); - $instance->increase(1, 1, 1); + $instance = new Specificity(0, 0, 0); + $instance = $instance->plus( + new Specificity(1, 1, 1) + ); $this->assertEquals( - array(1, 1, 1), - $instance->getValues() + 111, + $instance->getValue() ); } @@ -53,24 +55,24 @@ public function testCompareEqualItems() public function testSingleIdSelector() { $this->assertEquals( - array(1, 0, 0), - Specificity::fromSelector('#foo')->getValues() + 100, + Specificity::fromSelector('#foo')->getValue() ); } public function testSingleClassSelector() { $this->assertEquals( - array(0, 1, 0), - Specificity::fromSelector('.foo')->getValues() + 10, + Specificity::fromSelector('.foo')->getValue() ); } public function testSingleElementSelector() { $this->assertEquals( - array(0, 0, 1), - Specificity::fromSelector('a')->getValues() + 1, + Specificity::fromSelector('a')->getValue() ); } } diff --git a/tests/CssToInlineStylesTest.php b/tests/CssToInlineStylesTest.php index 8cdd8f9..a80b33e 100644 --- a/tests/CssToInlineStylesTest.php +++ b/tests/CssToInlineStylesTest.php @@ -86,25 +86,136 @@ public function testBasicRealHTMLExample() { $html = '

foo

'; $css = 'p { color: red; }'; + $expected = '

foo

'; + $this->assertCorrectConversion($expected, $html, $css); + } + + public function testSimpleElementSelector() + { + $html = '
'; + $css = 'div { display: none; }'; + $expected = '
'; + + $this->assertCorrectConversion($expected, $html, $css); + } + + public function testSimpleCssSelector() + { + $html = 'nodeContent'; + $css = '.test-class { background-color: #aaa; text-decoration: none; }'; + $expected = 'nodeContent'; + + $this->assertCorrectConversion($expected, $html, $css); + } + + public function testSimpleIdSelector() + { + $html = '
'; + $css = '#div1 { border: 1px solid red; }'; + $expected = '
'; + + $this->assertCorrectConversion($expected, $html, $css); + } + + public function testInlineStylesBlock() + { + $html = << + a { + padding: 10px; + margin: 0; + } + + +EOF; + $expected = ''; + + $this->assertCorrectConversion($expected, $html); + } + + public function testSpecificity() + { + $html = << + + +EOF; + $css = << + +EOF; $this->assertEquals( - '

foo

', - $this->stripAllWhitespaces( + $expected, + $this->getBodyContent( $this->cssToInlineStyles->convert($html, $css) ) ); } - private function stripAllWhitespaces($content) + public function testEqualSpecificity() { - $content = str_replace( - array("\n", "\t"), - '', - $content + $html = '
'; + $css = ' .one { display: inline; } a > strong {} a {} a {} a {} a {} a {} a {}a {} img { display: block; }'; + $expected = '
'; + + $this->assertCorrectConversion($expected, $html, $css); + } + + public function testInvalidSelector() + { + $html = "

"; + $css = ' p&@*$%& { display: inline; }'; + $expected = $html; + + $this->assertCorrectConversion($expected, $html, $css); + } + + private function assertCorrectConversion($expected, $html, $css = null) + { + $this->assertEquals( + $expected, + $this->getBodyContent( + $this->cssToInlineStyles->convert($html, $css) + ) ); - $content = preg_replace('|(\s)+<|', '<', $content); - $content = preg_replace('|>(\s)+|', '>', $content); + } + + private function getBodyContent($html) + { + $matches = array(); + preg_match('|(.*)|ims', $html, $matches); + + if (!isset($matches[1])) { + return null; + } - return $content; + return trim($matches[1]); } } From 560c9f7b28f5d84f89e978c7019db37012de1de4 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 12:59:51 +0100 Subject: [PATCH 27/39] Cleanup tests --- tests/CssToInlineStylesTest.php | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/CssToInlineStylesTest.php b/tests/CssToInlineStylesTest.php index a80b33e..7e30253 100644 --- a/tests/CssToInlineStylesTest.php +++ b/tests/CssToInlineStylesTest.php @@ -121,13 +121,19 @@ public function testSimpleIdSelector() public function testInlineStylesBlock() { $html = << - a { - padding: 10px; - margin: 0; - } - - + + + + + + + + EOF; $expected = ''; @@ -171,12 +177,7 @@ public function testSpecificity() EOF; - $this->assertEquals( - $expected, - $this->getBodyContent( - $this->cssToInlineStyles->convert($html, $css) - ) - ); + $this->assertCorrectConversion($expected, $html, $css); } public function testEqualSpecificity() From b1c0d16d35d374489ef993a1212d7bf6f73d5771 Mon Sep 17 00:00:00 2001 From: Scrutinizer Auto-Fixer Date: Tue, 8 Dec 2015 12:22:00 +0000 Subject: [PATCH 28/39] Scrutinizer Auto-Fixes This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com --- src/Css/Rule/Processor.php | 4 ++-- src/CssToInlineStyles.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Css/Rule/Processor.php b/src/Css/Rule/Processor.php index 9b95ab3..ae9f9b7 100644 --- a/src/Css/Rule/Processor.php +++ b/src/Css/Rule/Processor.php @@ -21,8 +21,8 @@ public function splitIntoSeparateRules($rulesString) } /** - * @param $string - * @return mixed|string + * @param string $string + * @return string */ private function cleanup($string) { diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index c13f352..8e7cd50 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -208,7 +208,7 @@ protected function calculatePropertiesToBeApplied( if ( ($existingProperty->isImportant() && $property->isImportant()) && ($property->getOriginalSpecificity()->getValue() >= - $existingProperty->getOriginalSpecificity()->getValue()) + $existingProperty->getOriginalSpecificity()->getValue()) ) { // if both the properties are important we should use the specificity $cssProperties[$property->getName()] = $property; @@ -218,7 +218,7 @@ protected function calculatePropertiesToBeApplied( } elseif ( !$existingProperty->isImportant() && ($property->getOriginalSpecificity()->getValue() >= - $existingProperty->getOriginalSpecificity()->getValue()) + $existingProperty->getOriginalSpecificity()->getValue()) ) { // if the existing propert is not important we should check the specificity $cssProperties[$property->getName()] = $property; From 174bb9ea7516ca86c5edfd96faec570f7d064f2b Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 15:25:19 +0100 Subject: [PATCH 29/39] Processed some feedback * some about style * some about improving the code * some about improving this package --- .travis.yml | 5 +---- README.md | 1 + phpunit.xml.dist | 10 ---------- src/Css/Rule/Processor.php | 2 +- src/CssToInlineStyles.php | 4 ++-- 5 files changed, 5 insertions(+), 17 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5020e92..7010c1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,14 +4,11 @@ sudo: false matrix: include: - - php: 5.4 - php: 5.5 - php: 5.6 - - php: 7 - - php: nightly + - php: 7.0 - php: hhvm allow_failures: - - php: nightly - php: hhvm before_script: diff --git a/README.md b/README.md index 31b0a70..0a4d5d7 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ $ composer require tijsverkoyen/css-to-inline-styles ## Known issues * no support for pseudo selectors +* no support for [css-escapes](https://mathiasbynens.be/notes/css-escapes) * UTF-8 charset is not always detected correctly. Make sure you set the charset to UTF-8 using the following meta-tag in the head: ``. _(Note: using `` does NOT work!)_ ## Sites using this class diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b08d7f6..da5f589 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,14 +15,4 @@ ./tests/ - - - - src - - tests - vendor - - - diff --git a/src/Css/Rule/Processor.php b/src/Css/Rule/Processor.php index ae9f9b7..87ef8d7 100644 --- a/src/Css/Rule/Processor.php +++ b/src/Css/Rule/Processor.php @@ -97,7 +97,7 @@ public function convertArrayToObjects(array $rules, array $objects = array()) */ private static function sortOnSpecificity(Rule $e1, Rule $e2) { - $e1Specificity = ($e1->getSpecificity()); + $e1Specificity = $e1->getSpecificity(); $value = $e1Specificity->compareTo($e2->getSpecificity()); // if the specificity is the same, use the order in which the element appeared diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index 8e7cd50..ba92de9 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -42,7 +42,7 @@ public function convert($html, $css = null) /** * Inle the given properties on an given DOMElement * - * @param \DOMElement $element + * @param \DOMElement $element * @param Css\Property\Property[] $properties * @return \DOMElement */ @@ -178,7 +178,7 @@ protected function inline(\DOMDocument $document, array $rules) * @param Property[] $properties * @return \DOMElement */ - protected function calculatePropertiesToBeApplied( + private function calculatePropertiesToBeApplied( \DOMElement $element, array $properties ) { From b09df2a88ac4c86d577827bf223d20749b68acb7 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 15:35:34 +0100 Subject: [PATCH 30/39] Extend Symfony\Component\CssSelector\Node\Specificity --- src/Css/Specificity/Specificity.php | 80 +---------------------------- 1 file changed, 1 insertion(+), 79 deletions(-) diff --git a/src/Css/Specificity/Specificity.php b/src/Css/Specificity/Specificity.php index 22d95ff..54af0af 100644 --- a/src/Css/Specificity/Specificity.php +++ b/src/Css/Specificity/Specificity.php @@ -2,86 +2,8 @@ namespace TijsVerkoyen\CssToInlineStyles\Css\Specificity; -class Specificity +class Specificity extends \Symfony\Component\CssSelector\Node\Specificity { - const A_FACTOR = 100; - const B_FACTOR = 10; - const C_FACTOR = 1; - - /** - * @var int - */ - private $a; - - /** - * @var int - */ - private $b; - - /** - * @var int - */ - private $c; - - /** - * Constructor. - * - * @param int $a - * @param int $b - * @param int $c - */ - public function __construct($a, $b, $c) - { - $this->a = $a; - $this->b = $b; - $this->c = $c; - } - - /** - * @param Specificity $specificity - * - * @return Specificity - */ - public function plus(Specificity $specificity) - { - return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c); - } - - /** - * Returns global specificity value. - * - * @return int - */ - public function getValue() - { - return $this->a * self::A_FACTOR + $this->b * self::B_FACTOR + $this->c * self::C_FACTOR; - } - - /** - * Returns -1 if the object specificity is lower than the argument, - * 0 if they are equal, and 1 if the argument is lower. - * - * @param Specificity $specificity - * - * @return int - */ - public function compareTo(Specificity $specificity) - { - if ($this->a !== $specificity->a) { - return $this->a > $specificity->a ? 1 : -1; - } - - if ($this->b !== $specificity->b) { - return $this->b > $specificity->b ? 1 : -1; - } - - if ($this->c !== $specificity->c) { - return $this->c > $specificity->c ? 1 : -1; - } - - return 0; - } - /** * Calculate the specificity based on a CSS Selector string, * Based on the patterns from premailer/css_parser by Alex Dunae From 21d03f61b244c19270a4a49549180bf4271c905c Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 15:43:53 +0100 Subject: [PATCH 31/39] Don't use getValue() anymore --- example/index.php | 29 +++++++++++++++++++++++ src/CssToInlineStyles.php | 10 ++++---- tests/Css/Specificity/SpecificityTest.php | 25 +++++-------------- 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/example/index.php b/example/index.php index 0b58668..19e4402 100644 --- a/example/index.php +++ b/example/index.php @@ -10,6 +10,35 @@ $html = file_get_contents(__DIR__ . '/examples/sumo/index.htm'); $css = file_get_contents(__DIR__ . '/examples/sumo/style.css'); +$html = << + a + +EOF; +$css = <<convert( $html, diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index ba92de9..9978edb 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -42,7 +42,7 @@ public function convert($html, $css = null) /** * Inle the given properties on an given DOMElement * - * @param \DOMElement $element + * @param \DOMElement $element * @param Css\Property\Property[] $properties * @return \DOMElement */ @@ -207,8 +207,7 @@ private function calculatePropertiesToBeApplied( if ( ($existingProperty->isImportant() && $property->isImportant()) && - ($property->getOriginalSpecificity()->getValue() >= - $existingProperty->getOriginalSpecificity()->getValue()) + ($existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0) ) { // if both the properties are important we should use the specificity $cssProperties[$property->getName()] = $property; @@ -217,10 +216,9 @@ private function calculatePropertiesToBeApplied( $cssProperties[$property->getName()] = $property; } elseif ( !$existingProperty->isImportant() && - ($property->getOriginalSpecificity()->getValue() >= - $existingProperty->getOriginalSpecificity()->getValue()) + ($existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0) ) { - // if the existing propert is not important we should check the specificity + // if the existing property is not important we should check the specificity $cssProperties[$property->getName()] = $property; } } else { diff --git a/tests/Css/Specificity/SpecificityTest.php b/tests/Css/Specificity/SpecificityTest.php index 96a8140..34ff438 100644 --- a/tests/Css/Specificity/SpecificityTest.php +++ b/tests/Css/Specificity/SpecificityTest.php @@ -6,19 +6,6 @@ class PropertyTest extends \PHPUnit_Framework_TestCase { - public function testIncreaseMethodShouldIncreaseAllWithOne() - { - $instance = new Specificity(0, 0, 0); - $instance = $instance->plus( - new Specificity(1, 1, 1) - ); - - $this->assertEquals( - 111, - $instance->getValue() - ); - } - public function testIdBeforeClass() { $idInstance = new Specificity(1, 0, 0); @@ -55,24 +42,24 @@ public function testCompareEqualItems() public function testSingleIdSelector() { $this->assertEquals( - 100, - Specificity::fromSelector('#foo')->getValue() + new Specificity(1, 0, 0), + Specificity::fromSelector('#foo') ); } public function testSingleClassSelector() { $this->assertEquals( - 10, - Specificity::fromSelector('.foo')->getValue() + new Specificity(0, 1, 0), + Specificity::fromSelector('.foo') ); } public function testSingleElementSelector() { $this->assertEquals( - 1, - Specificity::fromSelector('a')->getValue() + new Specificity(0, 0, 1), + Specificity::fromSelector('a') ); } } From 2480865631b757f3955f126878567dd2f785d2df Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 15:52:03 +0100 Subject: [PATCH 32/39] Don't extend the Symfony class anymore, as calculating the Specificity is the task of the Processor --- src/Css/Property/Processor.php | 2 +- src/Css/Property/Property.php | 2 +- src/Css/Rule/Processor.php | 48 ++++++++++++++++- src/Css/Rule/Rule.php | 2 +- src/Css/Specificity/Specificity.php | 50 ----------------- tests/Css/Rule/ProcessorTest.php | 25 +++++++++ tests/Css/Rule/RuleTest.php | 2 +- tests/Css/Specificity/SpecificityTest.php | 65 ----------------------- 8 files changed, 75 insertions(+), 121 deletions(-) delete mode 100644 src/Css/Specificity/Specificity.php delete mode 100644 tests/Css/Specificity/SpecificityTest.php diff --git a/src/Css/Property/Processor.php b/src/Css/Property/Processor.php index 876d913..2751fe6 100644 --- a/src/Css/Property/Processor.php +++ b/src/Css/Property/Processor.php @@ -2,7 +2,7 @@ namespace TijsVerkoyen\CssToInlineStyles\Css\Property; -use TijsVerkoyen\CssToInlineStyles\Css\Specificity\Specificity; +use Symfony\Component\CssSelector\Node\Specificity; class Processor { diff --git a/src/Css/Property/Property.php b/src/Css/Property/Property.php index fa69a1a..0452461 100644 --- a/src/Css/Property/Property.php +++ b/src/Css/Property/Property.php @@ -2,7 +2,7 @@ namespace TijsVerkoyen\CssToInlineStyles\Css\Property; -use TijsVerkoyen\CssToInlineStyles\Css\Specificity\Specificity; +use Symfony\Component\CssSelector\Node\Specificity; final class Property { diff --git a/src/Css/Rule/Processor.php b/src/Css/Rule/Processor.php index 87ef8d7..4beeac4 100644 --- a/src/Css/Rule/Processor.php +++ b/src/Css/Rule/Processor.php @@ -2,8 +2,8 @@ namespace TijsVerkoyen\CssToInlineStyles\Css\Rule; +use Symfony\Component\CssSelector\Node\Specificity; use \TijsVerkoyen\CssToInlineStyles\Css\Property\Processor as PropertyProcessor; -use TijsVerkoyen\CssToInlineStyles\Css\Specificity\Specificity; class Processor { @@ -60,7 +60,7 @@ public function convertToObjects($rule, $originalOrder) foreach ($selectors as $selector) { $selector = trim($selector); - $specificity = Specificity::fromSelector($selector); + $specificity = $this->calculateSpecificityBasedOnASelector($selector); $rules[] = new Rule( $selector, @@ -73,6 +73,50 @@ public function convertToObjects($rule, $originalOrder) return $rules; } + /** + * Calculate the specificity based on a CSS Selector string, + * Based on the patterns from premailer/css_parser by Alex Dunae + * + * @see https://github.com/premailer/css_parser/blob/master/lib/css_parser/regexps.rb + * @param string $selector + * @return Specificity + */ + public function calculateSpecificityBasedOnASelector($selector) + { + $idSelectorsPattern = " \#"; + $classAttributesPseudoClassesSelectorsPattern = " (\.[\w]+) # classes + | + \[(\w+) # attributes + | + (\:( # pseudo classes + link|visited|active + |hover|focus + |lang + |target + |enabled|disabled|checked|indeterminate + |root + |nth-child|nth-last-child|nth-of-type|nth-last-of-type + |first-child|last-child|first-of-type|last-of-type + |only-child|only-of-type + |empty|contains + ))"; + + $typePseudoElementsSelectorPattern = " ((^|[\s\+\>\~]+)[\w]+ # elements + | + \:{1,2}( # pseudo-elements + after|before + |first-letter|first-line + |selection + ) + )"; + + return new Specificity( + preg_match_all("/{$idSelectorsPattern}/ix", $selector, $matches), + preg_match_all("/{$classAttributesPseudoClassesSelectorsPattern}/ix", $selector, $matches), + preg_match_all("/{$typePseudoElementsSelectorPattern}/ix", $selector, $matches) + ); + } + /** * @param array $rules * @return Rule[] diff --git a/src/Css/Rule/Rule.php b/src/Css/Rule/Rule.php index 4130991..90947e5 100644 --- a/src/Css/Rule/Rule.php +++ b/src/Css/Rule/Rule.php @@ -2,7 +2,7 @@ namespace TijsVerkoyen\CssToInlineStyles\Css\Rule; -use TijsVerkoyen\CssToInlineStyles\Css\Specificity\Specificity; +use Symfony\Component\CssSelector\Node\Specificity; final class Rule { diff --git a/src/Css/Specificity/Specificity.php b/src/Css/Specificity/Specificity.php deleted file mode 100644 index 54af0af..0000000 --- a/src/Css/Specificity/Specificity.php +++ /dev/null @@ -1,50 +0,0 @@ -\~]+)[\w]+ # elements - | - \:{1,2}( # pseudo-elements - after|before - |first-letter|first-line - |selection - ) - )"; - - return new static( - preg_match_all("/{$idSelectorsPattern}/ix", $selector, $matches), - preg_match_all("/{$classAttributesPseudoClassesSelectorsPattern}/ix", $selector, $matches), - preg_match_all("/{$typePseudoElementsSelectorPattern}/ix", $selector, $matches) - ); - } -} diff --git a/tests/Css/Rule/ProcessorTest.php b/tests/Css/Rule/ProcessorTest.php index b687440..8e57fca 100644 --- a/tests/Css/Rule/ProcessorTest.php +++ b/tests/Css/Rule/ProcessorTest.php @@ -2,6 +2,7 @@ namespace TijsVerkoyen\CssToInlineStyles\Tests\Css\Rule; +use Symfony\Component\CssSelector\Node\Specificity; use TijsVerkoyen\CssToInlineStyles\Css\Rule\Processor; class ProcessorTest extends \PHPUnit_Framework_TestCase @@ -63,4 +64,28 @@ public function testMaintainOrderOfProperties() $this->assertEquals('211px', $rules[0]->getProperties()[1]->getValue()); $this->assertEquals(1, $rules[0]->getOrder()); } + + public function testSingleIdSelector() + { + $this->assertEquals( + new Specificity(1, 0, 0), + $this->processor->calculateSpecificityBasedOnASelector('#foo') + ); + } + + public function testSingleClassSelector() + { + $this->assertEquals( + new Specificity(0, 1, 0), + $this->processor->calculateSpecificityBasedOnASelector('.foo') + ); + } + + public function testSingleElementSelector() + { + $this->assertEquals( + new Specificity(0, 0, 1), + $this->processor->calculateSpecificityBasedOnASelector('a') + ); + } } diff --git a/tests/Css/Rule/RuleTest.php b/tests/Css/Rule/RuleTest.php index 555a035..a946659 100644 --- a/tests/Css/Rule/RuleTest.php +++ b/tests/Css/Rule/RuleTest.php @@ -4,7 +4,7 @@ use TijsVerkoyen\CssToInlineStyles\Css\Property\Property; use TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule; -use TijsVerkoyen\CssToInlineStyles\Css\Specificity\Specificity; +use Symfony\Component\CssSelector\Node\Specificity; class PropertyTest extends \PHPUnit_Framework_TestCase { diff --git a/tests/Css/Specificity/SpecificityTest.php b/tests/Css/Specificity/SpecificityTest.php deleted file mode 100644 index 34ff438..0000000 --- a/tests/Css/Specificity/SpecificityTest.php +++ /dev/null @@ -1,65 +0,0 @@ -assertEquals( - 1, - $idInstance->compareTo($classInstance) - ); - } - - public function testClassBeforeElement() - { - $idInstance = new Specificity(0, 1, 0); - $classInstance = new Specificity(0, 0, 1); - - $this->assertEquals( - 1, - $idInstance->compareTo($classInstance) - ); - } - - public function testCompareEqualItems() - { - $instance1 = new Specificity(1, 0, 0); - $instance2 = new Specificity(1, 0, 0); - - $this->assertEquals( - 0, - $instance1->compareTo($instance2) - ); - } - - public function testSingleIdSelector() - { - $this->assertEquals( - new Specificity(1, 0, 0), - Specificity::fromSelector('#foo') - ); - } - - public function testSingleClassSelector() - { - $this->assertEquals( - new Specificity(0, 1, 0), - Specificity::fromSelector('.foo') - ); - } - - public function testSingleElementSelector() - { - $this->assertEquals( - new Specificity(0, 0, 1), - Specificity::fromSelector('a') - ); - } -} From e22d56934d04abd18d4a0ca2e667c2346d8749e2 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 15:59:10 +0100 Subject: [PATCH 33/39] Fixed some PHPDoc --- src/Css/Rule/Rule.php | 2 +- src/CssToInlineStyles.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Css/Rule/Rule.php b/src/Css/Rule/Rule.php index 90947e5..8f9d2bf 100644 --- a/src/Css/Rule/Rule.php +++ b/src/Css/Rule/Rule.php @@ -30,7 +30,7 @@ final class Rule * Rule constructor. * * @param string $selector - * @param array $properties + * @param Property[] $properties * @param Specificity $specificity * @param int $order */ diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index 9978edb..a3d2587 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -126,8 +126,8 @@ protected function getHtmlFromDocument(\DOMDocument $document) } /** - * @param \DOMDocument $document - * @param array $rules + * @param \DOMDocument $document + * @param Css\Rule\Rule[] $rules * @return \DOMDocument */ protected function inline(\DOMDocument $document, array $rules) @@ -174,8 +174,8 @@ protected function inline(\DOMDocument $document, array $rules) /** * Store the calculated values in a temporary data-attribute * - * @param \DOMElement $element - * @param Property[] $properties + * @param \DOMElement $element + * @param Css\Property\Property[] $properties * @return \DOMElement */ private function calculatePropertiesToBeApplied( From de745fff7721010be10df3f6b8d3996f8faf6257 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 16:07:40 +0100 Subject: [PATCH 34/39] Added a branch-alias --- composer.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/composer.json b/composer.json index c3de758..3292a8c 100644 --- a/composer.json +++ b/composer.json @@ -21,5 +21,10 @@ "psr-4": { "TijsVerkoyen\\CssToInlineStyles\\": "src" } + }, + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } } } From 2851b4af35decfce5c4d218e36b351862ea386c7 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 16:14:50 +0100 Subject: [PATCH 35/39] Allow 5.1 also --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3292a8c..71fe80b 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "symfony/css-selector": "^2.7" }, "require-dev": { - "phpunit/phpunit": "~4.8" + "phpunit/phpunit": "~4.8|5.1.*" }, "autoload": { "psr-4": { From b016b8d99ab83d5bf90ea504abcb653a5b479f21 Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 16:26:31 +0100 Subject: [PATCH 36/39] Allow Symfony/CssSelector v3.0 --- composer.json | 2 +- src/CssToInlineStyles.php | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 71fe80b..6d738b2 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "symfony/css-selector": "^2.7" + "symfony/css-selector": "^2.7|~3.0" }, "require-dev": { "phpunit/phpunit": "~4.8|5.1.*" diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index a3d2587..1199610 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -3,6 +3,7 @@ namespace TijsVerkoyen\CssToInlineStyles; use Symfony\Component\CssSelector\CssSelector; +use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\CssSelector\Exception\SyntaxErrorException; use TijsVerkoyen\CssToInlineStyles\Css\Processor; use TijsVerkoyen\CssToInlineStyles\Css\Property\Processor as PropertyProcessor; @@ -138,9 +139,13 @@ protected function inline(\DOMDocument $document, array $rules) $xPath = new \DOMXPath($document); foreach ($rules as $rule) { - /** @var Rule $rule */ try { - $expression = CssSelector::toXPath($rule->getSelector()); + if (class_exists('Symfony\Component\CssSelector\CssSelectorConverter')) { + $converter = new CssSelectorConverter(); + $expression = $converter->toXPath($rule->getSelector()); + } else { + $expression = CssSelector::toXPath($rule->getSelector()); + } } catch (SyntaxErrorException $e) { continue; } From f622480bf7234fd1dd49a910a44e79ee4a80879b Mon Sep 17 00:00:00 2001 From: Tijs Verkoyen Date: Tue, 8 Dec 2015 16:29:36 +0100 Subject: [PATCH 37/39] Cleanup --- example/index.php | 29 ----------------------------- src/Css/Processor.php | 1 + src/Css/Rule/Rule.php | 2 +- src/Exception.php | 8 -------- 4 files changed, 2 insertions(+), 38 deletions(-) delete mode 100644 src/Exception.php diff --git a/example/index.php b/example/index.php index 19e4402..0b58668 100644 --- a/example/index.php +++ b/example/index.php @@ -10,35 +10,6 @@ $html = file_get_contents(__DIR__ . '/examples/sumo/index.htm'); $css = file_get_contents(__DIR__ . '/examples/sumo/style.css'); -$html = << - a - -EOF; -$css = <<convert( $html, diff --git a/src/Css/Processor.php b/src/Css/Processor.php index f00b708..c3af3d2 100644 --- a/src/Css/Processor.php +++ b/src/Css/Processor.php @@ -18,6 +18,7 @@ public function getRules($css, $existingRules = array()) $css = $this->doCleanup($css); $rulesProcessor = new RuleProcessor(); $rules = $rulesProcessor->splitIntoSeparateRules($css); + return $rulesProcessor->convertArrayToObjects($rules, $existingRules); } diff --git a/src/Css/Rule/Rule.php b/src/Css/Rule/Rule.php index 8f9d2bf..1f2b59f 100644 --- a/src/Css/Rule/Rule.php +++ b/src/Css/Rule/Rule.php @@ -30,7 +30,7 @@ final class Rule * Rule constructor. * * @param string $selector - * @param Property[] $properties + * @param Property[] $properties * @param Specificity $specificity * @param int $order */ diff --git a/src/Exception.php b/src/Exception.php deleted file mode 100644 index 52f2dc4..0000000 --- a/src/Exception.php +++ /dev/null @@ -1,8 +0,0 @@ - Date: Tue, 8 Dec 2015 16:49:57 +0100 Subject: [PATCH 38/39] Use ParseException instead of SyntaxErrorException --- src/CssToInlineStyles.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index 1199610..6b69a22 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -4,7 +4,7 @@ use Symfony\Component\CssSelector\CssSelector; use Symfony\Component\CssSelector\CssSelectorConverter; -use Symfony\Component\CssSelector\Exception\SyntaxErrorException; +use Symfony\Component\CssSelector\Exception\ParseException; use TijsVerkoyen\CssToInlineStyles\Css\Processor; use TijsVerkoyen\CssToInlineStyles\Css\Property\Processor as PropertyProcessor; use TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule; @@ -146,7 +146,7 @@ protected function inline(\DOMDocument $document, array $rules) } else { $expression = CssSelector::toXPath($rule->getSelector()); } - } catch (SyntaxErrorException $e) { + } catch (ParseException $e) { continue; } From c82c91952f637b559f59279405c99f0f351082a7 Mon Sep 17 00:00:00 2001 From: "Barry vd. Heuvel" Date: Tue, 8 Dec 2015 16:58:35 +0100 Subject: [PATCH 39/39] Use ExceptionInterface instead of ParseException --- src/CssToInlineStyles.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CssToInlineStyles.php b/src/CssToInlineStyles.php index 6b69a22..e119cbb 100644 --- a/src/CssToInlineStyles.php +++ b/src/CssToInlineStyles.php @@ -4,7 +4,7 @@ use Symfony\Component\CssSelector\CssSelector; use Symfony\Component\CssSelector\CssSelectorConverter; -use Symfony\Component\CssSelector\Exception\ParseException; +use Symfony\Component\CssSelector\Exception\ExceptionInterface; use TijsVerkoyen\CssToInlineStyles\Css\Processor; use TijsVerkoyen\CssToInlineStyles\Css\Property\Processor as PropertyProcessor; use TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule; @@ -146,7 +146,7 @@ protected function inline(\DOMDocument $document, array $rules) } else { $expression = CssSelector::toXPath($rule->getSelector()); } - } catch (ParseException $e) { + } catch (ExceptionInterface $e) { continue; }