PHP 8.5 compatibility - closes #5240

This commit is contained in:
FreeScout
2026-02-26 06:40:26 -08:00
parent ee9cda1f0c
commit 7c9f9635c5
7 changed files with 2815 additions and 3 deletions

View File

@@ -82,7 +82,8 @@
"classmap": [
"database/seeds",
"database/factories",
"overrides/natxet/cssmin/src/"
"overrides/natxet/cssmin/src/",
"overrides/erusev/parsedown/"
],
"psr-0": {
"Swift_": "overrides/swiftmailer/swiftmailer/lib/classes",
@@ -198,6 +199,8 @@
"Symfony\\Component\\Console\\Exception\\": "overrides/symfony/console/Exception/",
"Symfony\\Component\\Console\\Question\\": "overrides/symfony/console/",
"Symfony\\Component\\CssSelector\\XPath\\Extension\\": "overrides/symfony/css-selector/XPath/Extension/",
"Symfony\\Component\\CssSelector\\XPath\\": "overrides/symfony/css-selector/XPath/",
"Symfony\\Component\\CssSelector\\Parser\\": "overrides/symfony/css-selector/Parser/",
"Symfony\\Component\\Translation\\Loader\\": "overrides/symfony/translation/Loader/",
"Symfony\\Component\\Translation\\Formatter\\": "overrides/symfony/translation/Formatter/",
"Symfony\\Component\\Translation\\": "overrides/symfony/translation/",
@@ -215,7 +218,9 @@
"Psy\\TimeitCommand\\": "overrides/psy/psysh/src/TimeitCommand/",
"Psy\\Command\\ListCommand\\": "overrides/psy/psysh/src/Command/ListCommand/",
"Psy\\Exception\\": "overrides/psy/psysh/src/ErrorException/",
"Psy\\VarDumper\\": "overrides/psy/psysh/src/VarDumper/"
"Psy\\VarDumper\\": "overrides/psy/psysh/src/VarDumper/",
"TijsVerkoyen\\CssToInlineStyles\\": "overrides/tijsverkoyen/css-to-inline-styles/src/",
"TijsVerkoyen\\CssToInlineStyles\\Css\\Property\\": "overrides/tijsverkoyen/css-to-inline-styles/src/Css/Property/"
},
"exclude-from-classmap": [
"Modules/",
@@ -399,6 +404,8 @@
"vendor/symfony/console/Question/Question.php",
"vendor/symfony/finder/Iterator/SortableIterator.php",
"vendor/symfony/css-selector/XPath/Extension/NodeExtension.php",
"vendor/symfony/css-selector/XPath/Translator.php",
"vendor/symfony/css-selector/Parser/Parser.php",
"vendor/symfony/translation/Loader/ArrayLoader.php",
"vendor/symfony/translation/Formatter/MessageFormatter.php",
"vendor/symfony/translation/Translator.php",
@@ -499,7 +506,11 @@
"vendor/psy/psysh/src/Exception/BreakException.php",
"vendor/psy/psysh/src/VarDumper/Dumper.php",
"vendor/laravel/framework/src/Illuminate/Foundation/Application.php",
"vendor/laravel/framework/src/Illuminate/Contracts/Container/Container.php"
"vendor/laravel/framework/src/Illuminate/Contracts/Container/Container.php",
"vendor/erusev/parsedown/Parsedown.php",
"vendor/tijsverkoyen/css-to-inline-styles/src/CssToInlineStyles.php",
"vendor/tijsverkoyen/css-to-inline-styles/src/Css/Property/Processor.php",
"vendor/tijsverkoyen/css-to-inline-styles/src/Css/Property/Property.php"
]
},
"autoload-dev": {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,384 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\Parser;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Node;
use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer;
/**
* CSS selector parser.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Parser implements ParserInterface
{
private $tokenizer;
public function __construct(?Tokenizer $tokenizer = null)
{
$this->tokenizer = $tokenizer ?: new Tokenizer();
}
/**
* {@inheritdoc}
*/
public function parse($source)
{
$reader = new Reader($source);
$stream = $this->tokenizer->tokenize($reader);
return $this->parseSelectorList($stream);
}
/**
* Parses the arguments for ":nth-child()" and friends.
*
* @param Token[] $tokens
*
* @return array
*
* @throws SyntaxErrorException
*/
public static function parseSeries(array $tokens)
{
foreach ($tokens as $token) {
if ($token->isString()) {
throw SyntaxErrorException::stringAsFunctionArgument();
}
}
$joined = trim(implode('', array_map(function (Token $token) {
return $token->getValue();
}, $tokens)));
$int = function ($string) {
if (!is_numeric($string)) {
throw SyntaxErrorException::stringAsFunctionArgument();
}
return (int) $string;
};
switch (true) {
case 'odd' === $joined:
return array(2, 1);
case 'even' === $joined:
return array(2, 0);
case 'n' === $joined:
return array(1, 0);
case false === strpos($joined, 'n'):
return array(0, $int($joined));
}
$split = explode('n', $joined);
$first = isset($split[0]) ? $split[0] : null;
return array(
$first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1,
isset($split[1]) && $split[1] ? $int($split[1]) : 0,
);
}
/**
* Parses selector nodes.
*
* @return array
*/
private function parseSelectorList(TokenStream $stream)
{
$stream->skipWhitespace();
$selectors = array();
while (true) {
$selectors[] = $this->parserSelectorNode($stream);
if ($stream->getPeek()->isDelimiter(array(','))) {
$stream->getNext();
$stream->skipWhitespace();
} else {
break;
}
}
return $selectors;
}
/**
* Parses next selector or combined node.
*
* @return Node\SelectorNode
*
* @throws SyntaxErrorException
*/
private function parserSelectorNode(TokenStream $stream)
{
list($result, $pseudoElement) = $this->parseSimpleSelector($stream);
while (true) {
$stream->skipWhitespace();
$peek = $stream->getPeek();
if ($peek->isFileEnd() || $peek->isDelimiter(array(','))) {
break;
}
if (null !== $pseudoElement) {
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
}
if ($peek->isDelimiter(array('+', '>', '~'))) {
$combinator = $stream->getNext()->getValue();
$stream->skipWhitespace();
} else {
$combinator = ' ';
}
list($nextSelector, $pseudoElement) = $this->parseSimpleSelector($stream);
$result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
}
return new Node\SelectorNode($result, $pseudoElement);
}
/**
* Parses next simple node (hash, class, pseudo, negation).
*
* @param TokenStream $stream
* @param bool $insideNegation
*
* @return array
*
* @throws SyntaxErrorException
*/
private function parseSimpleSelector(TokenStream $stream, $insideNegation = false)
{
$stream->skipWhitespace();
$selectorStart = \count($stream->getUsed());
$result = $this->parseElementNode($stream);
$pseudoElement = null;
while (true) {
$peek = $stream->getPeek();
if ($peek->isWhitespace()
|| $peek->isFileEnd()
|| $peek->isDelimiter(array(',', '+', '>', '~'))
|| ($insideNegation && $peek->isDelimiter(array(')')))
) {
break;
}
if (null !== $pseudoElement) {
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
}
if ($peek->isHash()) {
$result = new Node\HashNode($result, $stream->getNext()->getValue());
} elseif ($peek->isDelimiter(array('.'))) {
$stream->getNext();
$result = new Node\ClassNode($result, $stream->getNextIdentifier());
} elseif ($peek->isDelimiter(array('['))) {
$stream->getNext();
$result = $this->parseAttributeNode($result, $stream);
} elseif ($peek->isDelimiter(array(':'))) {
$stream->getNext();
if ($stream->getPeek()->isDelimiter(array(':'))) {
$stream->getNext();
$pseudoElement = $stream->getNextIdentifier();
continue;
}
$identifier = $stream->getNextIdentifier();
if (\in_array(strtolower($identifier), array('first-line', 'first-letter', 'before', 'after'))) {
// Special case: CSS 2.1 pseudo-elements can have a single ':'.
// Any new pseudo-element must have two.
$pseudoElement = $identifier;
continue;
}
if (!$stream->getPeek()->isDelimiter(array('('))) {
$result = new Node\PseudoNode($result, $identifier);
continue;
}
$stream->getNext();
$stream->skipWhitespace();
if ('not' === strtolower($identifier)) {
if ($insideNegation) {
throw SyntaxErrorException::nestedNot();
}
list($argument, $argumentPseudoElement) = $this->parseSimpleSelector($stream, true);
$next = $stream->getNext();
if (null !== $argumentPseudoElement) {
throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside ::not()');
}
if (!$next->isDelimiter(array(')'))) {
throw SyntaxErrorException::unexpectedToken('")"', $next);
}
$result = new Node\NegationNode($result, $argument);
} else {
$arguments = array();
$next = null;
while (true) {
$stream->skipWhitespace();
$next = $stream->getNext();
if ($next->isIdentifier()
|| $next->isString()
|| $next->isNumber()
|| $next->isDelimiter(array('+', '-'))
) {
$arguments[] = $next;
} elseif ($next->isDelimiter(array(')'))) {
break;
} else {
throw SyntaxErrorException::unexpectedToken('an argument', $next);
}
}
if (empty($arguments)) {
throw SyntaxErrorException::unexpectedToken('at least one argument', $next);
}
$result = new Node\FunctionNode($result, $identifier, $arguments);
}
} else {
throw SyntaxErrorException::unexpectedToken('selector', $peek);
}
}
if (\count($stream->getUsed()) === $selectorStart) {
throw SyntaxErrorException::unexpectedToken('selector', $stream->getPeek());
}
return array($result, $pseudoElement);
}
/**
* Parses next element node.
*
* @return Node\ElementNode
*/
private function parseElementNode(TokenStream $stream)
{
$peek = $stream->getPeek();
if ($peek->isIdentifier() || $peek->isDelimiter(array('*'))) {
if ($peek->isIdentifier()) {
$namespace = $stream->getNext()->getValue();
} else {
$stream->getNext();
$namespace = null;
}
if ($stream->getPeek()->isDelimiter(array('|'))) {
$stream->getNext();
$element = $stream->getNextIdentifierOrStar();
} else {
$element = $namespace;
$namespace = null;
}
} else {
$element = $namespace = null;
}
return new Node\ElementNode($namespace, $element);
}
/**
* Parses next attribute node.
*
* @return Node\AttributeNode
*
* @throws SyntaxErrorException
*/
private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream)
{
$stream->skipWhitespace();
$attribute = $stream->getNextIdentifierOrStar();
if (null === $attribute && !$stream->getPeek()->isDelimiter(array('|'))) {
throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek());
}
if ($stream->getPeek()->isDelimiter(array('|'))) {
$stream->getNext();
if ($stream->getPeek()->isDelimiter(array('='))) {
$namespace = null;
$stream->getNext();
$operator = '|=';
} else {
$namespace = $attribute;
$attribute = $stream->getNextIdentifier();
$operator = null;
}
} else {
$namespace = $operator = null;
}
if (null === $operator) {
$stream->skipWhitespace();
$next = $stream->getNext();
if ($next->isDelimiter(array(']'))) {
return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null);
} elseif ($next->isDelimiter(array('='))) {
$operator = '=';
} elseif ($next->isDelimiter(array('^', '$', '*', '~', '|', '!'))
&& $stream->getPeek()->isDelimiter(array('='))
) {
$operator = $next->getValue().'=';
$stream->getNext();
} else {
throw SyntaxErrorException::unexpectedToken('operator', $next);
}
}
$stream->skipWhitespace();
$value = $stream->getNext();
if ($value->isNumber()) {
// if the value is a number, it's casted into a string
$value = new Token(Token::TYPE_STRING, (string) $value->getValue(), $value->getPosition());
}
if (!($value->isIdentifier() || $value->isString())) {
throw SyntaxErrorException::unexpectedToken('string or identifier', $value);
}
$stream->skipWhitespace();
$next = $stream->getNext();
if (!$next->isDelimiter(array(']'))) {
throw SyntaxErrorException::unexpectedToken('"]"', $next);
}
return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue());
}
}

View File

@@ -0,0 +1,267 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\CssSelector\XPath;
use Symfony\Component\CssSelector\Exception\ExpressionErrorException;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\Node\NodeInterface;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\Parser;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* XPath expression translator interface.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Translator implements TranslatorInterface
{
private $mainParser;
/**
* @var ParserInterface[]
*/
private $shortcutParsers = array();
/**
* @var Extension\ExtensionInterface[]
*/
private $extensions = array();
private $nodeTranslators = array();
private $combinationTranslators = array();
private $functionTranslators = array();
private $pseudoClassTranslators = array();
private $attributeMatchingTranslators = array();
public function __construct(?ParserInterface $parser = null)
{
$this->mainParser = $parser ?: new Parser();
$this
->registerExtension(new Extension\NodeExtension())
->registerExtension(new Extension\CombinationExtension())
->registerExtension(new Extension\FunctionExtension())
->registerExtension(new Extension\PseudoClassExtension())
->registerExtension(new Extension\AttributeMatchingExtension())
;
}
/**
* @param string $element
*
* @return string
*/
public static function getXpathLiteral($element)
{
if (false === strpos($element, "'")) {
return "'".$element."'";
}
if (false === strpos($element, '"')) {
return '"'.$element.'"';
}
$string = $element;
$parts = array();
while (true) {
if (false !== $pos = strpos($string, "'")) {
$parts[] = sprintf("'%s'", substr($string, 0, $pos));
$parts[] = "\"'\"";
$string = substr($string, $pos + 1);
} else {
$parts[] = "'$string'";
break;
}
}
return sprintf('concat(%s)', implode(', ', $parts));
}
/**
* {@inheritdoc}
*/
public function cssToXPath($cssExpr, $prefix = 'descendant-or-self::')
{
$selectors = $this->parseSelectors($cssExpr);
/** @var SelectorNode $selector */
foreach ($selectors as $index => $selector) {
if (null !== $selector->getPseudoElement()) {
throw new ExpressionErrorException('Pseudo-elements are not supported.');
}
$selectors[$index] = $this->selectorToXPath($selector, $prefix);
}
return implode(' | ', $selectors);
}
/**
* {@inheritdoc}
*/
public function selectorToXPath(SelectorNode $selector, $prefix = 'descendant-or-self::')
{
return ($prefix ?: '').$this->nodeToXPath($selector);
}
/**
* Registers an extension.
*
* @return $this
*/
public function registerExtension(Extension\ExtensionInterface $extension)
{
$this->extensions[$extension->getName()] = $extension;
$this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators());
$this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators());
$this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators());
$this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators());
$this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators());
return $this;
}
/**
* @param string $name
*
* @return Extension\ExtensionInterface
*
* @throws ExpressionErrorException
*/
public function getExtension($name)
{
if (!isset($this->extensions[$name])) {
throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name));
}
return $this->extensions[$name];
}
/**
* Registers a shortcut parser.
*
* @return $this
*/
public function registerParserShortcut(ParserInterface $shortcut)
{
$this->shortcutParsers[] = $shortcut;
return $this;
}
/**
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function nodeToXPath(NodeInterface $node)
{
if (!isset($this->nodeTranslators[$node->getNodeName()])) {
throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName()));
}
return \call_user_func($this->nodeTranslators[$node->getNodeName()], $node, $this);
}
/**
* @param string $combiner
* @param NodeInterface $xpath
* @param NodeInterface $combinedXpath
*
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function addCombination($combiner, NodeInterface $xpath, NodeInterface $combinedXpath)
{
if (!isset($this->combinationTranslators[$combiner])) {
throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner));
}
return \call_user_func($this->combinationTranslators[$combiner], $this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath));
}
/**
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function addFunction(XPathExpr $xpath, FunctionNode $function)
{
if (!isset($this->functionTranslators[$function->getName()])) {
throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName()));
}
return \call_user_func($this->functionTranslators[$function->getName()], $xpath, $function);
}
/**
* @param XPathExpr $xpath
* @param string $pseudoClass
*
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function addPseudoClass(XPathExpr $xpath, $pseudoClass)
{
if (!isset($this->pseudoClassTranslators[$pseudoClass])) {
throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass));
}
return \call_user_func($this->pseudoClassTranslators[$pseudoClass], $xpath);
}
/**
* @param XPathExpr $xpath
* @param string $operator
* @param string $attribute
* @param string $value
*
* @return XPathExpr
*
* @throws ExpressionErrorException
*/
public function addAttributeMatching(XPathExpr $xpath, $operator, $attribute, $value)
{
if (!isset($this->attributeMatchingTranslators[$operator])) {
throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator));
}
return \call_user_func($this->attributeMatchingTranslators[$operator], $xpath, $attribute, $value);
}
/**
* @param string $css
*
* @return SelectorNode[]
*/
private function parseSelectors($css)
{
foreach ($this->shortcutParsers as $shortcut) {
$tokens = $shortcut->parse($css);
if (!empty($tokens)) {
return $tokens;
}
}
return $this->mainParser->parse($css);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace TijsVerkoyen\CssToInlineStyles\Css\Property;
use Symfony\Component\CssSelector\Node\Specificity;
class Processor
{
/**
* Split a string into separate properties
*
* @param string $propertiesString
*
* @return string[]
*/
public function splitIntoSeparateProperties($propertiesString)
{
$propertiesString = $this->cleanup($propertiesString);
$properties = (array) explode(';', $propertiesString);
$keysToRemove = array();
$numberOfProperties = count($properties);
for ($i = 0; $i < $numberOfProperties; $i++) {
$properties[$i] = trim($properties[$i]);
// if the new property begins with base64 it is part of the current property
if (isset($properties[$i + 1]) && strpos(trim($properties[$i + 1]), 'base64,') === 0) {
$properties[$i] .= ';' . trim($properties[$i + 1]);
$keysToRemove[] = $i + 1;
}
}
if (!empty($keysToRemove)) {
foreach ($keysToRemove as $key) {
unset($properties[$key]);
}
}
return array_values($properties);
}
/**
* @param string $string
*
* @return string
*/
private function cleanup($string)
{
$string = str_replace(array("\r", "\n"), '', $string);
$string = str_replace(array("\t"), ' ', $string);
$string = str_replace('"', '\'', $string);
$string = preg_replace('|/\*.*?\*/|', '', $string);
$string = preg_replace('/\s\s+/', ' ', $string);
$string = trim($string);
$string = rtrim($string, ';');
return $string;
}
/**
* Converts a property-string into an object
*
* @param string $property
*
* @return Property|null
*/
public function convertToObject($property, ?Specificity $specificity = null)
{
if (strpos($property, ':') === false) {
return null;
}
list($name, $value) = explode(':', $property, 2);
$name = trim($name);
$value = trim($value);
if ($value === '') {
return null;
}
return new Property($name, $value, $specificity);
}
/**
* Converts an array of property-strings into objects
*
* @param string[] $properties
*
* @return Property[]
*/
public function convertArrayToObjects(array $properties, ?Specificity $specificity = null)
{
$objects = array();
foreach ($properties as $property) {
$object = $this->convertToObject($property, $specificity);
if ($object === null) {
continue;
}
$objects[] = $object;
}
return $objects;
}
/**
* Build the property-string for multiple properties
*
* @param Property[] $properties
*
* @return string
*/
public function buildPropertiesString(array $properties)
{
$chunks = array();
foreach ($properties as $property) {
$chunks[] = $property->toString();
}
return implode(' ', $chunks);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace TijsVerkoyen\CssToInlineStyles\Css\Property;
use Symfony\Component\CssSelector\Node\Specificity;
final class Property
{
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $value;
/**
* @var Specificity
*/
private $originalSpecificity;
/**
* Property constructor.
* @param string $name
* @param string $value
* @param Specificity|null $specificity
*/
public function __construct($name, $value, ?Specificity $specificity = null)
{
$this->name = $name;
$this->value = $value;
$this->originalSpecificity = $specificity;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Get value
*
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* Get originalSpecificity
*
* @return Specificity
*/
public function getOriginalSpecificity()
{
return $this->originalSpecificity;
}
/**
* Is this property important?
*
* @return bool
*/
public function isImportant()
{
return (stripos($this->value, '!important') !== false);
}
/**
* Get the textual representation of the property
*
* @return string
*/
public function toString()
{
return sprintf(
'%1$s: %2$s;',
$this->name,
$this->value
);
}
}

View File

@@ -0,0 +1,240 @@
<?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;
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_encode_numericentity($html, [0x80, 0x10FFFF, 0, 0x1FFFFF], '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->offsetExists($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;
}
}