| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764 |
- <?php
- //namespace TijsVerkoyen\CssToInlineStyles;
- /**
- * CSS to Inline Styles class
- *
- * @author Tijs Verkoyen <php-css-to-inline-styles@verkoyen.eu>
- * @version 1.2.1
- * @copyright Copyright (c), Tijs Verkoyen. All rights reserved.
- * @license 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 = false;
- /**
- * Only necessary and applicable for PHP5.4 and above.
- *
- * @var bool
- */
- private $correctUtf8 = false;
- /**
- * 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);
- $this->correctUtf8 = version_compare(PHP_VERSION, '5.4.0') >= 0;
- }
- /**
- * Convert a CSS-selector into an xPath-query
- *
- * @return string
- * @param string $selector The CSS-selector.
- */
- private function buildXPathQuery($selector)
- {
- // redefine
- $selector = (string) $selector;
- // the CSS selector
- $cssSelector = [
- // E F, Matches any F element that is a descendant of an E element
- '/(\w)\s+([\w\*])/',
- // E > F, Matches any F element that is a child of an element E
- '/(\w)\s*>\s*([\w\*])/',
- // E:first-child, Matches element E when E is the first child of its parent
- '/(\w):first-child/',
- // E + F, Matches any F element immediately preceded by an element
- '/(\w)\s*\+\s*(\w)/',
- // E[foo], Matches any E element with the "foo" attribute set (whatever the value)
- '/(\w)\[([\w\-_]+)]/',
- // E[foo="warning"], Matches any E element whose "foo" attribute value is exactly equal to "warning"
- '/(\w)\[([\w\-_]+)\=\"(.*)\"]/',
- // div.warning, HTML only. The same as DIV[class~="warning"]
- '/(\w+|\*)+\.([\w\-_]+)+/',
- // .warning, HTML only. The same as *[class~="warning"]
- '/\.([\w\-_]+)/',
- // E#myid, Matches any E element with id-attribute equal to "myid"
- '/(\w+)+\#([\w\-_]+)/',
- // #myid, Matches any element with id-attribute equal to "myid"
- '/\#([\w\-_]+)/'
- ];
- // the xPath-equivalent
- $xPathQuery = [
- // E F, Matches any F element that is a descendant of an E element
- '\1//\2',
- // E > F, Matches any F element that is a child of an element E
- '\1/\2',
- // E:first-child, Matches element E when E is the first child of its parent
- '*[1]/self::\1',
- // E + F, Matches any F element immediately preceded by an element
- '\1/following-sibling::*[1]/self::\2',
- // E[foo], Matches any E element with the "foo" attribute set (whatever the value)
- '\1 [ @\2 ]',
- // E[foo="warning"], Matches any E element whose "foo" attribute value is exactly equal to "warning"
- '\1[ contains( concat( " ", @\2, " " ), concat( " ", "\3", " " ) ) ]',
- // div.warning, HTML only. The same as DIV[class~="warning"]
- '\1[ contains( concat( " ", @class, " " ), concat( " ", "\2", " " ) ) ]',
- // .warning, HTML only. The same as *[class~="warning"]
- '*[ contains( concat( " ", @class, " " ), concat( " ", "\1", " " ) ) ]',
- // E#myid, Matches any E element with id-attribute equal to "myid"
- '\1[ @id = "\2" ]',
- // #myid, Matches any element with id-attribute equal to "myid"
- '*[ @id = "\1" ]'
- ];
- // return
- $xPath = (string) '//' . preg_replace($cssSelector, $xPathQuery, $selector);
- return str_replace('] *', ']//*', $xPath);
- }
- /**
- * Calculate the specifity for the CSS-selector
- *
- * @return integer
- * @param string $selector The selector to calculate the specifity for.
- */
- private function calculateCSSSpecifity($selector)
- {
- // cleanup selector
- $selector = preg_replace('/\s?(\>|\+)\s?/', '$1', $selector);
- // init var
- $specifity = 0;
- // split the selector into chunks based on spaces
- $chunks = preg_split( '/\>|\+|\s/', $selector);
- // loop chunks
- foreach ($chunks as $chunk) {
- // an ID is important, so give it a high specifity
- if(strstr($chunk, '#') !== false) $specifity += 100;
- // classes are more important than a tag, but less important then an ID
- elseif(strstr($chunk, '.')) $specifity += 10;
- // anything else isn't that important
- else $specifity += 1;
- }
- // return
- return $specifity;
- }
- /**
- * Cleanup the generated HTML
- *
- * @return string
- * @param string $html The HTML to cleanup.
- */
- private function cleanupHTML($html)
- {
- // remove classes
- $html = preg_replace('/(\s)+class="(.*)"(\s)*/U', ' ', $html);
- // remove IDs
- $html = preg_replace('/(\s)+id="(.*)"(\s)*/U', ' ', $html);
- // return
- return $html;
- }
- /**
- * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS
- *
- * @return string
- * @param bool[optional] $outputXHTML Should we output valid XHTML?
- */
- public function convert($outputXHTML = false)
- {
- // redefine
- $outputXHTML = (bool) $outputXHTML;
- // validate
- if($this->html == null) throw new Exception('No HTML provided.');
- // should we use inline style-block
- if ($this->useInlineStylesBlock) {
- // init var
- $matches = [];
- // match the style blocks
- preg_match_all('|<style(.*)>(.*)</style>|isU', $this->html, $matches);
- // any style-blocks found?
- if (!empty($matches[2])) {
- // add
- foreach($matches[2] as $match) $this->css .= "\n" . trim($match) ."\n";
- }
- }
- // process css
- $this->processCSS();
- // create new DOMDocument
- $document = new \DOMDocument('1.0', $this->getEncoding());
- // set error level
- libxml_use_internal_errors(true);
- //Check if html has ISO encoding and convert to UTF-8
- $encoding = mb_detect_encoding($this->html, ['UTF-8', 'ISO-8859-1']);
- if ($encoding === 'ISO-8859-1') {
- $this->html = utf8_encode($this->html);
- }
- // load HTML
- $document->loadHTML($this->html);
- // create new XPath
- $xPath = new \DOMXPath($document);
- // any rules?
- if (!empty($this->cssRules)) {
- // loop rules
- foreach ($this->cssRules as $rule) {
- // init var
- $query = $this->buildXPathQuery($rule['selector']);
- // validate query
- if($query === false) 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 = [];
- // 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 = (array) explode(';', $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) {
- $properties[$key] = $value;
- }
- // build string
- $propertyChunks = [];
- // 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
- $query = $this->buildXPathQuery(
- '*[@data-css-to-inline-styles-original-styles]'
- );
- // validate query
- if($query === false) return;
- // search elements
- $elements = $xPath->query($query);
- // 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 = [];
- $originalStyles = (array) explode(';', $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 = [];
- // any styles defined before?
- if ($stylesAttribute !== null) {
- // get value for the styles attribute
- $definedStyles = (string) $stylesAttribute->value;
- // split into properties
- $definedProperties = (array) explode(';', $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 = [];
- // 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
- );
- }
- // remove placeholder
- $element->removeAttribute(
- 'data-css-to-inline-styles-original-styles'
- );
- }
- }
- // should we output XHTML?
- if ($outputXHTML) {
- $document->formatOutput = true;
- // get the HTML as XML
- $html = $document->saveXML(null, LIBXML_NOEMPTYTAG);
- // get start of the XML-declaration
- $startPosition = strpos($html, '<?xml');
- // valid start position?
- if ($startPosition !== false) {
- // get end of the xml-declaration
- $endPosition = strpos($html, '?>', $startPosition);
- // remove the XML-header
- $html = ltrim(substr($html, $endPosition + 2));
- }
- }
- // just regular HTML 4.01 as it should be used in newsletters
- else {
- // get the HTML
- $document->formatOutput = true;
- $html = $document->saveHTML();
- }
- if ($this->correctUtf8) {
- // Only for >PHP5.4
- $chars = [
- ' ', '«', '»', '<', '>',
- '©', '®', '™', ''', '&', '"',
- ];
- // Make sure chars dont annihilate the result
- foreach ($chars as $char) {
- $html = str_replace($char, '[[' . substr($char, 1, -1) . ']]', $html);
- }
- // Correct scrambled UTF8 chars (ãƒ...) back to their correct representation.
- $html = html_entity_decode($html, ENT_XHTML);
- $html = utf8_decode($html);
- foreach ($chars as $char) {
- $html = str_replace('[[' . substr($char, 1, -1) . ']]', $char, $html);
- }
- }
- // cleanup the HTML if we need to
- if($this->cleanup) $html = $this->cleanupHTML($html);
- // strip original style tags if we need to
- if ($this->stripOriginalStyleTags) {
- $html = $this->stripOriginalStyleTags($html);
- }
- return $html;
- }
- /**
- * Get the encoding to use
- *
- * @return string
- */
- private function getEncoding()
- {
- return $this->encoding;
- }
- /**
- * Process the loaded CSS
- *
- * @return void
- */
- private function processCSS()
- {
- // init vars
- $css = (string) $this->css;
- // remove newlines
- $css = str_replace(["\r", "\n"], '', $css);
- // replace double quotes by single quotes
- $css = str_replace('"', '\'', $css);
- // remove comments
- $css = preg_replace('|/\*.*?\*/|', '', $css);
- // remove double 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);
- // build an array for each selector
- $ruleSet = [];
- // store selector
- $ruleSet['selector'] = $selector;
- // process the properties
- $ruleSet['properties'] = $this->processCSSProperties(
- $cssProperties
- );
- // calculate specifity
- $ruleSet['specifity'] = $this->calculateCSSSpecifity(
- $selector
- ) + $i;
- // add into global rules
- $this->cssRules[] = $ruleSet;
- }
- // increment
- $i++;
- }
- // sort based on specifity
- if (!empty($this->cssRules)) {
- usort($this->cssRules, [__CLASS__, 'sortOnSpecifity']);
- }
- }
- /**
- * Process the CSS-properties
- *
- * @return array
- * @param string $propertyString The CSS-properties.
- */
- private function processCSSProperties($propertyString)
- {
- // split into chunks
- $properties = (array) explode(';', $propertyString);
- // init var
- $pairs = [];
- // loop properties
- foreach ($properties as $property) {
- // split into chunks
- $chunks = (array) explode(':', $property, 2);
- // validate
- if(!isset($chunks[1])) 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.
- */
- 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 utf8 correction
- *
- * @return void
- * @param bool $on.
- */
- public function setCorrectUtf8($on = true)
- {
- $this->correctUtf8 = (bool) $on;
- }
- /**
- * 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 string $html The HTML to strip style tags.
- */
- private function stripOriginalStyleTags($html)
- {
- return preg_replace('|<style(.*)>(.*)</style>|isU', '', $html);
- }
- /**
- * Sort an array on the specifity element
- *
- * @return integer
- * @param array $e1 The first element.
- * @param array $e2 The second element.
- */
- private static function sortOnSpecifity($e1, $e2)
- {
- // validate
- if(!isset($e1['specifity']) || !isset($e2['specifity'])) return 0;
- // lower
- if($e1['specifity'] < $e2['specifity']) return -1;
- // higher
- if($e1['specifity'] > $e2['specifity']) return 1;
- // fallback
- return 0;
- }
- }
|