Нема описа

CssToInlineStyles.php 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. <?php
  2. namespace TijsVerkoyen\CssToInlineStyles;
  3. use Symfony\Component\CssSelector\CssSelector;
  4. use Symfony\Component\CssSelector\CssSelectorConverter;
  5. use Symfony\Component\CssSelector\Exception\ExceptionInterface;
  6. use TijsVerkoyen\CssToInlineStyles\Css\Processor;
  7. use TijsVerkoyen\CssToInlineStyles\Css\Property\Processor as PropertyProcessor;
  8. use TijsVerkoyen\CssToInlineStyles\Css\Rule\Processor as RuleProcessor;
  9. class CssToInlineStyles
  10. {
  11. private $cssConverter;
  12. public function __construct()
  13. {
  14. if (class_exists('Symfony\Component\CssSelector\CssSelectorConverter')) {
  15. $this->cssConverter = new CssSelectorConverter();
  16. }
  17. }
  18. /**
  19. * Will inline the $css into the given $html
  20. *
  21. * Remark: if the html contains <style>-tags those will be used, the rules
  22. * in $css will be appended.
  23. *
  24. * @param string $html
  25. * @param string $css
  26. *
  27. * @return string
  28. */
  29. public function convert($html, $css = null)
  30. {
  31. $document = $this->createDomDocumentFromHtml($html);
  32. $processor = new Processor();
  33. // get all styles from the style-tags
  34. $rules = $processor->getRules(
  35. $processor->getCssFromStyleTags($html)
  36. );
  37. if ($css !== null) {
  38. $rules = $processor->getRules($css, $rules);
  39. }
  40. $document = $this->inline($document, $rules);
  41. return $this->getHtmlFromDocument($document);
  42. }
  43. /**
  44. * Inline the given properties on an given DOMElement
  45. *
  46. * @param \DOMElement $element
  47. * @param Css\Property\Property[] $properties
  48. *
  49. * @return \DOMElement
  50. */
  51. public function inlineCssOnElement(\DOMElement $element, array $properties)
  52. {
  53. if (empty($properties)) {
  54. return $element;
  55. }
  56. $cssProperties = array();
  57. $inlineProperties = array();
  58. foreach ($this->getInlineStyles($element) as $property) {
  59. $inlineProperties[$property->getName()] = $property;
  60. }
  61. foreach ($properties as $property) {
  62. if (!isset($inlineProperties[$property->getName()])) {
  63. $cssProperties[$property->getName()] = $property;
  64. }
  65. }
  66. $rules = array();
  67. foreach (array_merge($cssProperties, $inlineProperties) as $property) {
  68. $rules[] = $property->toString();
  69. }
  70. $element->setAttribute('style', implode(' ', $rules));
  71. return $element;
  72. }
  73. /**
  74. * Get the current inline styles for a given DOMElement
  75. *
  76. * @param \DOMElement $element
  77. *
  78. * @return Css\Property\Property[]
  79. */
  80. public function getInlineStyles(\DOMElement $element)
  81. {
  82. $processor = new PropertyProcessor();
  83. return $processor->convertArrayToObjects(
  84. $processor->splitIntoSeparateProperties(
  85. $element->getAttribute('style')
  86. )
  87. );
  88. }
  89. /**
  90. * @param string $html
  91. *
  92. * @return \DOMDocument
  93. */
  94. protected function createDomDocumentFromHtml($html)
  95. {
  96. $document = new \DOMDocument('1.0', 'UTF-8');
  97. $internalErrors = libxml_use_internal_errors(true);
  98. $document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
  99. libxml_use_internal_errors($internalErrors);
  100. $document->formatOutput = true;
  101. return $document;
  102. }
  103. /**
  104. * @param \DOMDocument $document
  105. *
  106. * @return string
  107. */
  108. protected function getHtmlFromDocument(\DOMDocument $document)
  109. {
  110. // retrieve the document element
  111. // we do it this way to preserve the utf-8 encoding
  112. $htmlElement = $document->documentElement;
  113. $html = $document->saveHTML($htmlElement);
  114. $html = trim($html);
  115. // retrieve the doctype
  116. $document->removeChild($htmlElement);
  117. $doctype = $document->saveHTML();
  118. $doctype = trim($doctype);
  119. // if it is the html5 doctype convert it to lowercase
  120. if ($doctype === '<!DOCTYPE html>') {
  121. $doctype = strtolower($doctype);
  122. }
  123. return $doctype."\n".$html;
  124. }
  125. /**
  126. * @param \DOMDocument $document
  127. * @param Css\Rule\Rule[] $rules
  128. *
  129. * @return \DOMDocument
  130. */
  131. protected function inline(\DOMDocument $document, array $rules)
  132. {
  133. if (empty($rules)) {
  134. return $document;
  135. }
  136. $propertyStorage = new \SplObjectStorage();
  137. $xPath = new \DOMXPath($document);
  138. usort($rules, array(RuleProcessor::class, 'sortOnSpecificity'));
  139. foreach ($rules as $rule) {
  140. try {
  141. if (null !== $this->cssConverter) {
  142. $expression = $this->cssConverter->toXPath($rule->getSelector());
  143. } else {
  144. // Compatibility layer for Symfony 2.7 and older
  145. $expression = CssSelector::toXPath($rule->getSelector());
  146. }
  147. } catch (ExceptionInterface $e) {
  148. continue;
  149. }
  150. $elements = $xPath->query($expression);
  151. if ($elements === false) {
  152. continue;
  153. }
  154. foreach ($elements as $element) {
  155. $propertyStorage[$element] = $this->calculatePropertiesToBeApplied(
  156. $rule->getProperties(),
  157. $propertyStorage->contains($element) ? $propertyStorage[$element] : array()
  158. );
  159. }
  160. }
  161. foreach ($propertyStorage as $element) {
  162. $this->inlineCssOnElement($element, $propertyStorage[$element]);
  163. }
  164. return $document;
  165. }
  166. /**
  167. * Merge the CSS rules to determine the applied properties.
  168. *
  169. * @param Css\Property\Property[] $properties
  170. * @param Css\Property\Property[] $cssProperties existing applied properties indexed by name
  171. *
  172. * @return Css\Property\Property[] updated properties, indexed by name
  173. */
  174. private function calculatePropertiesToBeApplied(array $properties, array $cssProperties)
  175. {
  176. if (empty($properties)) {
  177. return $cssProperties;
  178. }
  179. foreach ($properties as $property) {
  180. if (isset($cssProperties[$property->getName()])) {
  181. $existingProperty = $cssProperties[$property->getName()];
  182. //skip check to overrule if existing property is important and current is not
  183. if ($existingProperty->isImportant() && !$property->isImportant()) {
  184. continue;
  185. }
  186. //overrule if current property is important and existing is not, else check specificity
  187. $overrule = !$existingProperty->isImportant() && $property->isImportant();
  188. if (!$overrule) {
  189. $overrule = $existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0;
  190. }
  191. if ($overrule) {
  192. unset($cssProperties[$property->getName()]);
  193. $cssProperties[$property->getName()] = $property;
  194. }
  195. } else {
  196. $cssProperties[$property->getName()] = $property;
  197. }
  198. }
  199. return $cssProperties;
  200. }
  201. }