PageRenderTime 53ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/add-ons/css2inline/css2inline.php

https://github.com/jcplat/console-seolan
PHP | 546 lines | 202 code | 108 blank | 236 comment | 26 complexity | f15b742330ade039cb95209cb56c1f49 MD5 | raw file
Possible License(s): LGPL-2.0, LGPL-2.1, GPL-3.0, Apache-2.0, BSD-3-Clause
  1. <?php
  2. /**
  3. * CSS to Inline Styles class
  4. *
  5. * This source file can be used to convert HTML with CSS into HTML with inline styles
  6. *
  7. * Known issues:
  8. * - no support for pseudo selectors
  9. *
  10. * The class is documented in the file itself. If you find any bugs help me out and report them. Reporting can be done by sending an email to php-css-to-inline-styles-bugs[at]verkoyen[dot]eu.
  11. * If you report a bug, make sure you give me enough information (include your code).
  12. *
  13. * Changelog since 1.0.2
  14. * - .class are matched from now on.
  15. * - fixed issue with #id
  16. * - new beta-feature: added a way to output valid XHTML (thx to Matt Hornsby)
  17. *
  18. * Changelog since 1.0.1
  19. * - fixed some stuff on specifity
  20. *
  21. * Changelog since 1.0.0
  22. * - rewrote the buildXPathQuery-method
  23. * - fixed some stuff on specifity
  24. * - added a way to use inline style-blocks
  25. *
  26. * License
  27. * Copyright (c) 2010, Tijs Verkoyen. All rights reserved.
  28. *
  29. * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
  30. *
  31. * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  32. * 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.
  33. * 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
  34. *
  35. * 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.
  36. *
  37. * @authorTijs Verkoyen <php-css-to-inline-styles@verkoyen.eu>
  38. * @version1.0.3
  39. *
  40. * @copyrightCopyright (c) 2010, Tijs Verkoyen. All rights reserved.
  41. * @licenseBSD License
  42. */
  43. class CSSToInlineStyles
  44. {
  45. /**
  46. * The CSS to use
  47. *
  48. * @varstring
  49. */
  50. private $css;
  51. /**
  52. * The processed CSS rules
  53. *
  54. * @vararray
  55. */
  56. private $cssRules;
  57. /**
  58. * Should the generated HTML be cleaned
  59. *
  60. * @varbool
  61. */
  62. private $cleanup = false;
  63. /**
  64. * The HTML to process
  65. *
  66. * @varstring
  67. */
  68. private $html;
  69. /**
  70. * Use inline-styles block as CSS
  71. *
  72. * @varbool
  73. */
  74. private $useInlineStylesBlock = false;
  75. /**
  76. * Creates an instance, you could set the HTML and CSS here, or load it later.
  77. *
  78. * @returnvoid
  79. * @paramstring[optional] $htmlThe HTML to process
  80. * @paramstring[optional] $cssThe CSS to use
  81. */
  82. public function __construct($html = null, $css = null)
  83. {
  84. if($html !== null) $this->setHTML($html);
  85. if($css !== null) $this->setCSS($css);
  86. }
  87. /**
  88. * Convert a CSS-selector into an xPath-query
  89. *
  90. * @returnstring
  91. * @paramstring $selectorThe CSS-selector
  92. */
  93. private function buildXPathQuery($selector)
  94. {
  95. // redefine
  96. $selector = (string) $selector;
  97. // the CSS selector
  98. $cssSelector = array('/(\w)\s+(\w)/',// E FMatches any F element that is a descendant of an E element
  99. '/(\w)\s*>\s*(\w)/',// E > FMatches any F element that is a child of an element E
  100. '/(\w):first-child/',// E:first-childMatches element E when E is the first child of its parent
  101. '/(\w)\s*\+\s*(\w)/',// E + FMatches any F element immediately preceded by an element
  102. '/(\w)\[([\w\-]+)]/',// E[foo]Matches any E element with the "foo" attribute set (whatever the value)
  103. '/(\w)\[([\w\-]+)\=\"(.*)\"]/',// E[foo="warning"]Matches any E element whose "foo" attribute value is exactly equal to "warning"
  104. '/(\w+|\*)+\.([\w\-]+)+/',// div.warningHTML only. The same as DIV[class~="warning"]
  105. '/\.([\w\-]+)/',// .warningHTML only. The same as *[class~="warning"]
  106. '/(\w+)+\#([\w\-]+)/',// E#myidMatches any E element with id-attribute equal to "myid"
  107. '/\#([\w\-]+)/'// #myidMatches any element with id-attribute equal to "myid"
  108. );
  109. // the xPath-equivalent
  110. $xPathQuery = array('\1//\2',// E FMatches any F element that is a descendant of an E element
  111. '\1/\2',// E > FMatches any F element that is a child of an element E
  112. '*[1]/self::\1',// E:first-childMatches element E when E is the first child of its parent
  113. '\1/following-sibling::*[1]/self::\2',// E + FMatches any F element immediately preceded by an element
  114. '\1 [ @\2 ]',// E[foo]Matches any E element with the "foo" attribute set (whatever the value)
  115. '\1[ contains( concat( " ", @\2, " " ), concat( " ", "\3", " " ) ) ]',// E[foo="warning"]Matches any E element whose "foo" attribute value is exactly equal to "warning"
  116. '\1[ contains( concat( " ", @class, " " ), concat( " ", "\2", " " ) ) ]',// div.warningHTML only. The same as DIV[class~="warning"]
  117. '*[ contains( concat( " ", @class, " " ), concat( " ", "\1", " " ) ) ]',// .warningHTML only. The same as *[class~="warning"]
  118. '\1[ @id = "\2" ]',// E#myidMatches any E element with id-attribute equal to "myid"
  119. '*[ @id = "\1" ]'// #myidMatches any element with id-attribute equal to "myid"
  120. );
  121. // return
  122. return (string) '//'. preg_replace($cssSelector, $xPathQuery, $selector);
  123. }
  124. /**
  125. * Calculate the specifity for the CSS-selector
  126. *
  127. * @returnint
  128. * @paramstring $selector
  129. */
  130. private function calculateCSSSpecifity($selector)
  131. {
  132. // cleanup selector
  133. $selector = str_replace(array('>', '+'), array(' > ', ' + '), $selector);
  134. // init var
  135. $specifity = 0;
  136. // split the selector into chunks based on spaces
  137. $chunks = explode(' ', $selector);
  138. // loop chunks
  139. foreach($chunks as $chunk)
  140. {
  141. // an ID is important, so give it a high specifity
  142. if(strstr($chunk, '#') !== false) $specifity += 100;
  143. // classes are more important than a tag, but less important then an ID
  144. elseif(strstr($chunk, '.')) $specifity += 10;
  145. // anything else isn't that important
  146. else $specifity += 1;
  147. }
  148. // return
  149. return $specifity;
  150. }
  151. /**
  152. * Cleanup the generated HTML
  153. *
  154. * @returnstring
  155. * @paramstring $htmlThe HTML to cleanup
  156. */
  157. private function cleanupHTML($html)
  158. {
  159. // remove classes
  160. $html = preg_replace('/(\s)+class="(.*)"(\s)+/U', ' ', $html);
  161. // remove IDs
  162. $html = preg_replace('/(\s)+id="(.*)"(\s)+/U', ' ', $html);
  163. // return
  164. return $html;
  165. }
  166. /**
  167. * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS
  168. *
  169. * @returnstring
  170. * @parambool $outputXHTMLShould we output valid XHTML?
  171. */
  172. public function convert($outputXHTML = false)
  173. {
  174. // redefine
  175. $outputXHTML = (bool) $outputXHTML;
  176. // validate
  177. if($this->html == null) throw new CSSToInlineStylesException('No HTML provided.');
  178. // should we use inline style-block
  179. if($this->useInlineStylesBlock)
  180. {
  181. // init var
  182. $matches = array();
  183. // match the style blocks
  184. preg_match_all('|<style(.*)>(.*)</style>|isU', $this->html, $matches);
  185. // any style-blocks found?
  186. if(!empty($matches[2]))
  187. {
  188. // add
  189. foreach($matches[2] as $match){
  190. $this->css .= trim($match) ."\n";
  191. //remove style from html
  192. $this->html = mb_eregi_replace('<style(.*)>(.*)</style>','', $this->html);
  193. }
  194. }
  195. }
  196. $body = $this->html;
  197. $encoding = mb_detect_encoding($body);
  198. $body = mb_convert_encoding($body, 'HTML-ENTITIES', $encoding);
  199. // create new DOMDocument
  200. $document = new DOMDocument();
  201. $document->encoding = $encoding;
  202. $document->strictErrorChecking = false;
  203. $document->formatOutput = true;
  204. // set error level
  205. libxml_use_internal_errors(true);
  206. // load HTML
  207. $document->loadHTML($body);
  208. $document->normalizeDocument();
  209. // create new XPath
  210. $xPath = new DOMXPath($document);
  211. // process css
  212. $this->processCSS();
  213. // any rules?
  214. if(!empty($this->cssRules))
  215. {
  216. // loop rules
  217. foreach($this->cssRules as $rule)
  218. {
  219. // init var
  220. $query = $this->buildXPathQuery($rule['selector']);
  221. // validate query
  222. if($query === false) continue;
  223. // search elements
  224. $elements = $xPath->query($query);
  225. // validate elements
  226. if($elements === false) continue;
  227. // loop found elements
  228. foreach($elements as $element)
  229. {
  230. // init var
  231. $properties = array();
  232. // get current styles
  233. $stylesAttribute = $element->attributes->getNamedItem('style');
  234. // any styles defined before?
  235. if($stylesAttribute !== null)
  236. {
  237. // get value for the styles attribute
  238. $definedStyles = (string) $stylesAttribute->value;
  239. // split into properties
  240. $definedProperties = (array) explode(';', $definedStyles);
  241. // loop properties
  242. foreach($definedProperties as $property)
  243. {
  244. // validate property
  245. if($property == '') continue;
  246. // split into chunks
  247. $chunks = (array) explode(':', trim($property), 2);
  248. // validate
  249. if(!isset($chunks[1])) continue;
  250. // loop chunks
  251. $properties[$chunks[0]] = trim($chunks[1]);
  252. }
  253. }
  254. // add new properties into the list
  255. foreach($rule['properties'] as $key => $value) $properties[$key] = $value;
  256. // build string
  257. $propertyChunks = array();
  258. // build chunks
  259. foreach($properties as $key => $value) $propertyChunks[] = $key .': '. $value .';';
  260. // build properties string
  261. $propertiesString = implode(' ', $propertyChunks);
  262. // set attribute
  263. if($propertiesString != '') $element->setAttribute('style', $propertiesString);
  264. }
  265. }
  266. }
  267. // should we output XHTML?
  268. if($outputXHTML)
  269. {
  270. // set formating
  271. $document->formatOutput = true;
  272. // get the HTML as XML
  273. $html = $document->saveXML(null, LIBXML_NOEMPTYTAG);
  274. // remove the XML-header
  275. $html = str_replace('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n", '', $html);
  276. }
  277. // just regular HTML 4.01 as it should be used in newsletters
  278. else
  279. {
  280. // get the HTML
  281. $html = $document->saveHTML();
  282. }
  283. // cleanup the HTML if we need to
  284. if($this->cleanup) $html = $this->cleanupHTML($html);
  285. // return
  286. return $html;
  287. }
  288. /**
  289. * Process the loaded CSS
  290. *
  291. * @returnvoid
  292. */
  293. private function processCSS()
  294. {
  295. // init vars
  296. $css = (string) $this->css;
  297. // remove newlines
  298. $css = str_replace(array("\r", "\n"), '', $css);
  299. // replace double quotes by single quotes
  300. $css = str_replace('"', '\'', $css);
  301. // remove comments
  302. $css = preg_replace('|/\*.*?\*/|', '', $css);
  303. // remove spaces
  304. $css = preg_replace('/\s\s+/', ' ', $css);
  305. // rules are splitted by }
  306. $rules = (array) explode('}', $css);
  307. // init var
  308. $i = 1;
  309. // loop rules
  310. foreach($rules as $rule)
  311. {
  312. // split into chunks
  313. $chunks = explode('{', $rule);
  314. // invalid rule?
  315. if(!isset($chunks[1])) continue;
  316. // set the selectors
  317. $selectors = trim($chunks[0]);
  318. // get cssProperties
  319. $cssProperties = trim($chunks[1]);
  320. // split multiple selectors
  321. $selectors = (array) explode(',', $selectors);
  322. // loop selectors
  323. foreach($selectors as $selector)
  324. {
  325. // cleanup
  326. $selector = trim($selector);
  327. // build an array for each selector
  328. $ruleSet = array();
  329. // store selector
  330. $ruleSet['selector'] = $selector;
  331. // process the properties
  332. $ruleSet['properties'] = $this->processCSSProperties($cssProperties);
  333. // calculate specifity
  334. $ruleSet['specifity'] = $this->calculateCSSSpecifity($selector);
  335. // add into global rules
  336. $this->cssRules[] = $ruleSet;
  337. }
  338. // increment
  339. $i++;
  340. }
  341. // sort based on specifity
  342. if(!empty($this->cssRules)) usort($this->cssRules, array('CSSToInlineStyles', 'sortOnSpecifity'));
  343. }
  344. /**
  345. * Process the CSS-properties
  346. *
  347. * @returnarray
  348. * @paramstring $propertyString
  349. */
  350. private function processCSSProperties($propertyString)
  351. {
  352. // split into chunks
  353. $properties = (array) explode(';', $propertyString);
  354. // init var
  355. $pairs = array();
  356. // loop properties
  357. foreach($properties as $property)
  358. {
  359. // split into chunks
  360. $chunks = (array) explode(':', $property, 2);
  361. // validate
  362. if(!isset($chunks[1])) continue;
  363. // add to pairs array
  364. $pairs[trim($chunks[0])] = trim($chunks[1]);
  365. }
  366. // sort the pairs
  367. ksort($pairs);
  368. // return
  369. return $pairs;
  370. }
  371. /**
  372. * Should the IDs and classes be removed?
  373. *
  374. * @returnvoid
  375. * @parambool[optional] $on
  376. */
  377. public function setCleanup($on = true)
  378. {
  379. $this->cleanup = (bool) $on;
  380. }
  381. /**
  382. * Set CSS to use
  383. *
  384. * @returnvoid
  385. * @paramstring $cssThe CSS to use
  386. */
  387. public function setCSS($css)
  388. {
  389. $this->css = (string) $css;
  390. }
  391. /**
  392. * Set HTML to process
  393. *
  394. * @returnvoid
  395. * @paramstring $html
  396. */
  397. public function setHTML($html)
  398. {
  399. $this->html = (string) $html;
  400. }
  401. /**
  402. * Set use of inline styles block
  403. * If this is enabled the class will use the style-block in the HTML.
  404. *
  405. * @parambool[optional] $on
  406. */
  407. public function setUseInlineStylesBlock($on = true)
  408. {
  409. $this->useInlineStylesBlock = (bool) $on;
  410. }
  411. /**
  412. * Sort an array on the specifity element
  413. *
  414. * @returnint
  415. * @paramarray $e1The first element
  416. * @paramarray $e2The second element
  417. */
  418. private static function sortOnSpecifity($e1, $e2)
  419. {
  420. // validate
  421. if(!isset($e1['specifity']) || !isset($e2['specifity'])) return 0;
  422. // lower
  423. if($e1['specifity'] < $e2['specifity']) return -1;
  424. // higher
  425. if($e1['specifity'] > $e2['specifity']) return 1;
  426. // fallback
  427. return 0;
  428. }
  429. }
  430. /**
  431. * CSSToInlineStyles Exception class
  432. *
  433. * @authorTijs Verkoyen <php-css-to-inline-styles@verkoyen.eu>
  434. */
  435. class CSSToInlineStylesException extends Exception
  436. {
  437. }
  438. ?>