organizrnginxsonarrradarrplexdashboardcouchpotatonzbgetbookmarkapplication-dashboardmuximuxlandingpagestartpagelandinghtpcserverhomepagesabnzbdheimdallemby
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
235 lines
7.2 KiB
235 lines
7.2 KiB
<?php
|
|
|
|
namespace TijsVerkoyen\CssToInlineStyles;
|
|
|
|
use Symfony\Component\CssSelector\CssSelector;
|
|
use Symfony\Component\CssSelector\CssSelectorConverter;
|
|
use Symfony\Component\CssSelector\Exception\ExceptionInterface;
|
|
use TijsVerkoyen\CssToInlineStyles\Css\Processor;
|
|
use TijsVerkoyen\CssToInlineStyles\Css\Property\Processor as PropertyProcessor;
|
|
use TijsVerkoyen\CssToInlineStyles\Css\Rule\Processor as RuleProcessor;
|
|
use TijsVerkoyen\CssToInlineStyles\Css\Rule\Rule;
|
|
|
|
class CssToInlineStyles
|
|
{
|
|
private $cssConverter;
|
|
|
|
public function __construct()
|
|
{
|
|
if (class_exists('Symfony\Component\CssSelector\CssSelectorConverter')) {
|
|
$this->cssConverter = new CssSelectorConverter();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Will inline the $css into the given $html
|
|
*
|
|
* Remark: if the html contains <style>-tags those will be used, the rules
|
|
* in $css will be appended.
|
|
*
|
|
* @param string $html
|
|
* @param string $css
|
|
* @return string
|
|
*/
|
|
public function convert($html, $css = null)
|
|
{
|
|
$document = $this->createDomDocumentFromHtml($html);
|
|
$processor = new Processor();
|
|
|
|
// get all styles from the style-tags
|
|
$rules = $processor->getRules(
|
|
$processor->getCssFromStyleTags($html)
|
|
);
|
|
|
|
if ($css !== null) {
|
|
$rules = $processor->getRules($css, $rules);
|
|
}
|
|
|
|
$document = $this->inline($document, $rules);
|
|
|
|
return $this->getHtmlFromDocument($document);
|
|
}
|
|
|
|
/**
|
|
* Inline the given properties on an given DOMElement
|
|
*
|
|
* @param \DOMElement $element
|
|
* @param Css\Property\Property[] $properties
|
|
* @return \DOMElement
|
|
*/
|
|
public function inlineCssOnElement(\DOMElement $element, array $properties)
|
|
{
|
|
if (empty($properties)) {
|
|
return $element;
|
|
}
|
|
|
|
$cssProperties = array();
|
|
$inlineProperties = array();
|
|
|
|
foreach ($this->getInlineStyles($element) as $property) {
|
|
$inlineProperties[$property->getName()] = $property;
|
|
}
|
|
|
|
foreach ($properties as $property) {
|
|
if (!isset($inlineProperties[$property->getName()])) {
|
|
$cssProperties[$property->getName()] = $property;
|
|
}
|
|
}
|
|
|
|
$rules = array();
|
|
foreach (array_merge($cssProperties, $inlineProperties) as $property) {
|
|
$rules[] = $property->toString();
|
|
}
|
|
$element->setAttribute('style', implode(' ', $rules));
|
|
|
|
return $element;
|
|
}
|
|
|
|
/**
|
|
* Get the current inline styles for a given DOMElement
|
|
*
|
|
* @param \DOMElement $element
|
|
* @return Css\Property\Property[]
|
|
*/
|
|
public function getInlineStyles(\DOMElement $element)
|
|
{
|
|
$processor = new PropertyProcessor();
|
|
|
|
return $processor->convertArrayToObjects(
|
|
$processor->splitIntoSeparateProperties(
|
|
$element->getAttribute('style')
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param string $html
|
|
* @return \DOMDocument
|
|
*/
|
|
protected function createDomDocumentFromHtml($html)
|
|
{
|
|
$document = new \DOMDocument('1.0', 'UTF-8');
|
|
$internalErrors = libxml_use_internal_errors(true);
|
|
$document->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
|
libxml_use_internal_errors($internalErrors);
|
|
$document->formatOutput = true;
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* @param \DOMDocument $document
|
|
* @return string
|
|
*/
|
|
protected function getHtmlFromDocument(\DOMDocument $document)
|
|
{
|
|
// retrieve the document element
|
|
// we do it this way to preserve the utf-8 encoding
|
|
$htmlElement = $document->documentElement;
|
|
$html = $document->saveHTML($htmlElement);
|
|
$html = trim($html);
|
|
|
|
// retrieve the doctype
|
|
$document->removeChild($htmlElement);
|
|
$doctype = $document->saveHTML();
|
|
$doctype = trim($doctype);
|
|
|
|
// if it is the html5 doctype convert it to lowercase
|
|
if ($doctype === '<!DOCTYPE html>') {
|
|
$doctype = strtolower($doctype);
|
|
}
|
|
|
|
return $doctype."\n".$html;
|
|
}
|
|
|
|
/**
|
|
* @param \DOMDocument $document
|
|
* @param Css\Rule\Rule[] $rules
|
|
* @return \DOMDocument
|
|
*/
|
|
protected function inline(\DOMDocument $document, array $rules)
|
|
{
|
|
if (empty($rules)) {
|
|
return $document;
|
|
}
|
|
|
|
$propertyStorage = new \SplObjectStorage();
|
|
|
|
$xPath = new \DOMXPath($document);
|
|
|
|
usort($rules, array(RuleProcessor::class, 'sortOnSpecificity'));
|
|
|
|
foreach ($rules as $rule) {
|
|
try {
|
|
if (null !== $this->cssConverter) {
|
|
$expression = $this->cssConverter->toXPath($rule->getSelector());
|
|
} else {
|
|
// Compatibility layer for Symfony 2.7 and older
|
|
$expression = CssSelector::toXPath($rule->getSelector());
|
|
}
|
|
} catch (ExceptionInterface $e) {
|
|
continue;
|
|
}
|
|
|
|
$elements = $xPath->query($expression);
|
|
|
|
if ($elements === false) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($elements as $element) {
|
|
$propertyStorage[$element] = $this->calculatePropertiesToBeApplied(
|
|
$rule->getProperties(),
|
|
$propertyStorage->contains($element) ? $propertyStorage[$element] : array()
|
|
);
|
|
}
|
|
}
|
|
|
|
foreach ($propertyStorage as $element) {
|
|
$this->inlineCssOnElement($element, $propertyStorage[$element]);
|
|
}
|
|
|
|
return $document;
|
|
}
|
|
|
|
/**
|
|
* Merge the CSS rules to determine the applied properties.
|
|
*
|
|
* @param Css\Property\Property[] $properties
|
|
* @param Css\Property\Property[] $cssProperties existing applied properties indexed by name
|
|
*
|
|
* @return Css\Property\Property[] updated properties, indexed by name
|
|
*/
|
|
private function calculatePropertiesToBeApplied(array $properties, array $cssProperties)
|
|
{
|
|
if (empty($properties)) {
|
|
return $cssProperties;
|
|
}
|
|
|
|
foreach ($properties as $property) {
|
|
if (isset($cssProperties[$property->getName()])) {
|
|
$existingProperty = $cssProperties[$property->getName()];
|
|
|
|
//skip check to overrule if existing property is important and current is not
|
|
if ($existingProperty->isImportant() && !$property->isImportant()) {
|
|
continue;
|
|
}
|
|
|
|
//overrule if current property is important and existing is not, else check specificity
|
|
$overrule = !$existingProperty->isImportant() && $property->isImportant();
|
|
if (!$overrule) {
|
|
$overrule = $existingProperty->getOriginalSpecificity()->compareTo($property->getOriginalSpecificity()) <= 0;
|
|
}
|
|
|
|
if ($overrule) {
|
|
unset($cssProperties[$property->getName()]);
|
|
$cssProperties[$property->getName()] = $property;
|
|
}
|
|
} else {
|
|
$cssProperties[$property->getName()] = $property;
|
|
}
|
|
}
|
|
|
|
return $cssProperties;
|
|
}
|
|
}
|
|
|