PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/tijsverkoyen/css-to-inline-styles/src/CssToInlineStyles.php

https://gitlab.com/techniconline/kmc
PHP | 686 lines | 307 code | 114 blank | 265 comment | 46 complexity | 15193c1d74be121184e4f83eab8aef17 MD5 | raw file
  1. <?php
  2. namespace TijsVerkoyen\CssToInlineStyles;
  3. use Symfony\Component\CssSelector\CssSelector;
  4. use Symfony\Component\CssSelector\Exception\ExceptionInterface;
  5. /**
  6. * CSS to Inline Styles class
  7. *
  8. * @author Tijs Verkoyen <php-css-to-inline-styles@verkoyen.eu>
  9. * @version 1.5.4
  10. * @copyright Copyright (c), Tijs Verkoyen. All rights reserved.
  11. * @license BSD License
  12. */
  13. class CssToInlineStyles
  14. {
  15. /**
  16. * The CSS to use
  17. *
  18. * @var string
  19. */
  20. private $css;
  21. /**
  22. * The processed CSS rules
  23. *
  24. * @var array
  25. */
  26. private $cssRules;
  27. /**
  28. * Should the generated HTML be cleaned
  29. *
  30. * @var bool
  31. */
  32. private $cleanup = false;
  33. /**
  34. * The encoding to use.
  35. *
  36. * @var string
  37. */
  38. private $encoding = 'UTF-8';
  39. /**
  40. * The HTML to process
  41. *
  42. * @var string
  43. */
  44. private $html;
  45. /**
  46. * Use inline-styles block as CSS
  47. *
  48. * @var bool
  49. */
  50. private $useInlineStylesBlock = false;
  51. /**
  52. * Strip original style tags
  53. *
  54. * @var bool
  55. */
  56. private $stripOriginalStyleTags = false;
  57. /**
  58. * Exclude the media queries from the inlined styles
  59. *
  60. * @var bool
  61. */
  62. private $excludeMediaQueries = true;
  63. /**
  64. * Creates an instance, you could set the HTML and CSS here, or load it
  65. * later.
  66. *
  67. * @return void
  68. * @param string [optional] $html The HTML to process.
  69. * @param string [optional] $css The CSS to use.
  70. */
  71. public function __construct($html = null, $css = null)
  72. {
  73. if ($html !== null) {
  74. $this->setHTML($html);
  75. }
  76. if ($css !== null) {
  77. $this->setCSS($css);
  78. }
  79. }
  80. /**
  81. * Cleanup the generated HTML
  82. *
  83. * @return string
  84. * @param string $html The HTML to cleanup.
  85. */
  86. private function cleanupHTML($html)
  87. {
  88. // remove classes
  89. $html = preg_replace('/(\s)+class="(.*)"(\s)*/U', ' ', $html);
  90. // remove IDs
  91. $html = preg_replace('/(\s)+id="(.*)"(\s)*/U', ' ', $html);
  92. // return
  93. return $html;
  94. }
  95. /**
  96. * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS
  97. *
  98. * @return string
  99. * @param bool [optional] $outputXHTML Should we output valid XHTML?
  100. */
  101. public function convert($outputXHTML = false)
  102. {
  103. // redefine
  104. $outputXHTML = (bool)$outputXHTML;
  105. // validate
  106. if ($this->html == null) {
  107. throw new Exception('No HTML provided.');
  108. }
  109. // should we use inline style-block
  110. if ($this->useInlineStylesBlock) {
  111. // init var
  112. $matches = array();
  113. // match the style blocks
  114. preg_match_all('|<style(.*)>(.*)</style>|isU', $this->html, $matches);
  115. // any style-blocks found?
  116. if (!empty($matches[2])) {
  117. // add
  118. foreach ($matches[2] as $match) {
  119. $this->css .= trim($match) . "\n";
  120. }
  121. }
  122. }
  123. // process css
  124. $this->processCSS();
  125. // create new DOMDocument
  126. $document = new \DOMDocument('1.0', $this->getEncoding());
  127. // set error level
  128. $internalErrors = libxml_use_internal_errors(true);
  129. // load HTML
  130. $document->loadHTML($this->html);
  131. // Restore error level
  132. libxml_use_internal_errors($internalErrors);
  133. // create new XPath
  134. $xPath = new \DOMXPath($document);
  135. // any rules?
  136. if (!empty($this->cssRules)) {
  137. // loop rules
  138. foreach ($this->cssRules as $rule) {
  139. try {
  140. $query = CssSelector::toXPath($rule['selector']);
  141. } catch (ExceptionInterface $e) {
  142. continue;
  143. }
  144. // search elements
  145. $elements = $xPath->query($query);
  146. // validate elements
  147. if ($elements === false) {
  148. continue;
  149. }
  150. // loop found elements
  151. foreach ($elements as $element) {
  152. // no styles stored?
  153. if ($element->attributes->getNamedItem(
  154. 'data-css-to-inline-styles-original-styles'
  155. ) == null
  156. ) {
  157. // init var
  158. $originalStyle = '';
  159. if ($element->attributes->getNamedItem('style') !== null) {
  160. $originalStyle = $element->attributes->getNamedItem('style')->value;
  161. }
  162. // store original styles
  163. $element->setAttribute(
  164. 'data-css-to-inline-styles-original-styles',
  165. $originalStyle
  166. );
  167. // clear the styles
  168. $element->setAttribute('style', '');
  169. }
  170. // init var
  171. $properties = array();
  172. // get current styles
  173. $stylesAttribute = $element->attributes->getNamedItem('style');
  174. // any styles defined before?
  175. if ($stylesAttribute !== null) {
  176. // get value for the styles attribute
  177. $definedStyles = (string)$stylesAttribute->value;
  178. // split into properties
  179. $definedProperties = $this->splitIntoProperties($definedStyles);
  180. // loop properties
  181. foreach ($definedProperties as $property) {
  182. // validate property
  183. if ($property == '') {
  184. continue;
  185. }
  186. // split into chunks
  187. $chunks = (array)explode(':', trim($property), 2);
  188. // validate
  189. if (!isset($chunks[1])) {
  190. continue;
  191. }
  192. // loop chunks
  193. $properties[$chunks[0]] = trim($chunks[1]);
  194. }
  195. }
  196. // add new properties into the list
  197. foreach ($rule['properties'] as $key => $value) {
  198. // If one of the rules is already set and is !important, don't apply it,
  199. // except if the new rule is also important.
  200. if (
  201. !isset($properties[$key])
  202. || stristr($properties[$key], '!important') === false
  203. || (stristr(implode('', $value), '!important') !== false)
  204. ) {
  205. $properties[$key] = $value;
  206. }
  207. }
  208. // build string
  209. $propertyChunks = array();
  210. // build chunks
  211. foreach ($properties as $key => $values) {
  212. foreach ((array)$values as $value) {
  213. $propertyChunks[] = $key . ': ' . $value . ';';
  214. }
  215. }
  216. // build properties string
  217. $propertiesString = implode(' ', $propertyChunks);
  218. // set attribute
  219. if ($propertiesString != '') {
  220. $element->setAttribute('style', $propertiesString);
  221. }
  222. }
  223. }
  224. // reapply original styles
  225. // search elements
  226. $elements = $xPath->query('//*[@data-css-to-inline-styles-original-styles]');
  227. // loop found elements
  228. foreach ($elements as $element) {
  229. // get the original styles
  230. $originalStyle = $element->attributes->getNamedItem(
  231. 'data-css-to-inline-styles-original-styles'
  232. )->value;
  233. if ($originalStyle != '') {
  234. $originalProperties = array();
  235. $originalStyles = $this->splitIntoProperties($originalStyle);
  236. foreach ($originalStyles as $property) {
  237. // validate property
  238. if ($property == '') {
  239. continue;
  240. }
  241. // split into chunks
  242. $chunks = (array)explode(':', trim($property), 2);
  243. // validate
  244. if (!isset($chunks[1])) {
  245. continue;
  246. }
  247. // loop chunks
  248. $originalProperties[$chunks[0]] = trim($chunks[1]);
  249. }
  250. // get current styles
  251. $stylesAttribute = $element->attributes->getNamedItem('style');
  252. $properties = array();
  253. // any styles defined before?
  254. if ($stylesAttribute !== null) {
  255. // get value for the styles attribute
  256. $definedStyles = (string)$stylesAttribute->value;
  257. // split into properties
  258. $definedProperties = $this->splitIntoProperties($definedStyles);
  259. // loop properties
  260. foreach ($definedProperties as $property) {
  261. // validate property
  262. if ($property == '') {
  263. continue;
  264. }
  265. // split into chunks
  266. $chunks = (array)explode(':', trim($property), 2);
  267. // validate
  268. if (!isset($chunks[1])) {
  269. continue;
  270. }
  271. // loop chunks
  272. $properties[$chunks[0]] = trim($chunks[1]);
  273. }
  274. }
  275. // add new properties into the list
  276. foreach ($originalProperties as $key => $value) {
  277. $properties[$key] = $value;
  278. }
  279. // build string
  280. $propertyChunks = array();
  281. // build chunks
  282. foreach ($properties as $key => $values) {
  283. foreach ((array)$values as $value) {
  284. $propertyChunks[] = $key . ': ' . $value . ';';
  285. }
  286. }
  287. // build properties string
  288. $propertiesString = implode(' ', $propertyChunks);
  289. // set attribute
  290. if ($propertiesString != '') {
  291. $element->setAttribute(
  292. 'style',
  293. $propertiesString
  294. );
  295. }
  296. }
  297. // remove placeholder
  298. $element->removeAttribute(
  299. 'data-css-to-inline-styles-original-styles'
  300. );
  301. }
  302. }
  303. // strip original style tags if we need to
  304. if ($this->stripOriginalStyleTags) {
  305. $this->stripOriginalStyleTags($xPath);
  306. }
  307. // should we output XHTML?
  308. if ($outputXHTML) {
  309. // set formating
  310. $document->formatOutput = true;
  311. // get the HTML as XML
  312. $html = $document->saveXML(null, LIBXML_NOEMPTYTAG);
  313. // remove the XML-header
  314. $html = ltrim(preg_replace('/<?xml (.*)?>/', '', $html));
  315. } // just regular HTML 4.01 as it should be used in newsletters
  316. else {
  317. // get the HTML
  318. $html = $document->saveHTML();
  319. }
  320. // cleanup the HTML if we need to
  321. if ($this->cleanup) {
  322. $html = $this->cleanupHTML($html);
  323. }
  324. // return
  325. return $html;
  326. }
  327. /**
  328. * Split a style string into an array of properties.
  329. * The returned array can contain empty strings.
  330. *
  331. * @param string $styles ex: 'color:blue;font-size:12px;'
  332. * @return array an array of strings containing css property ex: array('color:blue','font-size:12px')
  333. */
  334. private function splitIntoProperties($styles)
  335. {
  336. $properties = (array)explode(';', $styles);
  337. for ($i = 0; $i < count($properties); $i++) {
  338. // If next property begins with base64,
  339. // Then the ';' was part of this property (and we should not have split on it).
  340. if (isset($properties[$i + 1]) && strpos($properties[$i + 1], 'base64,') === 0) {
  341. $properties[$i] .= ';' . $properties[$i + 1];
  342. $properties[$i + 1] = '';
  343. $i += 1;
  344. }
  345. }
  346. return $properties;
  347. }
  348. /**
  349. * Get the encoding to use
  350. *
  351. * @return string
  352. */
  353. private function getEncoding()
  354. {
  355. return $this->encoding;
  356. }
  357. /**
  358. * Process the loaded CSS
  359. *
  360. * @return void
  361. */
  362. private function processCSS()
  363. {
  364. // init vars
  365. $css = (string)$this->css;
  366. // remove newlines
  367. $css = str_replace(array("\r", "\n"), '', $css);
  368. // replace double quotes by single quotes
  369. $css = str_replace('"', '\'', $css);
  370. // remove comments
  371. $css = preg_replace('|/\*.*?\*/|', '', $css);
  372. // remove spaces
  373. $css = preg_replace('/\s\s+/', ' ', $css);
  374. if ($this->excludeMediaQueries) {
  375. $css = preg_replace('/@media [^{]*{([^{}]|{[^{}]*})*}/', '', $css);
  376. }
  377. // rules are splitted by }
  378. $rules = (array)explode('}', $css);
  379. // init var
  380. $i = 1;
  381. // loop rules
  382. foreach ($rules as $rule) {
  383. // split into chunks
  384. $chunks = explode('{', $rule);
  385. // invalid rule?
  386. if (!isset($chunks[1])) {
  387. continue;
  388. }
  389. // set the selectors
  390. $selectors = trim($chunks[0]);
  391. // get cssProperties
  392. $cssProperties = trim($chunks[1]);
  393. // split multiple selectors
  394. $selectors = (array)explode(',', $selectors);
  395. // loop selectors
  396. foreach ($selectors as $selector) {
  397. // cleanup
  398. $selector = trim($selector);
  399. // build an array for each selector
  400. $ruleSet = array();
  401. // store selector
  402. $ruleSet['selector'] = $selector;
  403. // process the properties
  404. $ruleSet['properties'] = $this->processCSSProperties(
  405. $cssProperties
  406. );
  407. // calculate specificity
  408. $ruleSet['specificity'] = Specificity::fromSelector($selector);
  409. // remember the order in which the rules appear
  410. $ruleSet['order'] = $i;
  411. // add into global rules
  412. $this->cssRules[] = $ruleSet;
  413. }
  414. // increment
  415. $i++;
  416. }
  417. // sort based on specificity
  418. if (!empty($this->cssRules)) {
  419. usort($this->cssRules, array(__CLASS__, 'sortOnSpecificity'));
  420. }
  421. }
  422. /**
  423. * Process the CSS-properties
  424. *
  425. * @return array
  426. * @param string $propertyString The CSS-properties.
  427. */
  428. private function processCSSProperties($propertyString)
  429. {
  430. // split into chunks
  431. $properties = $this->splitIntoProperties($propertyString);
  432. // init var
  433. $pairs = array();
  434. // loop properties
  435. foreach ($properties as $property) {
  436. // split into chunks
  437. $chunks = (array)explode(':', $property, 2);
  438. // validate
  439. if (!isset($chunks[1])) {
  440. continue;
  441. }
  442. // cleanup
  443. $chunks[0] = trim($chunks[0]);
  444. $chunks[1] = trim($chunks[1]);
  445. // add to pairs array
  446. if (!isset($pairs[$chunks[0]]) ||
  447. !in_array($chunks[1], $pairs[$chunks[0]])
  448. ) {
  449. $pairs[$chunks[0]][] = $chunks[1];
  450. }
  451. }
  452. // sort the pairs
  453. ksort($pairs);
  454. // return
  455. return $pairs;
  456. }
  457. /**
  458. * Should the IDs and classes be removed?
  459. *
  460. * @return void
  461. * @param bool [optional] $on Should we enable cleanup?
  462. */
  463. public function setCleanup($on = true)
  464. {
  465. $this->cleanup = (bool)$on;
  466. }
  467. /**
  468. * Set CSS to use
  469. *
  470. * @return void
  471. * @param string $css The CSS to use.
  472. */
  473. public function setCSS($css)
  474. {
  475. $this->css = (string)$css;
  476. }
  477. /**
  478. * Set the encoding to use with the DOMDocument
  479. *
  480. * @return void
  481. * @param string $encoding The encoding to use.
  482. *
  483. * @deprecated Doesn't have any effect
  484. */
  485. public function setEncoding($encoding)
  486. {
  487. $this->encoding = (string)$encoding;
  488. }
  489. /**
  490. * Set HTML to process
  491. *
  492. * @return void
  493. * @param string $html The HTML to process.
  494. */
  495. public function setHTML($html)
  496. {
  497. $this->html = (string)$html;
  498. }
  499. /**
  500. * Set use of inline styles block
  501. * If this is enabled the class will use the style-block in the HTML.
  502. *
  503. * @return void
  504. * @param bool [optional] $on Should we process inline styles?
  505. */
  506. public function setUseInlineStylesBlock($on = true)
  507. {
  508. $this->useInlineStylesBlock = (bool)$on;
  509. }
  510. /**
  511. * Set strip original style tags
  512. * If this is enabled the class will remove all style tags in the HTML.
  513. *
  514. * @return void
  515. * @param bool [optional] $on Should we process inline styles?
  516. */
  517. public function setStripOriginalStyleTags($on = true)
  518. {
  519. $this->stripOriginalStyleTags = (bool)$on;
  520. }
  521. /**
  522. * Set exclude media queries
  523. *
  524. * If this is enabled the media queries will be removed before inlining the rules
  525. *
  526. * @return void
  527. * @param bool [optional] $on
  528. */
  529. public function setExcludeMediaQueries($on = true)
  530. {
  531. $this->excludeMediaQueries = (bool)$on;
  532. }
  533. /**
  534. * Strip style tags into the generated HTML
  535. *
  536. * @return string
  537. * @param \DOMXPath $xPath The DOMXPath for the entire document.
  538. */
  539. private function stripOriginalStyleTags(\DOMXPath $xPath)
  540. {
  541. // Get all style tags
  542. $nodes = $xPath->query('descendant-or-self::style');
  543. foreach ($nodes as $node) {
  544. if ($this->excludeMediaQueries) {
  545. //Search for Media Queries
  546. preg_match_all('/@media [^{]*{([^{}]|{[^{}]*})*}/', $node->nodeValue, $mqs);
  547. // Replace the nodeValue with just the Media Queries
  548. $node->nodeValue = implode("\n", $mqs[0]);
  549. } else {
  550. // Remove the entire style tag
  551. $node->parentNode->removeChild($node);
  552. }
  553. }
  554. }
  555. /**
  556. * Sort an array on the specificity element
  557. *
  558. * @return int
  559. * @param array $e1 The first element.
  560. * @param array $e2 The second element.
  561. */
  562. private static function sortOnSpecificity($e1, $e2)
  563. {
  564. // Compare the specificity
  565. $value = $e1['specificity']->compareTo($e2['specificity']);
  566. // if the specificity is the same, use the order in which the element appeared
  567. if ($value === 0) {
  568. $value = $e1['order'] - $e2['order'];
  569. }
  570. return $value;
  571. }
  572. }