PageRenderTime 47ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

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

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