N°2435.5 Manage SwiftMailer lib using composer

This commit is contained in:
Molkobain
2019-08-13 14:09:16 +02:00
parent 3e13c9e825
commit 0985415e11
125 changed files with 19578 additions and 2595 deletions

View File

@@ -13,6 +13,7 @@
"scssphp/scssphp": "1.0.0",
"swiftmailer/swiftmailer": "5.4.9",
"pelago/emogrifier": "2.1.0",
"symfony/console": "3.4.*",
"symfony/dotenv": "3.4.*",

129
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "dee6a275ba517b372d3e8d0d3d371a1b",
"content-hash": "3009859b319b32e2248fd281abba1df4",
"packages": [
{
"name": "paragonie/random_compat",
@@ -55,6 +55,80 @@
],
"time": "2019-01-03T20:59:08+00:00"
},
{
"name": "pelago/emogrifier",
"version": "v2.1.0",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/emogrifier.git",
"reference": "40c3d4f475d44ffc7265a760d1dd0e81f579f96f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/40c3d4f475d44ffc7265a760d1dd0e81f579f96f",
"reference": "40c3d4f475d44ffc7265a760d1dd0e81f579f96f",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0",
"symfony/css-selector": "^3.4.0 || ^4.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.2.0",
"phpmd/phpmd": "^2.6.0",
"phpunit/phpunit": "^4.8.0",
"squizlabs/php_codesniffer": "^3.3.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Pelago\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "John Reeve",
"email": "jreeve@pelagodesign.com"
},
{
"name": "Cameron Brooks"
},
{
"name": "Jaime Prado"
},
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Zoli Szabó",
"email": "zoli.szabo+github@gmail.com"
},
{
"name": "Jake Hotson",
"email": "jake@qzdesign.co.uk"
}
],
"description": "Converts CSS styles into inline style attributes in your HTML code",
"homepage": "https://www.myintervals.com/emogrifier.php",
"keywords": [
"css",
"email",
"pre-processing"
],
"time": "2018-12-08T13:55:46+00:00"
},
{
"name": "psr/cache",
"version": "1.0.1",
@@ -620,6 +694,59 @@
"homepage": "https://symfony.com",
"time": "2019-07-24T14:46:41+00:00"
},
{
"name": "symfony/css-selector",
"version": "v3.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/8ca29297c29b64fb3a1a135e71cb25f67f9fdccf",
"reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"time": "2019-01-16T09:39:14+00:00"
},
{
"name": "symfony/debug",
"version": "v3.4.30",

View File

@@ -331,7 +331,6 @@ class EMail
{
if (($sMimeType === 'text/html') && ($sCustomStyles !== null))
{
require_once(APPROOT.'lib/emogrifier/Classes/Emogrifier.php');
$emogrifier = new \Pelago\Emogrifier($sBody, $sCustomStyles);
$sBody = $emogrifier->emogrify(); // Adds html/body tags if not already present
}

View File

@@ -325,6 +325,12 @@ return array(
'ParseError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/ParseError.php',
'ParseyyStackEntry' => $baseDir . '/core/oql/build/PHP/Lempar.php',
'ParseyyToken' => $baseDir . '/core/oql/build/PHP/Lempar.php',
'Pelago\\Emogrifier' => $vendorDir . '/pelago/emogrifier/src/Emogrifier.php',
'Pelago\\Emogrifier\\CssConcatenator' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/CssConcatenator.php',
'Pelago\\Emogrifier\\CssInliner' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/CssInliner.php',
'Pelago\\Emogrifier\\HtmlProcessor\\AbstractHtmlProcessor' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php',
'Pelago\\Emogrifier\\HtmlProcessor\\CssToAttributeConverter' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php',
'Pelago\\Emogrifier\\HtmlProcessor\\HtmlNormalizer' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php',
'PortalDispatcher' => $baseDir . '/application/portaldispatcher.class.inc.php',
'PortalURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php',
'PrintableDataTable' => $baseDir . '/application/datatable.class.inc.php',
@@ -839,6 +845,54 @@ return array(
'Symfony\\Component\\Console\\Terminal' => $vendorDir . '/symfony/console/Terminal.php',
'Symfony\\Component\\Console\\Tester\\ApplicationTester' => $vendorDir . '/symfony/console/Tester/ApplicationTester.php',
'Symfony\\Component\\Console\\Tester\\CommandTester' => $vendorDir . '/symfony/console/Tester/CommandTester.php',
'Symfony\\Component\\CssSelector\\CssSelectorConverter' => $vendorDir . '/symfony/css-selector/CssSelectorConverter.php',
'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/css-selector/Exception/ExceptionInterface.php',
'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => $vendorDir . '/symfony/css-selector/Exception/ExpressionErrorException.php',
'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => $vendorDir . '/symfony/css-selector/Exception/InternalErrorException.php',
'Symfony\\Component\\CssSelector\\Exception\\ParseException' => $vendorDir . '/symfony/css-selector/Exception/ParseException.php',
'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => $vendorDir . '/symfony/css-selector/Exception/SyntaxErrorException.php',
'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => $vendorDir . '/symfony/css-selector/Node/AbstractNode.php',
'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => $vendorDir . '/symfony/css-selector/Node/AttributeNode.php',
'Symfony\\Component\\CssSelector\\Node\\ClassNode' => $vendorDir . '/symfony/css-selector/Node/ClassNode.php',
'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => $vendorDir . '/symfony/css-selector/Node/CombinedSelectorNode.php',
'Symfony\\Component\\CssSelector\\Node\\ElementNode' => $vendorDir . '/symfony/css-selector/Node/ElementNode.php',
'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => $vendorDir . '/symfony/css-selector/Node/FunctionNode.php',
'Symfony\\Component\\CssSelector\\Node\\HashNode' => $vendorDir . '/symfony/css-selector/Node/HashNode.php',
'Symfony\\Component\\CssSelector\\Node\\NegationNode' => $vendorDir . '/symfony/css-selector/Node/NegationNode.php',
'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => $vendorDir . '/symfony/css-selector/Node/NodeInterface.php',
'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => $vendorDir . '/symfony/css-selector/Node/PseudoNode.php',
'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => $vendorDir . '/symfony/css-selector/Node/SelectorNode.php',
'Symfony\\Component\\CssSelector\\Node\\Specificity' => $vendorDir . '/symfony/css-selector/Node/Specificity.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/CommentHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => $vendorDir . '/symfony/css-selector/Parser/Handler/HandlerInterface.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/HashHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/NumberHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/StringHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Parser' => $vendorDir . '/symfony/css-selector/Parser/Parser.php',
'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => $vendorDir . '/symfony/css-selector/Parser/ParserInterface.php',
'Symfony\\Component\\CssSelector\\Parser\\Reader' => $vendorDir . '/symfony/css-selector/Parser/Reader.php',
'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ClassParser.php',
'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ElementParser.php',
'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php',
'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/HashParser.php',
'Symfony\\Component\\CssSelector\\Parser\\Token' => $vendorDir . '/symfony/css-selector/Parser/Token.php',
'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => $vendorDir . '/symfony/css-selector/Parser/TokenStream.php',
'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php',
'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php',
'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AbstractExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/CombinationExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => $vendorDir . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/FunctionExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/HtmlExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/NodeExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Translator' => $vendorDir . '/symfony/css-selector/XPath/Translator.php',
'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => $vendorDir . '/symfony/css-selector/XPath/TranslatorInterface.php',
'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => $vendorDir . '/symfony/css-selector/XPath/XPathExpr.php',
'Symfony\\Component\\Debug\\BufferingLogger' => $vendorDir . '/symfony/debug/BufferingLogger.php',
'Symfony\\Component\\Debug\\Debug' => $vendorDir . '/symfony/debug/Debug.php',
'Symfony\\Component\\Debug\\DebugClassLoader' => $vendorDir . '/symfony/debug/DebugClassLoader.php',

View File

@@ -23,6 +23,7 @@ return array(
'Symfony\\Component\\Dotenv\\' => array($vendorDir . '/symfony/dotenv'),
'Symfony\\Component\\DependencyInjection\\' => array($vendorDir . '/symfony/dependency-injection'),
'Symfony\\Component\\Debug\\' => array($vendorDir . '/symfony/debug'),
'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Symfony\\Component\\Config\\' => array($vendorDir . '/symfony/config'),
'Symfony\\Component\\ClassLoader\\' => array($vendorDir . '/symfony/class-loader'),
@@ -36,4 +37,5 @@ return array(
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'Pelago\\' => array($vendorDir . '/pelago/emogrifier/src'),
);

View File

@@ -39,6 +39,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'Symfony\\Component\\Dotenv\\' => 25,
'Symfony\\Component\\DependencyInjection\\' => 38,
'Symfony\\Component\\Debug\\' => 24,
'Symfony\\Component\\CssSelector\\' => 30,
'Symfony\\Component\\Console\\' => 26,
'Symfony\\Component\\Config\\' => 25,
'Symfony\\Component\\ClassLoader\\' => 30,
@@ -55,6 +56,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'Psr\\Log\\' => 8,
'Psr\\Container\\' => 14,
'Psr\\Cache\\' => 10,
'Pelago\\' => 7,
),
);
@@ -127,6 +129,10 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
array (
0 => __DIR__ . '/..' . '/symfony/debug',
),
'Symfony\\Component\\CssSelector\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/css-selector',
),
'Symfony\\Component\\Console\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/console',
@@ -179,6 +185,10 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
array (
0 => __DIR__ . '/..' . '/psr/cache/src',
),
'Pelago\\' =>
array (
0 => __DIR__ . '/..' . '/pelago/emogrifier/src',
),
);
public static $prefixesPsr0 = array (
@@ -511,6 +521,12 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'ParseError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/ParseError.php',
'ParseyyStackEntry' => __DIR__ . '/../..' . '/core/oql/build/PHP/Lempar.php',
'ParseyyToken' => __DIR__ . '/../..' . '/core/oql/build/PHP/Lempar.php',
'Pelago\\Emogrifier' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier.php',
'Pelago\\Emogrifier\\CssConcatenator' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/CssConcatenator.php',
'Pelago\\Emogrifier\\CssInliner' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/CssInliner.php',
'Pelago\\Emogrifier\\HtmlProcessor\\AbstractHtmlProcessor' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php',
'Pelago\\Emogrifier\\HtmlProcessor\\CssToAttributeConverter' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php',
'Pelago\\Emogrifier\\HtmlProcessor\\HtmlNormalizer' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php',
'PortalDispatcher' => __DIR__ . '/../..' . '/application/portaldispatcher.class.inc.php',
'PortalURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php',
'PrintableDataTable' => __DIR__ . '/../..' . '/application/datatable.class.inc.php',
@@ -1025,6 +1041,54 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'Symfony\\Component\\Console\\Terminal' => __DIR__ . '/..' . '/symfony/console/Terminal.php',
'Symfony\\Component\\Console\\Tester\\ApplicationTester' => __DIR__ . '/..' . '/symfony/console/Tester/ApplicationTester.php',
'Symfony\\Component\\Console\\Tester\\CommandTester' => __DIR__ . '/..' . '/symfony/console/Tester/CommandTester.php',
'Symfony\\Component\\CssSelector\\CssSelectorConverter' => __DIR__ . '/..' . '/symfony/css-selector/CssSelectorConverter.php',
'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExceptionInterface.php',
'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExpressionErrorException.php',
'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/InternalErrorException.php',
'Symfony\\Component\\CssSelector\\Exception\\ParseException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ParseException.php',
'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/SyntaxErrorException.php',
'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AbstractNode.php',
'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AttributeNode.php',
'Symfony\\Component\\CssSelector\\Node\\ClassNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ClassNode.php',
'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/CombinedSelectorNode.php',
'Symfony\\Component\\CssSelector\\Node\\ElementNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ElementNode.php',
'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/FunctionNode.php',
'Symfony\\Component\\CssSelector\\Node\\HashNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/HashNode.php',
'Symfony\\Component\\CssSelector\\Node\\NegationNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/NegationNode.php',
'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => __DIR__ . '/..' . '/symfony/css-selector/Node/NodeInterface.php',
'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/PseudoNode.php',
'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/SelectorNode.php',
'Symfony\\Component\\CssSelector\\Node\\Specificity' => __DIR__ . '/..' . '/symfony/css-selector/Node/Specificity.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/CommentHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HandlerInterface.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HashHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/NumberHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/StringHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php',
'Symfony\\Component\\CssSelector\\Parser\\Parser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Parser.php',
'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/ParserInterface.php',
'Symfony\\Component\\CssSelector\\Parser\\Reader' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Reader.php',
'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ClassParser.php',
'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ElementParser.php',
'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php',
'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/HashParser.php',
'Symfony\\Component\\CssSelector\\Parser\\Token' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Token.php',
'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => __DIR__ . '/..' . '/symfony/css-selector/Parser/TokenStream.php',
'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php',
'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php',
'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AbstractExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/CombinationExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/FunctionExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/HtmlExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/NodeExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php',
'Symfony\\Component\\CssSelector\\XPath\\Translator' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Translator.php',
'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/TranslatorInterface.php',
'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => __DIR__ . '/..' . '/symfony/css-selector/XPath/XPathExpr.php',
'Symfony\\Component\\Debug\\BufferingLogger' => __DIR__ . '/..' . '/symfony/debug/BufferingLogger.php',
'Symfony\\Component\\Debug\\Debug' => __DIR__ . '/..' . '/symfony/debug/Debug.php',
'Symfony\\Component\\Debug\\DebugClassLoader' => __DIR__ . '/..' . '/symfony/debug/DebugClassLoader.php',

View File

@@ -50,6 +50,82 @@
"random"
]
},
{
"name": "pelago/emogrifier",
"version": "v2.1.0",
"version_normalized": "2.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/MyIntervals/emogrifier.git",
"reference": "40c3d4f475d44ffc7265a760d1dd0e81f579f96f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/40c3d4f475d44ffc7265a760d1dd0e81f579f96f",
"reference": "40c3d4f475d44ffc7265a760d1dd0e81f579f96f",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0",
"symfony/css-selector": "^3.4.0 || ^4.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.2.0",
"phpmd/phpmd": "^2.6.0",
"phpunit/phpunit": "^4.8.0",
"squizlabs/php_codesniffer": "^3.3.2"
},
"time": "2018-12-08T13:55:46+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Pelago\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "John Reeve",
"email": "jreeve@pelagodesign.com"
},
{
"name": "Cameron Brooks"
},
{
"name": "Jaime Prado"
},
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Zoli Szabó",
"email": "zoli.szabo+github@gmail.com"
},
{
"name": "Jake Hotson",
"email": "jake@qzdesign.co.uk"
}
],
"description": "Converts CSS styles into inline style attributes in your HTML code",
"homepage": "https://www.myintervals.com/emogrifier.php",
"keywords": [
"css",
"email",
"pre-processing"
]
},
{
"name": "psr/cache",
"version": "1.0.1",
@@ -635,6 +711,61 @@
"description": "Symfony Console Component",
"homepage": "https://symfony.com"
},
{
"name": "symfony/css-selector",
"version": "v3.4.30",
"version_normalized": "3.4.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/8ca29297c29b64fb3a1a135e71cb25f67f9fdccf",
"reference": "8ca29297c29b64fb3a1a135e71cb25f67f9fdccf",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
},
"time": "2019-01-16T09:39:14+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com"
},
{
"name": "symfony/debug",
"version": "v3.4.30",

View File

@@ -1,31 +0,0 @@
sudo: false
language: php
cache:
directories:
- vendor
env:
global:
secure: nOIIWvxRsDlkg+5H21dmVeqvFbweOAk3l3ZiyZO1m5XuGuuZR9yj10oOudee8m0hzJ7e9eoZ+dfB3t8lmK0fTRTB6w0G7RuGiQb89ief3Zhs1vOveYOgS5yfTMRym57iluxsLeCe7AxWmy7+0fWAvx1qL7bKp+THGK9yv/aj9eM=
php:
- 5.4
- 5.5
- 5.6
- 7.0
- hhvm
before_script:
- composer install
- vendor/bin/phpcs --config-set encoding utf-8
- if [ "$GITHUB_COMPOSER_AUTH" ]; then composer config -g github-oauth.github.com $GITHUB_COMPOSER_AUTH; fi
script:
# Run PHP lint on all PHP files.
- find Classes/ Tests/ -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l
# Check the coding style.
- vendor/bin/phpcs --standard=Configuration/PhpCodeSniffer/Standards/Emogrifier/ Classes/ Tests/
# Run the unit tests.
- vendor/bin/phpunit Tests/

View File

@@ -1,92 +0,0 @@
# Emogrifier Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
Emogrifier is in a pre-1.0 state. This means that its APIs and behavior are
subject to breaking changes without deprecation notices.
## [1.0.0][] (2015-10-15)
### Added
- Add branch alias ([#231](https://github.com/jjriv/emogrifier/pull/231))
- Remove media queries which do not impact the document
([#217](https://github.com/jjriv/emogrifier/pull/217))
- Allow elements to be excluded from emogrification
([#215](https://github.com/jjriv/emogrifier/pull/215))
- Handle !important ([#214](https://github.com/jjriv/emogrifier/pull/214))
- emogrifyBodyContent() method
([#206](https://github.com/jjriv/emogrifier/pull/206))
- Cache combinedStyles ([#211](https://github.com/jjriv/emogrifier/pull/211))
- Allow user to define media types to keep
([#200](https://github.com/jjriv/emogrifier/pull/200))
- Ignore invalid CSS selectors
([#194](https://github.com/jjriv/emogrifier/pull/194))
- isRemoveDisplayNoneEnabled option
([#162](https://github.com/jjriv/emogrifier/pull/162))
- Allow disabling of "inline style" and "style block" parsing
([#156](https://github.com/jjriv/emogrifier/pull/156))
- Preserve @media if necessary
([#62](https://github.com/jjriv/emogrifier/pull/62))
- Add extraction of style blocks within the HTML
- Add several new pseudo-selectors (first-child, last-child, nth-child,
and nth-of-type)
### Changed
- Make HTML5 the default document type
([#245](https://github.com/jjriv/emogrifier/pull/245))
- Make copyCssWithMediaToStyleNode private
([#218](https://github.com/jjriv/emogrifier/pull/218))
- Stop encoding umlauts and dollar signs
([#170](https://github.com/jjriv/emogrifier/pull/170))
- Convert the classes to namespaces
([#41](https://github.com/jjriv/emogrifier/pull/41))
### Deprecated
- Support for PHP 5.4 will be removed in Emogrifier 2.0.
### Removed
- Drop support for PHP 5.3
([#114](https://github.com/jjriv/emogrifier/pull/114))
- Support for character sets other than UTF-8 was removed.
### Fixed
- Fix failing tests on Windows due to line endings
([#263](https://github.com/jjriv/emogrifier/pull/263))
- Parsing CSS declaration blocks
([#261](https://github.com/jjriv/emogrifier/pull/261))
- Fix first-child and last-child selectors
([#257](https://github.com/jjriv/emogrifier/pull/257))
- Fix parsing of CSS for data URIs
([#243](https://github.com/jjriv/emogrifier/pull/243))
- Fix multi-line media queries
([#241](https://github.com/jjriv/emogrifier/pull/241))
- Keep CSS media queries even if followed by CSS comments
([#201](https://github.com/jjriv/emogrifier/pull/201))
- Fix CSS selectors with exact attribute only
([#197](https://github.com/jjriv/emogrifier/pull/197))
- Properly handle UTF-8 characters and entities
([#189](https://github.com/jjriv/emogrifier/pull/189))
- Add mbstring extension to composer.json
([#93](https://github.com/jjriv/emogrifier/pull/93))
- Prevent incorrectly capitalized CSS selectors from being stripped
([#85](https://github.com/jjriv/emogrifier/pull/85))
- Fix CSS selectors with exact attribute only
([#197](https://github.com/jjriv/emogrifier/pull/197))
- Wrong selector extraction from minified CSS
([#69](https://github.com/jjriv/emogrifier/pull/69))
- Restore libxml error handler state after clearing
([#65](https://github.com/jjriv/emogrifier/pull/65))
- Ignore all warnings produced by DOMDocument::loadHTML()
([#63](https://github.com/jjriv/emogrifier/pull/63))
- Style tags in HTML cause an Xpath invalid query error
([#60](https://github.com/jjriv/emogrifier/pull/60))
- Fix PHP warnings with PHP 5.5
([#26](https://github.com/jjriv/emogrifier/pull/26))
- Make removal of invisible nodes operate in a case-insensitive manner
- Fix a bug that was overwriting existing inline styles from the original HTML

View File

@@ -1,78 +0,0 @@
# Contributing to Emogrifier
Those that wish to contribute bug fixes, new features, refactorings and
clean-up to Emogrifier are more than welcome.
When you contribute, please take the following things into account:
## General workflow
After you have submitted a pull request, the Emogrifier team will review your
changes. This will probably result in quite a few comments on ways to improve
your pull request. The Emogrifier project receives contributions from
developers around the world, so we need the code to be the most consistent,
readable, and maintainable that it can be.
Please do not feel frustrated by this - instead please view this both as our
contribution to your pull request as well as a way to learn more about
improving code quality.
If you would like to know whether an idea would fit in the general strategy of
the Emogrifier project or would like to get feedback on the best architecture
for your ideas, we propose you open a ticket first and discuss your ideas there
first before investing a lot of time in writing code.
## Install the development dependencies
To install the development dependencies (PHPUnit and PHP_CodeSniffer), please
run the following command:
composer install
## Unit-test your changes
Please cover all changes with unit tests and make sure that your code does not
break any existing tests. We will only merge pull request that include full
code coverage of the fixed bugs and the new features.
To run the existing PHPUnit tests, run this command:
vendor/bin/phpunit Tests/
## Coding Style
Please use the same coding style (PSR-2) as the rest of the code. Indentation
is four spaces.
We will only merge pull requests that follow the project's coding style.
Please check your code with the provided PHP_CodeSniffer standard:
vendor/bin/phpcs --standard=Configuration/PhpCodeSniffer/Standards/Emogrifier/ Classes/ Tests/
Please make your code clean, well-readable and easy to understand.
If you add new methods or fields, please add proper PHPDoc for the new
methods/fields. Please use grammatically correct, complete sentences in the
code documentation.
## Git commits
Git commits should have a <= 50 character summary, optionally followed by a
blank line and a more in depth description of 79 characters per line.
[Please squash related commits together](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html).
If you already have a commit and work on it, you can also
[amend the first commit](https://nathanhoad.net/git-amend-your-last-commit).
Please use grammatically correct, complete sentences in the commit messages.
Also, please prefix the subject line of the commit message with either
[FEATURE], [TASK], [BUGFIX] OR [CLEANUP]. This makes it faster to see what
a commit is about.

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
Emogrifier is copyright (c) 2008-2014 Pelago and licensed under the MIT license.
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
{
"name": "pelago/emogrifier",
"description": "Converts CSS styles into inline style attributes in your HTML code",
"tags": ["email", "css", "pre-processing"],
"license": "MIT",
"homepage": "http://www.pelagodesign.com/sidecar/emogrifier/",
"authors": [
{
"name": "John Reeve",
"email": "jreeve@pelagodesign.com"
},
{
"name": "Cameron Brooks"
},
{
"name": "Jaime Prado"
},
{
"name": "Oliver Klee",
"email": "typo3-coding@oliverklee.de"
},
{
"name": "Roman Ožana",
"email": "ozana@omdesign.cz"
}
],
"require": {
"php": ">=5.4.0",
"ext-mbstring": "*"
},
"require-dev": {
"squizlabs/php_codesniffer": "2.3.4",
"typo3-ci/typo3sniffpool": "2.1.1",
"phpunit/phpunit": "4.8.11"
},
"autoload": {
"psr-4": {
"Pelago\\": "Classes/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
}
}

View File

@@ -1,5 +0,0 @@
#
# The following source files are not re-distributed with the "build" of the application
# since they are used solely for constructing other files during the build process
#
Test

View File

@@ -0,0 +1,129 @@
# Contributing to Emogrifier
Those that wish to contribute bug fixes, new features, refactorings and
clean-up to Emogrifier are more than welcome.
When you contribute, please take the following things into account:
## Contributor Code of Conduct
Please note that this project is released with a
[Contributor Code of Conduct](../CODE_OF_CONDUCT.md). By participating in this
project, you agree to abide by its terms.
## General workflow
This is the workflow for contributing changes to Emogrifier:
1. [Fork the Emogrifier Git repository](https://guides.github.com/activities/forking/).
2. Clone your forked repository and
[install the development dependencies](#install-the-development-dependencies).
3. Add a local remote "upstream" so you will be able to
[synchronize your fork with the original Emogrifier repository](https://help.github.com/articles/syncing-a-fork/).
4. Create a local branch for your changes.
5. [Add unit tests for your changes](#unit-test-your-changes).
These tests should fail without your changes.
6. Add your changes. Your added unit tests now should pass, and no other tests
should be broken. Check that your changes follow the same
[coding style](#coding-style) as the rest of the project.
7. Add a changelog entry.
8. [Commit](#git-commits) and push your changes.
9. [Create a pull request](https://help.github.com/articles/about-pull-requests/)
for your changes. Check that the Travis build is green. (If it is not, fix the
problems listed by Travis.)
10. [Request a review](https://help.github.com/articles/about-pull-request-reviews/)
from @oliverklee.
11. Together with him, polish your changes until they are ready to be merged.
## About code reviews
After you have submitted a pull request, the Emogrifier team will review your
changes. This will probably result in quite a few comments on ways to improve
your pull request. The Emogrifier project receives contributions from
developers around the world, so we need the code to be the most consistent,
readable, and maintainable that it can be.
Please do not feel frustrated by this - instead please view this both as our
contribution to your pull request as well as a way to learn more about
improving code quality.
If you would like to know whether an idea would fit in the general strategy of
the Emogrifier project or would like to get feedback on the best architecture
for your ideas, we propose you open a ticket first and discuss your ideas there
first before investing a lot of time in writing code.
## Install the development dependencies
To install the development dependencies (PHPUnit and PHP_CodeSniffer), please
run the following commands:
```shell
composer install
composer require --dev slevomat/coding-standard:^4.0
```
Note that the development dependencies (in particular, for PHP_CodeSniffer)
require PHP 7.0 or later. The second command installs the PHP_CodeSniffer
dependencies and should be omitted if specifically testing against an earlier
version of PHP, however you will not be able to run the static code analysis.
## Unit-test your changes
Please cover all changes with unit tests and make sure that your code does not
break any existing tests. We will only merge pull requests that include full
code coverage of the fixed bugs and the new features.
To run the existing PHPUnit tests, run this command:
```shell
composer ci:tests:unit
```
## Coding Style
Please use the same coding style (PSR-2) as the rest of the code. Indentation
is four spaces.
We will only merge pull requests that follow the project's coding style.
Please check your code with the provided static code analysis tools:
```shell
composer ci:static
```
Please make your code clean, well-readable and easy to understand.
If you add new methods or fields, please add proper PHPDoc for the new
methods/fields. Please use grammatically correct, complete sentences in the
code documentation.
You can autoformat your code using the following command:
```shell
composer php:fix
```
## Git commits
Commit message should have a <= 50 character summary, optionally followed by a
blank line and a more in depth description of 79 characters per line.
Please use grammatically correct, complete sentences in the commit messages.
Also, please prefix the subject line of the commit message with either
[FEATURE], [TASK], [BUGFIX] OR [CLEANUP]. This makes it faster to see what
a commit is about.
## Creating pull requests (PRs)
When you create a pull request, please
[make your PR editable](https://github.com/blog/2247-improving-collaboration-with-forks).

View File

@@ -20,5 +20,6 @@
.TemporaryItems
.webprj
nbproject
/.php_cs.cache
/vendor/
composer.lock

View File

@@ -0,0 +1,65 @@
sudo: false
language: php
php:
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- 7.3
cache:
directories:
- vendor
- $HOME/.composer/cache
env:
matrix:
- DEPENDENCIES_PREFERENCE="--prefer-lowest"
- DEPENDENCIES_PREFERENCE=""
before_install:
- phpenv config-rm xdebug.ini || echo "xdebug not available"
install:
- >
export IGNORE_PLATFORM_REQS="$(composer php:version |grep -q '^7.3' && printf -- --ignore-platform-reqs)";
echo;
echo "Updating the dependencies";
composer update $IGNORE_PLATFORM_REQS --with-dependencies $DEPENDENCIES_PREFERENCE;
composer show;
script:
- >
echo;
echo "Validating the composer.json";
composer validate --no-check-all --no-check-lock --strict;
- >
echo;
echo "Linting all PHP files";
composer ci:php:lint;
- >
echo;
echo "Running the unit tests";
composer ci:tests:unit;
- >
echo;
echo "Running PHPMD";
composer ci:php:md;
- >
echo;
function version_gte() { test "$(printf '%s\n' "$@" | sort -n -t. -r | head -n 1)" = "$1"; };
if version_gte $(composer php:version) 7; then
echo "Installing slevomat/coding-standard only for PHP 7.x";
composer require $IGNORE_PLATFORM_REQS --dev slevomat/coding-standard:^4.0 $DEPENDENCIES_PREFERENCE;
echo "Running PHP_CodeSniffer";
composer ci:php:sniff;
else
echo "Skipped PHP_CodeSniffer due to insufficient PHP version: $(composer php:version)";
fi;

View File

@@ -0,0 +1,312 @@
# Emogrifier Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).
## x.y.z
### Added
### Changed
### Deprecated
### Removed
### Fixed
## 2.1.0
### Added
- PHP 7.3 support
([#638](https://github.com/MyIntervals/emogrifier/pull/638))
- Allow PHP 7.3 in `composer.json`
- Test in Travis for PHP 7.3
- Add a `renderBodyContent()` method
([#633](https://github.com/MyIntervals/emogrifier/pull/633))
- Add a `getDomDocument()` method
([#630](https://github.com/MyIntervals/emogrifier/pull/630))
- Add a Composer script for PHP CS Fixer
([#607](https://github.com/MyIntervals/emogrifier/pull/607))
- Copy matching rules with dynamic pseudo-classes or pseudo-elements in
selectors to the style element
([#280](https://github.com/MyIntervals/emogrifier/issues/280),
[#562](https://github.com/MyIntervals/emogrifier/pull/562),
[#567](https://github.com/MyIntervals/emogrifier/pull/567))
- Add a CssToAttributeConverter
([#546](https://github.com/jjriv/emogrifier/pull/546))
- Expose the DOMDocument in AbstractHtmlProcessor
([#520](https://github.com/jjriv/emogrifier/pull/520))
- Add an HtmlNormalizer class
([#513](https://github.com/jjriv/emogrifier/pull/513),
[#516](https://github.com/jjriv/emogrifier/pull/516))
- Add a CssInliner class
([#514](https://github.com/jjriv/emogrifier/pull/514),
[#522](https://github.com/jjriv/emogrifier/pull/522))
- Composer scripts for the various CI build steps
- Validate the composer.json on Travis
([#476](https://github.com/jjriv/emogrifier/pull/476))
### Changed
- Mark the work-in-progress classes as `@internal`
([#640](https://github.com/MyIntervals/emogrifier/pull/640))
- Remove the unprocessable tags from the DOM, not from the raw HTML
([#627](https://github.com/MyIntervals/emogrifier/pull/627))
- Reject empty HTML in `setHtml()`
([#622](https://github.com/MyIntervals/emogrifier/pull/622))
- Stop passing the DOM document around
([#618](https://github.com/MyIntervals/emogrifier/pull/618))
- Improve performance by using explicit namespaces for PHP functions
([#573](https://github.com/MyIntervals/emogrifier/pull/573),
[#576](https://github.com/MyIntervals/emogrifier/pull/576))
- Add type hint checking to the code sniffs
([#566](https://github.com/MyIntervals/emogrifier/pull/566))
- Check the code with PHPMD
([#561](https://github.com/jjriv/emogrifier/pull/561))
- Add the cyclomatic complexity to the checked code sniffs
([#558](https://github.com/jjriv/emogrifier/pull/558))
- Use the Symfony CSS selector component
([#540](https://github.com/jjriv/emogrifier/pull/540))
### Deprecated
- Support for PHP 5.5 will be removed in Emogrifier 3.0.
- Support for PHP 5.6 will be removed in Emogrifier 4.0.
- The removal of invisible nodes will be removed in Emogrifier 3.0.
([#473](https://github.com/jjriv/emogrifier/pull/473))
- Converting CSS styles to (non-CSS) HTML attributes will be removed
in Emogrifier 3.0. Please use the new CssToAttributeConverter instead.
([#474](https://github.com/jjriv/emogrifier/pull/474))
- Emogrifier 3.x.y will be the last release that supports usage without
Composer (i.e., you can still require the class file).
Starting with version 4.0, Emogrifier will only work with Composer.
- The Emogrifier class will be superseded by CssInliner class in
Emogrifier 3.0. For this, the Emogrifier class will be deprecated for
version 3.0 and removed for version 4.0.
### Removed
- Drop the `@version` PHPDoc annotations
([#637](https://github.com/MyIntervals/emogrifier/pull/637))
- Drop the destructors
([#619](https://github.com/MyIntervals/emogrifier/pull/619))
### Fixed
- Add required XML PHP extension to `composer.json`
([#614](https://github.com/MyIntervals/emogrifier/pull/614))
- Add required DOM PHP extension to `composer.json`
([#595](https://github.com/MyIntervals/emogrifier/pull/595))
- Escape hyphens in regular expressions
([#588](https://github.com/MyIntervals/emogrifier/pull/588))
- Fix Travis for PHP 5.x
([#589](https://github.com/MyIntervals/emogrifier/pull/589))
- Allow CSS between empty `@media` rule and another `@media` rule
([#534](https://github.com/MyIntervals/emogrifier/pull/534))
- Allow additional whitespace in media-query-list of disallowed `@media` rules
([#532](https://github.com/MyIntervals/emogrifier/pull/532))
- Allow multiple minified `@import` rules in the CSS without error (note:
`@import`s are currently ignored,
[#527](https://github.com/MyIntervals/emogrifier/pull/527))
- Style property ordering when multiple mixed individual and shorthand
properties apply ([#511](https://github.com/MyIntervals/emogrifier/pull/511),
[#508](https://github.com/MyIntervals/emogrifier/issues/508))
- Calculation of selector precedence for selectors involving pseudo-classes
and/or attributes ([#502](https://github.com/MyIntervals/emogrifier/pull/502))
- Allow `@charset` in the CSS without error (note: its value is currently
ignored, [#507](https://github.com/MyIntervals/emogrifier/pull/507))
- Allow attribute selectors in descendants
([#506](https://github.com/MyIntervals/emogrifier/pull/506),
[#381](https://github.com/MyIntervals/emogrifier/issues/381))
- Allow adjacent sibling CSS selector combinator in minified CSS
([#505](https://github.com/MyIntervals/emogrifier/pull/505))
- Allow CSS property values containing newlines
([#504](https://github.com/MyIntervals/emogrifier/pull/504))
## 2.0.0
### Added
- Support for CSS :not() selector
([#431](https://github.com/jjriv/emogrifier/pull/431))
- Automatically remove !important annotations from final inline style declarations
([#420](https://github.com/MyIntervals/emogrifier/pull/420))
- Automatically move `<style>` block from `<head>` to `<body>`
([#396](https://github.com/MyIntervals/emogrifier/pull/396))
- PHP 7.2 support ([#398](https://github.com/MyIntervals/emogrifier/pull/398))
- Allow PHP 7.2 in `composer.json`, cleaner PHP version constraint
- Test in Travis for PHP 7.2
- Debug mode. Throw debug exceptions only if debug is active.
([#392](https://github.com/MyIntervals/emogrifier/pull/392))
### Changed
- Test with latest and oldest dependencies on Travis
([#463](https://github.com/MyIntervals/emogrifier/pull/463))
- Always enable the debug mode in the tests
([#448](https://github.com/MyIntervals/emogrifier/pull/448))
- Optimize the string operations
([#430](https://github.com/MyIntervals/emogrifier/pull/430))
### Deprecated
- Support for PHP 5.5 will be removed in Emogrifier 3.0.
- Support for PHP 5.6 will be removed in Emogrifier 4.0.
### Removed
- Drop support for PHP 5.4
([#422](https://github.com/MyIntervals/emogrifier/pull/422))
- Drop support for HHVM
([#386](https://github.com/MyIntervals/emogrifier/pull/386))
### Fixed
- Handle invalid/unrecognized selectors in media query blocks
([#442](https://github.com/MyIntervals/emogrifier/pull/442))
- Throw (the correct) exception for invalid excluded selectors
([#437](https://github.com/MyIntervals/emogrifier/pull/437))
- emogrifyBody must not encode umlaut entities
([#414](https://github.com/MyIntervals/emogrifier/pull/414))
- Fix mapped HTML attribute values
([#405](https://github.com/MyIntervals/emogrifier/pull/405))
- Make sure the HTML always has a BODY element
([#410](https://github.com/MyIntervals/emogrifier/pull/410))
- Make inline style priority higher than css block priority
([#404](https://github.com/MyIntervals/emogrifier/pull/404))
- Fix media regex parsing
([#402](https://github.com/MyIntervals/emogrifier/pull/402))
- Silence purposefully ignored PHP Warnings
([#400](https://github.com/MyIntervals/emogrifier/pull/400))
## 1.2.0 (2017-03-02)
### Added
- Handling invalid xPath expression warnings
([#361](https://github.com/MyIntervals/emogrifier/pull/361))
### Deprecated
- Support for PHP 5.5 will be removed in Emogrifier 3.0.
- Support for PHP 5.4 will be removed in Emogrifier 2.0.
### Fixed
- Allow colon (`:`) and semi-colon (`;`) when using the `*=` selector
([#371](https://github.com/MyIntervals/emogrifier/pull/371))
- Ignore "auto" width and height
([#365](https://github.com/MyIntervals/emogrifier/pull/365))
## 1.1.0 (2016-09-18)
### Added
- Add support for PHP 7.1
([#342](https://github.com/MyIntervals/emogrifier/pull/342))
- Support the attr|=value selector
([#337](https://github.com/MyIntervals/emogrifier/pull/337))
- Support the attr*=value selector
([#330](https://github.com/MyIntervals/emogrifier/pull/330))
- Support the attr$=value selector
([#329](https://github.com/MyIntervals/emogrifier/pull/329))
- Support the attr^=value selector
([#324](https://github.com/MyIntervals/emogrifier/pull/324))
- Support the attr~=value selector
([#323](https://github.com/MyIntervals/emogrifier/pull/323))
- Add CSS to HTML attribute mapper
([#288](https://github.com/MyIntervals/emogrifier/pull/288))
### Changed
- Remove composer dependency from PHP mbstring extension
(Actual code dependency were removed a lot of time ago)
([#295](https://github.com/MyIntervals/emogrifier/pull/295))
### Deprecated
- Support for PHP 5.5 will be removed in Emogrifier 3.0.
- Support for PHP 5.4 will be removed in Emogrifier 2.0.
### Fixed
- Method emogrifyBodyContent() doesn't keeps utf8 umlauts
([#349](https://github.com/MyIntervals/emogrifier/pull/349))
- Ignore value with words more than one in the attribute selector
([#327](https://github.com/MyIntervals/emogrifier/pull/327))
- Ignore spaces around the > in the direct child selector
([#322](https://github.com/MyIntervals/emogrifier/pull/322))
- Ignore empty media queries
([#307](https://github.com/MyIntervals/emogrifier/pull/307))
([#237](https://github.com/MyIntervals/emogrifier/issues/237))
- Ignore pseudo-class when combined with pseudo-element
([#308](https://github.com/MyIntervals/emogrifier/pull/308))
- First-child and last-child selectors are broken
([#293](https://github.com/MyIntervals/emogrifier/pull/293))
- Second !important rule needs to overwrite the first one
([#292](https://github.com/MyIntervals/emogrifier/pull/292))
## 1.0.0 (2015-10-15)
### Added
- Add branch alias ([#231](https://github.com/MyIntervals/emogrifier/pull/231))
- Remove media queries which do not impact the document
([#217](https://github.com/MyIntervals/emogrifier/pull/217))
- Allow elements to be excluded from emogrification
([#215](https://github.com/MyIntervals/emogrifier/pull/215))
- Handle !important ([#214](https://github.com/MyIntervals/emogrifier/pull/214))
- emogrifyBodyContent() method
([#206](https://github.com/MyIntervals/emogrifier/pull/206))
- Cache combinedStyles ([#211](https://github.com/MyIntervals/emogrifier/pull/211))
- Allow user to define media types to keep
([#200](https://github.com/MyIntervals/emogrifier/pull/200))
- Ignore invalid CSS selectors
([#194](https://github.com/MyIntervals/emogrifier/pull/194))
- isRemoveDisplayNoneEnabled option
([#162](https://github.com/MyIntervals/emogrifier/pull/162))
- Allow disabling of "inline style" and "style block" parsing
([#156](https://github.com/MyIntervals/emogrifier/pull/156))
- Preserve @media if necessary
([#62](https://github.com/MyIntervals/emogrifier/pull/62))
- Add extraction of style blocks within the HTML
- Add several new pseudo-selectors (first-child, last-child, nth-child,
and nth-of-type)
### Changed
- Make HTML5 the default document type
([#245](https://github.com/MyIntervals/emogrifier/pull/245))
- Make copyCssWithMediaToStyleNode private
([#218](https://github.com/MyIntervals/emogrifier/pull/218))
- Stop encoding umlauts and dollar signs
([#170](https://github.com/MyIntervals/emogrifier/pull/170))
- Convert the classes to namespaces
([#41](https://github.com/MyIntervals/emogrifier/pull/41))
### Deprecated
- Support for PHP 5.4 will be removed in Emogrifier 2.0.
### Removed
- Drop support for PHP 5.3
([#114](https://github.com/MyIntervals/emogrifier/pull/114))
- Support for character sets other than UTF-8 was removed.
### Fixed
- Fix failing tests on Windows due to line endings
([#263](https://github.com/MyIntervals/emogrifier/pull/263))
- Parsing CSS declaration blocks
([#261](https://github.com/MyIntervals/emogrifier/pull/261))
- Fix first-child and last-child selectors
([#257](https://github.com/MyIntervals/emogrifier/pull/257))
- Fix parsing of CSS for data URIs
([#243](https://github.com/MyIntervals/emogrifier/pull/243))
- Fix multi-line media queries
([#241](https://github.com/MyIntervals/emogrifier/pull/241))
- Keep CSS media queries even if followed by CSS comments
([#201](https://github.com/MyIntervals/emogrifier/pull/201))
- Fix CSS selectors with exact attribute only
([#197](https://github.com/MyIntervals/emogrifier/pull/197))
- Properly handle UTF-8 characters and entities
([#189](https://github.com/MyIntervals/emogrifier/pull/189))
- Add mbstring extension to composer.json
([#93](https://github.com/MyIntervals/emogrifier/pull/93))
- Prevent incorrectly capitalized CSS selectors from being stripped
([#85](https://github.com/MyIntervals/emogrifier/pull/85))
- Fix CSS selectors with exact attribute only
([#197](https://github.com/MyIntervals/emogrifier/pull/197))
- Wrong selector extraction from minified CSS
([#69](https://github.com/MyIntervals/emogrifier/pull/69))
- Restore libxml error handler state after clearing
([#65](https://github.com/MyIntervals/emogrifier/pull/65))
- Ignore all warnings produced by DOMDocument::loadHTML()
([#63](https://github.com/MyIntervals/emogrifier/pull/63))
- Style tags in HTML cause an Xpath invalid query error
([#60](https://github.com/MyIntervals/emogrifier/pull/60))
- Fix PHP warnings with PHP 5.5
([#26](https://github.com/MyIntervals/emogrifier/pull/26))
- Make removal of invisible nodes operate in a case-insensitive manner
- Fix a bug that was overwriting existing inline styles from the original HTML

View File

@@ -0,0 +1,77 @@
# Contributor Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age,
body size, disability, ethnicity, gender identity and expression, level of
experience, nationality, personal appearance, race, religion, or sexual
identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an
appointed representative at an online or offline event. Representation of a
project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at (emogrifier at myintervals dot com).
All complaints will be reviewed and investigated and will result in a response
that is deemed necessary and appropriate to the circumstances. The project team
is obligated to maintain confidentiality with regard to the reporter of an
incident. Further details of specific enforcement policies may be posted
separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 1.4, available at
[http://contributor-covenant.org/version/1/4/][version].
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2008-2018 Pelago
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +1,6 @@
# Emogrifier
[![Build Status](https://travis-ci.org/jjriv/emogrifier.svg?branch=master)](https://travis-ci.org/jjriv/emogrifier)
[![Build Status](https://travis-ci.org/MyIntervals/emogrifier.svg?branch=master)](https://travis-ci.org/MyIntervals/emogrifier)
[![Latest Stable Version](https://poser.pugx.org/pelago/emogrifier/v/stable.svg)](https://packagist.org/packages/pelago/emogrifier)
[![Total Downloads](https://poser.pugx.org/pelago/emogrifier/downloads.svg)](https://packagist.org/packages/pelago/emogrifier)
[![Latest Unstable Version](https://poser.pugx.org/pelago/emogrifier/v/unstable.svg)](https://packagist.org/packages/pelago/emogrifier)
@@ -28,14 +28,14 @@ in `<link>` elements. Emogrifier solves this problem by converting CSS styles
into inline style attributes in your HTML code.
- [How it works](#how-it-works)
- [Installation](#installation)
- [Usage](#usage)
- [Options](#options)
- [Installing with Composer](#installing-with-composer)
- [Usage](#usage)
- [Supported CSS selectors](#supported-css-selectors)
- [Caveats](#caveats)
- [Maintainer](#maintainer)
- [Contributing](#contributing)
- [Processing HTML](#processing-html)
- [Maintainers](#maintainers)
## How it Works
@@ -43,30 +43,44 @@ Emogrifier automagically transmogrifies your HTML by parsing your CSS and
inserting your CSS definitions into tags within your HTML based on your CSS
selectors.
## Installation
For installing emogrifier, either add pelago/emogrifier to your
project's composer.json, or you can use composer as below:
```bash
composer require pelago/emogrifier
```
## Usage
First, you provide Emogrifier with the HTML and CSS you would like to merge.
This can happen directly during instantiation:
$html = '<html><h1>Hello world!</h1></html>';
$css = 'h1 {font-size: 32px;}';
$emogrifier = new \Pelago\Emogrifier($html, $css);
```php
$html = '<html><h1>Hello world!</h1></html>';
$css = 'h1 {font-size: 32px;}';
$emogrifier = new \Pelago\Emogrifier($html, $css);
```
You could also use the setters for providing this data after instantiation:
$emogrifier = new \Pelago\Emogrifier();
```php
$emogrifier = new \Pelago\Emogrifier();
$html = '<html><h1>Hello world!</h1></html>';
$css = 'h1 {font-size: 32px;}';
$html = '<html><h1>Hello world!</h1></html>';
$css = 'h1 {font-size: 32px;}';
$emogrifier->setHtml($html);
$emogrifier->setCss($css);
$emogrifier->setHtml($html);
$emogrifier->setCss($css);
```
After you have set the HTML and CSS, you can call the `emogrify` method to
merge both:
$mergedHtml = $emogrifier->emogrify();
```php
$mergedHtml = $emogrifier->emogrify();
```
Emogrifier automatically adds a Content-Type meta tag to set the charset for
the document (if it is not provided).
@@ -74,8 +88,9 @@ the document (if it is not provided).
If you would like to get back only the content of the BODY element instead of
the complete HTML document, you can use the `emogrifyBodyContent` instead:
$bodyContent = $emogrifier->emogrifyBodyContent();
```php
$bodyContent = $emogrifier->emogrifyBodyContent();
```
## Options
@@ -87,7 +102,9 @@ calling the `emogrify` method:
"style" attributes to the HTML. The `<style>` blocks will then be removed
from the HTML. If you want to disable this functionality so that Emogrifier
leaves these `<style>` blocks in the HTML and does not parse them, you should
use this option.
use this option. If you use this option, the contents of the `<style>` blocks
will _not_ be applied as inline styles and any CSS you want Emogrifier to
use must be passed in as described in the [Usage section](#usage) above.
* `$emogrifier->disableInlineStylesParsing()` - By default, Emogrifier
preserves all of the "style" attributes on tags in the HTML you pass to it.
However if you want to discard all existing inline styles in the HTML before
@@ -95,6 +112,8 @@ calling the `emogrify` method:
* `$emogrifier->disableInvisibleNodeRemoval()` - By default, Emogrifier removes
elements from the DOM that have the style attribute `display: none;`. If
you would like to keep invisible elements in the DOM, use this option.
Note: This option will be removed in Emogrifier 3.0. HTML tags with
`display: none;` then will always be retained.
* `$emogrifier->addAllowedMediaType(string $mediaName)` - By default, Emogrifier
will keep only media types `all`, `screen` and `print`. If you want to keep
some others, you can use this method to define them.
@@ -102,60 +121,78 @@ calling the `emogrify` method:
method to remove media types that Emogrifier keeps.
* `$emogrifier->addExcludedSelector(string $selector)` - Keeps elements from
being affected by emogrification.
## Requirements
* PHP from 5.4 to 7.0 (with the mbstring extension)
* or HHVM
* `$emogrifier->enableCssToHtmlMapping()` - Some email clients don't support CSS
well, even if inline and prefer HTML attributes. This function allows you to
put properties such as height, width, background color and font color in your
CSS while the transformed content will have all the available HTML
attributes set. This option will be removed in Emogrifier 3.0. Please use the
`CssToAttributeConverter` class instead.
## Installing with Composer
Download the [`composer.phar`](https://getcomposer.org/composer.phar) locally
or install [Composer](https://getcomposer.org/) globally:
curl -s https://getcomposer.org/installer | php
```bash
curl -s https://getcomposer.org/installer | php
```
Run the following command for a local installation:
php composer.phar require pelago/emogrifier:@dev
```bash
php composer.phar require pelago/emogrifier:^2.1.0
```
Or for a global installation, run the following command:
composer require pelago/emogrifier:@dev
```bash
composer require pelago/emogrifier:^2.1.0
```
You can also add follow lines to your `composer.json` and run the
`composer update` command:
"require": {
"pelago/emogrifier": "@dev"
}
```json
"require": {
"pelago/emogrifier": "^2.1.0"
}
```
See https://getcomposer.org/ for more information and documentation.
## Supported CSS selectors
Emogrifier currently support the following
[CSS selectors](http://www.w3.org/TR/CSS2/selector.html):
Emogrifier currently supports the following
[CSS selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors):
* ID
* class
* type
* descendant
* child
* adjacent
* attribute presence
* attribute value
* attribute only
* first-child
* last-child
* [type](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors)
* [class](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors)
* [ID](https://developer.mozilla.org/en-US/docs/Web/CSS/ID_selectors)
* [universal](https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors):
* [attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors):
* presence
* exact value match
* value with `~` (one word within a whitespace-separated list of words)
* value with `|` (either exact value match or prefix followed by a hyphen)
* value with `^` (prefix match)
* value with `$` (suffix match)
* value with `*` (substring match)
* [adjacent](https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_selectors)
* [child](https://developer.mozilla.org/en-US/docs/Web/CSS/Child_selectors)
* [descendant](https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_selectors)
* [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes):
* [first-child](https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child)
* [last-child](https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child)
* [not()](https://developer.mozilla.org/en-US/docs/Web/CSS/:not)
The following selectors are not implemented yet:
* universal
* [universal](https://developer.mozilla.org/en-US/docs/Web/CSS/Universal_selectors)
* [case-insensitive attribute value](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors#case-insensitive)
* [general sibling](https://developer.mozilla.org/en-US/docs/Web/CSS/General_sibling_selectors)
* [pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes)
(some of them will never be supported)
* [pseudo-elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements)
## Caveats
@@ -191,8 +228,67 @@ The following selectors are not implemented yet:
works by converting CSS selectors to XPath selectors, and pseudo selectors
cannot be converted accurately).
## Processing HTML
## Maintainer
The Emogrifier package also provides classes for (post-)processing the HTML
generated by `emogrify` (and it also works on any other HTML).
Emogrifier is maintained by the good people at
[Pelago](http://www.pelagodesign.com/), info AT pelagodesign DOT com.
### Normalizing and cleaning up HTML
The `HtmlNormalizer` class normalizes the given HTML in the following ways:
- add a document type (HTML5) if missing
- disentangle incorrectly nested tags
- add HEAD and BODY elements (if they are missing)
- reformat the HTML
The class can be used like this:
```php
$normalizer = new \Pelago\Emogrifier\HtmlProcessor\HtmlNormalizer($rawHtml);
$cleanHtml = $normalizer->render();
```
### Converting CSS styles to visual HTML attributes
The `CssToAttributeConverter` converts a few style attributes values to visual
HTML attributes. This allows to get at least a bit of visual styling for email
clients that do not support CSS well. For example, `style="width: 100px"`
will be converted to `width="100"`.
The class can be used like this:
```php
$converter = new \Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter($rawHtml);
$visualHtml = $converter->convertCssToVisualAttributes()->render();
```
### Technology preview of new classes
Currently, a refactoring effort is underway, aiming towards replacing the
grown-over-time `Emogrifier` class with the new `CssInliner` class and moving
additional HTML processing into separate `CssProcessor` classes (which will
inherit from `AbstractHtmlProcessor`). You can try the new classes, but be
aware that the APIs of the new classes still are subject to change.
## Steps to release a new version
1. Create a pull request "Prepare release of version x.y.z" with the following
changes.
1. In the [composer.json](composer.json), update the `branch-alias` entry to
point to the release _after_ the upcoming release.
1. In the [README.md](README.md), update the version numbers in the section
[Installing with Composer](#installing-with-composer).
1. In the [CHANGELOG.md](CHANGELOG.md), set the version number and remove any
empty sections.
1. Have the pull request reviewed and merged.
1. In the [Releases tab](https://github.com/MyIntervals/emogrifier/releases),
create a new release and copy the change log entries to the new release.
1. Post about the new release on social media.
## Maintainers
* [Oliver Klee](https://github.com/oliverklee)
* [Zoli Szabó](https://github.com/zoliszabo)
* [Jake Hotson](https://github.com/JakeQZ)
* [John Reeve](https://github.com/jjriv)

View File

@@ -0,0 +1,95 @@
{
"name": "pelago/emogrifier",
"description": "Converts CSS styles into inline style attributes in your HTML code",
"keywords": [
"email",
"css",
"pre-processing"
],
"homepage": "https://www.myintervals.com/emogrifier.php",
"license": "MIT",
"authors": [
{
"name": "Oliver Klee",
"email": "github@oliverklee.de"
},
{
"name": "Zoli Szabó",
"email": "zoli.szabo+github@gmail.com"
},
{
"name": "John Reeve",
"email": "jreeve@pelagodesign.com"
},
{
"name": "Jake Hotson",
"email": "jake@qzdesign.co.uk"
},
{
"name": "Cameron Brooks"
},
{
"name": "Jaime Prado"
}
],
"support": {
"issues": "https://github.com/MyIntervals/emogrifier/issues",
"source": "https://github.com/MyIntervals/emogrifier"
},
"require": {
"php": "^5.5.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0",
"ext-dom": "*",
"ext-libxml": "*",
"symfony/css-selector": "^3.4.0 || ^4.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.2.0",
"squizlabs/php_codesniffer": "^3.3.2",
"phpmd/phpmd": "^2.6.0",
"phpunit/phpunit": "^4.8.0"
},
"autoload": {
"psr-4": {
"Pelago\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Pelago\\Tests\\": "tests/"
}
},
"prefer-stable": true,
"config": {
"preferred-install": {
"*": "dist"
}
},
"scripts": {
"php:version": "php -v | grep -Po 'PHP\\s++\\K(?:\\d++\\.)*+\\d++(?:-\\w++)?+'",
"php:fix": "php-cs-fixer --config=config/php-cs-fixer.php fix config/ src/ tests/",
"ci:php:lint": "find config src tests -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l",
"ci:php:sniff": "phpcs config src tests",
"ci:php:md": "phpmd src text config/phpmd.xml",
"ci:tests:unit": "phpunit tests/",
"ci:tests": [
"@ci:tests:unit"
],
"ci:dynamic": [
"@ci:tests"
],
"ci:static": [
"@ci:php:lint",
"@ci:php:sniff",
"@ci:php:md"
],
"ci": [
"@ci:static",
"@ci:dynamic"
]
},
"extra": {
"branch-alias": {
"dev-master": "2.1.x-dev"
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
if (PHP_SAPI !== 'cli') {
die('This script supports command line usage only. Please check your command.');
}
return \PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules(
[
// copied from the TYPO3 Core
'@PSR2' => true,
'@DoctrineAnnotation' => true,
'no_leading_import_slash' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_unused_imports' => true,
'concat_space' => ['spacing' => 'one'],
'no_whitespace_in_blank_line' => true,
'ordered_imports' => true,
'single_quote' => true,
'no_empty_statement' => true,
'no_extra_consecutive_blank_lines' => true,
'phpdoc_no_package' => true,
'phpdoc_scalar' => true,
'no_blank_lines_after_phpdoc' => true,
'array_syntax' => ['syntax' => 'short'],
'whitespace_after_comma_in_array' => true,
'function_typehint_space' => true,
'hash_to_slash_comment' => true,
'no_alias_functions' => true,
'lowercase_cast' => true,
'no_leading_namespace_whitespace' => true,
'native_function_casing' => true,
'no_short_bool_cast' => true,
'no_unneeded_control_parentheses' => true,
'phpdoc_trim' => true,
'no_superfluous_elseif' => true,
'no_useless_else' => true,
'phpdoc_types' => true,
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
'return_type_declaration' => ['space_before' => 'none'],
'cast_spaces' => ['space' => 'none'],
'declare_equal_normalize' => ['space' => 'single'],
'dir_constant' => true,
// additional rules
'combine_consecutive_issets' => true,
'combine_consecutive_unsets' => true,
'compact_nullable_typehint' => true,
// PHP >= 7.0
// 'declare_strict_types' => true,
'elseif' => true,
'encoding' => true,
'escape_implicit_backslashes' => ['single_quoted' => true],
'is_null' => true,
'linebreak_after_opening_tag' => true,
'magic_constant_casing' => true,
'method_separation' => true,
'modernize_types_casting' => true,
// not yet, but maybe later to improve performance
// 'native_function_invocation' => true,
'new_with_braces' => true,
'no_blank_lines_after_class_opening' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
'no_extra_blank_lines' => true,
'no_multiline_whitespace_before_semicolons' => true,
'no_php4_constructor' => true,
'no_short_echo_tag' => true,
'no_spaces_after_function_name' => true,
'no_spaces_inside_parenthesis' => true,
'no_unneeded_curly_braces' => true,
'no_useless_return' => true,
'no_whitespace_before_comma_in_array' => true,
'php_unit_construct' => true,
'php_unit_fqcn_annotation' => true,
'php_unit_set_up_tear_down_visibility' => true,
'phpdoc_add_missing_param_annotation' => true,
'phpdoc_indent' => true,
'phpdoc_separation' => true,
'semicolon_after_instruction' => true,
'short_scalar_cast' => true,
'space_after_semicolon' => true,
'standardize_not_equals' => true,
'psr4' => true,
'ternary_operator_spaces' => true,
// PHP >= 7.0
// 'ternary_to_null_coalescing' => true,
'trailing_comma_in_multiline_array' => true,
'unary_operator_spaces' => true,
]
);

View File

@@ -0,0 +1,49 @@
<?xml version="1.0"?>
<ruleset name="phpList">
<description>
PHPMD rules for Emogrifier
</description>
<!-- The commented-out rules will be enabled once the code does not generate any warnings anymore. -->
<rule ref="rulesets/cleancode.xml/BooleanArgumentFlag"/>
<rule ref="rulesets/cleancode.xml/StaticAccess"/>
<rule ref="rulesets/codesize.xml/CyclomaticComplexity"/>
<rule ref="rulesets/codesize.xml/NPathComplexity"/>
<rule ref="rulesets/codesize.xml/ExcessiveMethodLength"/>
<!--<rule ref="rulesets/codesize.xml/ExcessiveClassLength"/>-->
<!--<rule ref="rulesets/codesize.xml/ExcessiveParameterList"/>-->
<rule ref="rulesets/codesize.xml/ExcessivePublicCount"/>
<!--<rule ref="rulesets/codesize.xml/TooManyFields"/>-->
<!--<rule ref="rulesets/codesize.xml/TooManyMethods"/>-->
<!--<rule ref="rulesets/codesize.xml/TooManyPublicMethods"/>-->
<!--<rule ref="rulesets/codesize.xml/ExcessiveClassComplexity"/>-->
<rule ref="rulesets/controversial.xml/Superglobals"/>
<rule ref="rulesets/controversial.xml/CamelCaseClassName"/>
<rule ref="rulesets/controversial.xml/CamelCasePropertyName"/>
<rule ref="rulesets/controversial.xml/CamelCaseMethodName"/>
<rule ref="rulesets/controversial.xml/CamelCaseParameterName"/>
<rule ref="rulesets/controversial.xml/CamelCaseVariableName"/>
<rule ref="rulesets/design.xml/ExitExpression"/>
<rule ref="rulesets/design.xml/EvalExpression"/>
<rule ref="rulesets/design.xml/GotoStatement"/>
<rule ref="rulesets/design.xml/NumberOfChildren"/>
<rule ref="rulesets/design.xml/DepthOfInheritance"/>
<rule ref="rulesets/design.xml/CouplingBetweenObjects"/>
<rule ref="rulesets/design.xml/DevelopmentCodeFragment"/>
<!--<rule ref="rulesets/naming.xml/ShortVariable"/>-->
<!--<rule ref="rulesets/naming.xml/LongVariable"/>-->
<rule ref="rulesets/naming.xml/ShortMethodName"/>
<rule ref="rulesets/naming.xml/ConstructorWithNameAsEnclosingClass"/>
<rule ref="rulesets/naming.xml/ConstantNamingConventions"/>
<rule ref="rulesets/naming.xml/BooleanGetMethodName"/>
<rule ref="rulesets/unusedcode.xml/UnusedPrivateField"/>
<rule ref="rulesets/unusedcode.xml/UnusedLocalVariable"/>
<!--<rule ref="rulesets/unusedcode.xml/UnusedPrivateMethod"/>-->
<!--<rule ref="rulesets/unusedcode.xml/UnusedFormalParameter"/>-->
</ruleset>

View File

@@ -1,15 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="PPW Coding Standard">
<description>This is the coding standard used for the Emogrifier code.
This standard has been tested with to work with PHP_CodeSniffer >= 2.3.0.
<ruleset name="Coding Standard">
<description>
This standard requires PHP_CodeSniffer >= 3.2.0.
</description>
<config name="installed_paths" value="../../slevomat/coding-standard"/>
<!--The complete PSR-2 ruleset-->
<rule ref="PSR2"/>
<!-- Arrays -->
<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
<rule ref="Squiz.Arrays.ArrayBracketSpacing"/>
<rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast"/>
<!-- Classes -->
<rule ref="Generic.Classes.DuplicateClassName"/>
@@ -20,6 +23,7 @@
<!-- Code analysis -->
<rule ref="Generic.CodeAnalysis.EmptyStatement"/>
<rule ref="Generic.CodeAnalysis.AssignmentInCondition"/>
<rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
<rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
<rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
@@ -34,52 +38,58 @@
<rule ref="PEAR.Commenting.InlineComment"/>
<rule ref="Squiz.Commenting.DocCommentAlignment"/>
<rule ref="Squiz.Commenting.EmptyCatchComment"/>
<rule ref="Squiz.Commenting.FunctionComment">
<!-- Allow PHP-5-compatible type hinting. -->
<exclude name="Squiz.Commenting.FunctionComment.ScalarTypeHintMissing"/>
<!-- Allow no comment for self-describing parameter and exception class names. -->
<exclude name="Squiz.Commenting.FunctionComment.MissingParamComment"/>
<exclude name="Squiz.Commenting.FunctionComment.EmptyThrows"/>
<!-- Allow "int" rather than "integer", etc., in PHPDoc. -->
<exclude name="Squiz.Commenting.FunctionComment.IncorrectParamVarName"/>
<exclude name="Squiz.Commenting.FunctionComment.InvalidReturn"/>
<!-- Allow "@return" to be omitted (for methods which do not return a value). -->
<exclude name="Squiz.Commenting.FunctionComment.MissingReturn"/>
<!-- Allow parameter type, name and comment not all vertically aligned. -->
<exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamType"/>
<exclude name="Squiz.Commenting.FunctionComment.SpacingAfterParamName"/>
<!-- Allow parameter and exception descriptions which are not full sentences. -->
<exclude name="Squiz.Commenting.FunctionComment.ParamCommentNotCapital"/>
<exclude name="Squiz.Commenting.FunctionComment.ParamCommentFullStop"/>
<exclude name="Squiz.Commenting.FunctionComment.ThrowsNotCapital"/>
<exclude name="Squiz.Commenting.FunctionComment.ThrowsNoFullStop"/>
</rule>
<rule ref="Squiz.Commenting.FunctionCommentThrowTag"/>
<rule ref="Squiz.Commenting.PostStatementComment"/>
<rule ref="TYPO3SniffPool.Commenting.ClassComment"/>
<rule ref="TYPO3SniffPool.Commenting.DoubleSlashCommentsInNewLine"/>
<rule ref="TYPO3SniffPool.Commenting.SpaceAfterDoubleSlash"/>
<!-- Control structures -->
<rule ref="PEAR.ControlStructures.ControlSignature"/>
<rule ref="TYPO3SniffPool.ControlStructures.DisallowEachInLoopCondition"/>
<rule ref="TYPO3SniffPool.ControlStructures.DisallowElseIfConstruct"/>
<rule ref="TYPO3SniffPool.ControlStructures.ExtraBracesByAssignmentInLoop"/>
<rule ref="TYPO3SniffPool.ControlStructures.SwitchDeclaration"/>
<rule ref="TYPO3SniffPool.ControlStructures.TernaryConditionalOperator"/>
<rule ref="TYPO3SniffPool.ControlStructures.UnusedVariableInForEachLoop"/>
<!-- Debug -->
<rule ref="Generic.Debug.ClosureLinter"/>
<rule ref="TYPO3SniffPool.Debug.DebugCode"/>
<!-- Files -->
<rule ref="Generic.Files.OneClassPerFile"/>
<rule ref="Generic.Files.OneInterfacePerFile"/>
<rule ref="TYPO3SniffPool.Files.FileExtension"/>
<rule ref="TYPO3SniffPool.Files.Filename"/>
<rule ref="TYPO3SniffPool.Files.IncludingFile"/>
<rule ref="Generic.Files.OneObjectStructurePerFile"/>
<rule ref="Zend.Files.ClosingTag"/>
<!-- Formatting -->
<rule ref="Generic.Formatting.SpaceAfterCast"/>
<rule ref="Generic.Formatting.NoSpaceAfterCast"/>
<rule ref="PEAR.Formatting.MultiLineAssignment"/>
<!-- Functions -->
<rule ref="Generic.Functions.CallTimePassByReference"/>
<rule ref="SlevomatCodingStandard.Namespaces.FullyQualifiedGlobalFunctions"/>
<rule ref="Squiz.Functions.FunctionDuplicateArgument"/>
<rule ref="Squiz.Functions.GlobalFunction"/>
<!-- Metrics -->
<!-- Enable this rule when the cyclomatic complexity of all methods is sufficiently low. -->
<!--<rule ref="Generic.Metrics.CyclomaticComplexity"/>-->
<rule ref="Generic.Metrics.CyclomaticComplexity"/>
<rule ref="Generic.Metrics.NestingLevel"/>
<!-- Naming conventions -->
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="PEAR.NamingConventions.ValidClassName"/>
<rule ref="TYPO3SniffPool.NamingConventions.ValidFunctionName"/>
<rule ref="TYPO3SniffPool.NamingConventions.ValidVariableName"/>
<!-- Objects -->
<rule ref="Squiz.Objects.ObjectMemberComma"/>
@@ -89,9 +99,12 @@
<rule ref="Squiz.Operators.ValidLogicalOperators"/>
<!-- PHP -->
<rule ref="Generic.PHP.BacktickOperator"/>
<rule ref="Generic.PHP.CharacterBeforePHPOpeningTag"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.DisallowAlternativePHPTags"/>
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
<rule ref="Generic.PHP.DiscourageGoto"/>
<rule ref="Generic.PHP.ForbiddenFunctions"/>
<rule ref="Generic.PHP.NoSilencedErrors"/>
<rule ref="Squiz.PHP.CommentedOutCode">
@@ -103,7 +116,6 @@
<rule ref="Squiz.PHP.DisallowSizeFunctionsInLoops"/>
<rule ref="Squiz.PHP.DiscouragedFunctions"/>
<rule ref="Squiz.PHP.Eval"/>
<rule ref="Squiz.PHP.ForbiddenFunctions"/>
<rule ref="Squiz.PHP.GlobalKeyword"/>
<rule ref="Squiz.PHP.Heredoc"/>
<rule ref="Squiz.PHP.InnerFunctions"/>
@@ -113,14 +125,9 @@
<!-- Scope -->
<rule ref="Squiz.Scope.MemberVarScope"/>
<rule ref="Squiz.Scope.StaticThisUsage"/>
<rule ref="TYPO3SniffPool.Scope.AlwaysReturn">
<exclude-pattern>*/Tests/*</exclude-pattern>
</rule>
<!--Strings-->
<rule ref="Squiz.Strings.DoubleQuoteUsage"/>
<rule ref="TYPO3SniffPool.Strings.ConcatenationSpacing"/>
<rule ref="TYPO3SniffPool.Strings.UnnecessaryStringConcat"/>
<!-- Whitespace -->
<rule ref="PEAR.WhiteSpace.ObjectOperatorIndent"/>
@@ -130,7 +137,4 @@
<rule ref="Squiz.WhiteSpace.OperatorSpacing"/>
<rule ref="Squiz.WhiteSpace.PropertyLabelSpacing"/>
<rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
<rule ref="TYPO3SniffPool.WhiteSpace.NoWhitespaceAtInDecrement"/>
<rule ref="TYPO3SniffPool.WhiteSpace.ScopeClosingBrace"/>
<rule ref="TYPO3SniffPool.WhiteSpace.WhitespaceAfterCommentSigns"/>
</ruleset>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,154 @@
<?php
namespace Pelago\Emogrifier;
/**
* Facilitates building a CSS string by appending rule blocks one at a time, checking whether the media query,
* selectors, or declarations block are the same as those from the preceding block and combining blocks in such cases.
*
* Example:
* $concatenator = new CssConcatenator();
* $concatenator->append(['body'], 'color: blue;');
* $concatenator->append(['body'], 'font-size: 16px;');
* $concatenator->append(['p'], 'margin: 1em 0;');
* $concatenator->append(['ul', 'ol'], 'margin: 1em 0;');
* $concatenator->append(['body'], 'font-size: 14px;', '@media screen and (max-width: 400px)');
* $concatenator->append(['ul', 'ol'], 'margin: 0.75em 0;', '@media screen and (max-width: 400px)');
* $css = $concatenator->getCss();
*
* `$css` (if unminified) would contain the following CSS:
* ` body {
* ` color: blue;
* ` font-size: 16px;
* ` }
* ` p, ul, ol {
* ` margin: 1em 0;
* ` }
* ` @media screen and (max-width: 400px) {
* ` body {
* ` font-size: 14px;
* ` }
* ` ul, ol {
* ` margin: 0.75em 0;
* ` }
* ` }
*
* @author Jake Hotson <jake.github@qzdesign.co.uk>
*/
class CssConcatenator
{
/**
* Array of media rules in order. Each element is an object with the following properties:
* - string `media` - The media query string, e.g. "@media screen and (max-width:639px)", or an empty string for
* rules not within a media query block;
* - \stdClass[] `ruleBlocks` - Array of rule blocks in order, where each element is an object with the following
* properties:
* - mixed[] `selectorsAsKeys` - Array whose keys are selectors for the rule block (values are of no
* significance);
* - string `declarationsBlock` - The property declarations, e.g. "margin-top: 0.5em; padding: 0".
*
* @var \stdClass[]
*/
private $mediaRules = [];
/**
* Appends a declaration block to the CSS.
*
* @param string[] $selectors Array of selectors for the rule, e.g. ["ul", "ol", "p:first-child"].
* @param string $declarationsBlock The property declarations, e.g. "margin-top: 0.5em; padding: 0".
* @param string $media The media query for the rule, e.g. "@media screen and (max-width:639px)",
* or an empty string if none.
*/
public function append(array $selectors, $declarationsBlock, $media = '')
{
$selectorsAsKeys = \array_flip($selectors);
$mediaRule = $this->getOrCreateMediaRuleToAppendTo($media);
$lastRuleBlock = \end($mediaRule->ruleBlocks);
$hasSameDeclarationsAsLastRule = $lastRuleBlock !== false
&& $declarationsBlock === $lastRuleBlock->declarationsBlock;
if ($hasSameDeclarationsAsLastRule) {
$lastRuleBlock->selectorsAsKeys += $selectorsAsKeys;
} else {
$hasSameSelectorsAsLastRule = $lastRuleBlock !== false
&& static::hasEquivalentSelectors($selectorsAsKeys, $lastRuleBlock->selectorsAsKeys);
if ($hasSameSelectorsAsLastRule) {
$lastDeclarationsBlockWithoutSemicolon = \rtrim(\rtrim($lastRuleBlock->declarationsBlock), ';');
$lastRuleBlock->declarationsBlock = $lastDeclarationsBlockWithoutSemicolon . ';' . $declarationsBlock;
} else {
$mediaRule->ruleBlocks[] = (object)\compact('selectorsAsKeys', 'declarationsBlock');
}
}
}
/**
* @return string
*/
public function getCss()
{
return \implode('', \array_map([$this, 'getMediaRuleCss'], $this->mediaRules));
}
/**
* @param string $media The media query for rules to be appended, e.g. "@media screen and (max-width:639px)",
* or an empty string if none.
*
* @return \stdClass Object with properties as described for elements of `$mediaRules`.
*/
private function getOrCreateMediaRuleToAppendTo($media)
{
$lastMediaRule = \end($this->mediaRules);
if ($lastMediaRule !== false && $media === $lastMediaRule->media) {
return $lastMediaRule;
}
$newMediaRule = (object)[
'media' => $media,
'ruleBlocks' => [],
];
$this->mediaRules[] = $newMediaRule;
return $newMediaRule;
}
/**
* Tests if two sets of selectors are equivalent (i.e. the same selectors, possibly in a different order).
*
* @param mixed[] $selectorsAsKeys1 Array in which the selectors are the keys, and the values are of no
* significance.
* @param mixed[] $selectorsAsKeys2 Another such array.
*
* @return bool
*/
private static function hasEquivalentSelectors(array $selectorsAsKeys1, array $selectorsAsKeys2)
{
return \count($selectorsAsKeys1) === \count($selectorsAsKeys2)
&& \count($selectorsAsKeys1) === \count($selectorsAsKeys1 + $selectorsAsKeys2);
}
/**
* @param \stdClass $mediaRule Object with properties as described for elements of `$mediaRules`.
*
* @return string CSS for the media rule.
*/
private static function getMediaRuleCss(\stdClass $mediaRule)
{
$css = \implode('', \array_map([static::class, 'getRuleBlockCss'], $mediaRule->ruleBlocks));
if ($mediaRule->media !== '') {
$css = $mediaRule->media . '{' . $css . '}';
}
return $css;
}
/**
* @param \stdClass $ruleBlock Object with properties as described for elements of the `ruleBlocks` property of
* elements of `$mediaRules`.
*
* @return string CSS for the rule block.
*/
private static function getRuleBlockCss(\stdClass $ruleBlock)
{
$selectors = \array_keys($ruleBlock->selectorsAsKeys);
return \implode(',', $selectors) . '{' . $ruleBlock->declarationsBlock . '}';
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,219 @@
<?php
namespace Pelago\Emogrifier\HtmlProcessor;
/**
* Base class for HTML processor that e.g., can remove, add or modify nodes or attributes.
*
* The "vanilla" subclass is the HtmlNormalizer.
*
* @internal This class currently is a new technology preview, and its API is still in flux. Don't use it in production.
*
* @author Oliver Klee <github@oliverklee.de>
*/
abstract class AbstractHtmlProcessor
{
/**
* @var string
*/
const DEFAULT_DOCUMENT_TYPE = '<!DOCTYPE html>';
/**
* @var string
*/
const CONTENT_TYPE_META_TAG = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">';
/**
* @var \DOMDocument
*/
protected $domDocument = null;
/**
* @param string $unprocessedHtml raw HTML, must be UTF-encoded, must not be empty
*
* @throws \InvalidArgumentException if $unprocessedHtml is anything other than a non-empty string
*/
public function __construct($unprocessedHtml)
{
if (!\is_string($unprocessedHtml)) {
throw new \InvalidArgumentException('The provided HTML must be a string.', 1515459744);
}
if ($unprocessedHtml === '') {
throw new \InvalidArgumentException('The provided HTML must not be empty.', 1515763647);
}
$this->setHtml($unprocessedHtml);
}
/**
* Sets the HTML to process.
*
* @param string $html the HTML to process, must be UTF-8-encoded
*
* @return void
*/
private function setHtml($html)
{
$this->createUnifiedDomDocument($html);
}
/**
* Provides access to the internal DOMDocument representation of the HTML in its current state.
*
* @return \DOMDocument
*/
public function getDomDocument()
{
return $this->domDocument;
}
/**
* Renders the normalized and processed HTML.
*
* @return string
*/
public function render()
{
return $this->domDocument->saveHTML();
}
/**
* Renders the content of the BODY element of the normalized and processed HTML.
*
* @return string
*/
public function renderBodyContent()
{
$bodyNodeHtml = $this->domDocument->saveHTML($this->getBodyElement());
return \str_replace(['<body>', '</body>'], '', $bodyNodeHtml);
}
/**
* Returns the BODY element.
*
* This method assumes that there always is a BODY element.
*
* @return \DOMElement
*/
private function getBodyElement()
{
return $this->domDocument->getElementsByTagName('body')->item(0);
}
/**
* Creates a DOM document from the given HTML and stores it in $this->domDocument.
*
* The DOM document will always have a BODY element and a document type.
*
* @param string $html
*
* @return void
*/
private function createUnifiedDomDocument($html)
{
$this->createRawDomDocument($html);
$this->ensureExistenceOfBodyElement();
}
/**
* Creates a DOMDocument instance from the given HTML and stores it in $this->domDocument.
*
* @param string $html
*
* @return void
*/
private function createRawDomDocument($html)
{
$domDocument = new \DOMDocument();
$domDocument->strictErrorChecking = false;
$domDocument->formatOutput = true;
$libXmlState = \libxml_use_internal_errors(true);
$domDocument->loadHTML($this->prepareHtmlForDomConversion($html));
\libxml_clear_errors();
\libxml_use_internal_errors($libXmlState);
$this->domDocument = $domDocument;
}
/**
* Returns the HTML with added document type and Content-Type meta tag if needed,
* ensuring that the HTML will be good for creating a DOM document from it.
*
* @param string $html
*
* @return string the unified HTML
*/
private function prepareHtmlForDomConversion($html)
{
$htmlWithDocumentType = $this->ensureDocumentType($html);
return $this->addContentTypeMetaTag($htmlWithDocumentType);
}
/**
* Makes sure that the passed HTML has a document type.
*
* @param string $html
*
* @return string HTML with document type
*/
private function ensureDocumentType($html)
{
$hasDocumentType = \stripos($html, '<!DOCTYPE') !== false;
if ($hasDocumentType) {
return $html;
}
return static::DEFAULT_DOCUMENT_TYPE . $html;
}
/**
* Adds a Content-Type meta tag for the charset.
*
* @param string $html
*
* @return string the HTML with the meta tag added
*/
private function addContentTypeMetaTag($html)
{
$hasContentTypeMetaTag = \stripos($html, 'Content-Type') !== false;
if ($hasContentTypeMetaTag) {
return $html;
}
// We are trying to insert the meta tag to the right spot in the DOM.
// If we just prepended it to the HTML, we would lose attributes set to the HTML tag.
$hasHeadTag = \stripos($html, '<head') !== false;
$hasHtmlTag = \stripos($html, '<html') !== false;
if ($hasHeadTag) {
$reworkedHtml = \preg_replace('/<head(.*?)>/i', '<head$1>' . static::CONTENT_TYPE_META_TAG, $html);
} elseif ($hasHtmlTag) {
$reworkedHtml = \preg_replace(
'/<html(.*?)>/i',
'<html$1><head>' . static::CONTENT_TYPE_META_TAG . '</head>',
$html
);
} else {
$reworkedHtml = static::CONTENT_TYPE_META_TAG . $html;
}
return $reworkedHtml;
}
/**
* Checks that $this->domDocument has a BODY element and adds it if it is missing.
*
* @return void
*/
private function ensureExistenceOfBodyElement()
{
if ($this->domDocument->getElementsByTagName('body')->item(0) !== null) {
return;
}
$htmlElement = $this->domDocument->getElementsByTagName('html')->item(0);
$htmlElement->appendChild($this->domDocument->createElement('body'));
}
}

View File

@@ -0,0 +1,320 @@
<?php
namespace Pelago\Emogrifier\HtmlProcessor;
/**
* This HtmlProcessor can convert style HTML attributes to the corresponding other visual HTML attributes,
* e.g. it converts style="width: 100px" to width="100".
*
* It will only add attributes, but leaves the style attribute untouched.
*
* To trigger the conversion, call the convertCssToVisualAttributes method.
*
* @internal This class currently is a new technology preview, and its API is still in flux. Don't use it in production.
*
* @author Oliver Klee <github@oliverklee.de>
*/
class CssToAttributeConverter extends AbstractHtmlProcessor
{
/**
* This multi-level array contains simple mappings of CSS properties to
* HTML attributes. If a mapping only applies to certain HTML nodes or
* only for certain values, the mapping is an object with a whitelist
* of nodes and values.
*
* @var mixed[][]
*/
private $cssToHtmlMap = [
'background-color' => [
'attribute' => 'bgcolor',
],
'text-align' => [
'attribute' => 'align',
'nodes' => ['p', 'div', 'td'],
'values' => ['left', 'right', 'center', 'justify'],
],
'float' => [
'attribute' => 'align',
'nodes' => ['table', 'img'],
'values' => ['left', 'right'],
],
'border-spacing' => [
'attribute' => 'cellspacing',
'nodes' => ['table'],
],
];
/**
* @var string[][]
*/
private static $parsedCssCache = [];
/**
* Maps the CSS from the style nodes to visual HTML attributes.
*
* @return CssToAttributeConverter fluent interface
*/
public function convertCssToVisualAttributes()
{
/** @var \DOMElement $node */
foreach ($this->getAllNodesWithStyleAttribute() as $node) {
$inlineStyleDeclarations = $this->parseCssDeclarationsBlock($node->getAttribute('style'));
$this->mapCssToHtmlAttributes($inlineStyleDeclarations, $node);
}
return $this;
}
/**
* Returns a list with all DOM nodes that have a style attribute.
*
* @return \DOMNodeList
*/
private function getAllNodesWithStyleAttribute()
{
$xPath = new \DOMXPath($this->domDocument);
return $xPath->query('//*[@style]');
}
/**
* Parses a CSS declaration block into property name/value pairs.
*
* Example:
*
* The declaration block
*
* "color: #000; font-weight: bold;"
*
* will be parsed into the following array:
*
* "color" => "#000"
* "font-weight" => "bold"
*
* @param string $cssDeclarationsBlock the CSS declarations block without the curly braces, may be empty
*
* @return string[]
* the CSS declarations with the property names as array keys and the property values as array values
*/
private function parseCssDeclarationsBlock($cssDeclarationsBlock)
{
if (isset(self::$parsedCssCache[$cssDeclarationsBlock])) {
return self::$parsedCssCache[$cssDeclarationsBlock];
}
$properties = [];
$declarations = \preg_split('/;(?!base64|charset)/', $cssDeclarationsBlock);
foreach ($declarations as $declaration) {
$matches = [];
if (!\preg_match('/^([A-Za-z\\-]+)\\s*:\\s*(.+)$/s', \trim($declaration), $matches)) {
continue;
}
$propertyName = \strtolower($matches[1]);
$propertyValue = $matches[2];
$properties[$propertyName] = $propertyValue;
}
self::$parsedCssCache[$cssDeclarationsBlock] = $properties;
return $properties;
}
/**
* Applies $styles to $node.
*
* This method maps CSS styles to HTML attributes and adds those to the
* node.
*
* @param string[] $styles the new CSS styles taken from the global styles to be applied to this node
* @param \DOMElement $node node to apply styles to
*
* @return void
*/
private function mapCssToHtmlAttributes(array $styles, \DOMElement $node)
{
foreach ($styles as $property => $value) {
// Strip !important indicator
$value = \trim(\str_replace('!important', '', $value));
$this->mapCssToHtmlAttribute($property, $value, $node);
}
}
/**
* Tries to apply the CSS style to $node as an attribute.
*
* This method maps a CSS rule to HTML attributes and adds those to the node.
*
* @param string $property the name of the CSS property to map
* @param string $value the value of the style rule to map
* @param \DOMElement $node node to apply styles to
*
* @return void
*/
private function mapCssToHtmlAttribute($property, $value, \DOMElement $node)
{
if (!$this->mapSimpleCssProperty($property, $value, $node)) {
$this->mapComplexCssProperty($property, $value, $node);
}
}
/**
* Looks up the CSS property in the mapping table and maps it if it matches the conditions.
*
* @param string $property the name of the CSS property to map
* @param string $value the value of the style rule to map
* @param \DOMElement $node node to apply styles to
*
* @return bool true if the property can be mapped using the simple mapping table
*/
private function mapSimpleCssProperty($property, $value, \DOMElement $node)
{
if (!isset($this->cssToHtmlMap[$property])) {
return false;
}
$mapping = $this->cssToHtmlMap[$property];
$nodesMatch = !isset($mapping['nodes']) || \in_array($node->nodeName, $mapping['nodes'], true);
$valuesMatch = !isset($mapping['values']) || \in_array($value, $mapping['values'], true);
if (!$nodesMatch || !$valuesMatch) {
return false;
}
$node->setAttribute($mapping['attribute'], $value);
return true;
}
/**
* Maps CSS properties that need special transformation to an HTML attribute.
*
* @param string $property the name of the CSS property to map
* @param string $value the value of the style rule to map
* @param \DOMElement $node node to apply styles to
*
* @return void
*/
private function mapComplexCssProperty($property, $value, \DOMElement $node)
{
switch ($property) {
case 'background':
$this->mapBackgroundProperty($node, $value);
break;
case 'width':
// intentional fall-through
case 'height':
$this->mapWidthOrHeightProperty($node, $value, $property);
break;
case 'margin':
$this->mapMarginProperty($node, $value);
break;
case 'border':
$this->mapBorderProperty($node, $value);
break;
default:
}
}
/**
* @param \DOMElement $node node to apply styles to
* @param string $value the value of the style rule to map
*
* @return void
*/
private function mapBackgroundProperty(\DOMElement $node, $value)
{
// parse out the color, if any
$styles = \explode(' ', $value);
$first = $styles[0];
if (!\is_numeric($first[0]) && \strpos($first, 'url') !== 0) {
// as this is not a position or image, assume it's a color
$node->setAttribute('bgcolor', $first);
}
}
/**
* @param \DOMElement $node node to apply styles to
* @param string $value the value of the style rule to map
* @param string $property the name of the CSS property to map
*
* @return void
*/
private function mapWidthOrHeightProperty(\DOMElement $node, $value, $property)
{
// only parse values in px and %, but not values like "auto"
if (!\preg_match('/^(\\d+)(px|%)$/', $value)) {
return;
}
$number = \preg_replace('/[^0-9.%]/', '', $value);
$node->setAttribute($property, $number);
}
/**
* @param \DOMElement $node node to apply styles to
* @param string $value the value of the style rule to map
*
* @return void
*/
private function mapMarginProperty(\DOMElement $node, $value)
{
if (!$this->isTableOrImageNode($node)) {
return;
}
$margins = $this->parseCssShorthandValue($value);
if ($margins['left'] === 'auto' && $margins['right'] === 'auto') {
$node->setAttribute('align', 'center');
}
}
/**
* @param \DOMElement $node node to apply styles to
* @param string $value the value of the style rule to map
*
* @return void
*/
private function mapBorderProperty(\DOMElement $node, $value)
{
if (!$this->isTableOrImageNode($node)) {
return;
}
if ($value === 'none' || $value === '0') {
$node->setAttribute('border', '0');
}
}
/**
* @param \DOMElement $node
*
* @return bool
*/
private function isTableOrImageNode(\DOMElement $node)
{
return $node->nodeName === 'table' || $node->nodeName === 'img';
}
/**
* Parses a shorthand CSS value and splits it into individual values
*
* @param string $value a string of CSS value with 1, 2, 3 or 4 sizes
* For example: padding: 0 auto;
* '0 auto' is split into top: 0, left: auto, bottom: 0,
* right: auto.
*
* @return string[] an array of values for top, right, bottom and left (using these as associative array keys)
*/
private function parseCssShorthandValue($value)
{
$values = \preg_split('/\\s+/', $value);
$css = [];
$css['top'] = $values[0];
$css['right'] = (\count($values) > 1) ? $values[1] : $css['top'];
$css['bottom'] = (\count($values) > 2) ? $values[2] : $css['top'];
$css['left'] = (\count($values) > 3) ? $values[3] : $css['right'];
return $css;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Pelago\Emogrifier\HtmlProcessor;
/**
* Normalizes HTML:
* - add a document type (HTML5) if missing
* - disentangle incorrectly nested tags
* - add HEAD and BODY elements (if they are missing)
* - reformat the HTML
*
* @internal This class currently is a new technology preview, and its API is still in flux. Don't use it in production.
*
* @author Oliver Klee <github@oliverklee.de>
*/
class HtmlNormalizer extends AbstractHtmlProcessor
{
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Pelago\Tests\Support\Traits;
/**
* Provides assertion methods for use with CSS content where whitespace may vary.
*
* @author Jake Hotson <jake.github@qzdesign.co.uk>
*/
trait AssertCss
{
/**
* Processing of @media rules may involve removal of some unnecessary whitespace from the CSS placed in the <style>
* element added to the docuemnt, due to the way that certain parts are `trim`med. Notably, whitespace either side
* of "{", "}" and "," or at the beginning of the CSS may be removed.
*
* This method helps takes care of that, by converting a search needle for an exact match into a regular expression
* that allows for such whitespace removal, so that the tests themselves do not need to be written less humanly
* readable and can use inputs containing extra whitespace.
*
* @param string $needle Needle that would be used with `assertContains` or `assertNotContains`.
*
* @return string Needle to use with `assertRegExp` or `assertNotRegExp` instead.
*/
private static function getCssNeedleRegExp($needle)
{
$needleMatcher = \preg_replace_callback(
'/\\s*+([{},])\\s*+|(^\\s++)|(>)\\s*+|(?:(?!\\s*+[{},]|^\\s)[^>])++/',
function (array $matches) {
if (isset($matches[1]) && $matches[1] !== '') {
// matched possibly some whitespace, followed by "{", "}" or ",", then possibly more whitespace
return '\\s*+' . \preg_quote($matches[1], '/') . '\\s*+';
}
if (isset($matches[2]) && $matches[2] !== '') {
// matched whitespace at start
return '\\s*+';
}
if (isset($matches[3]) && $matches[3] !== '') {
// matched ">" (e.g. end of <style> tag) followed by possibly some whitespace
return \preg_quote($matches[3], '/') . '\\s*+';
}
// matched any other sequence which could not overlap with the above
return \preg_quote($matches[0], '/');
},
$needle
);
return '/' . $needleMatcher . '/';
}
/**
* Like `assertContains` but allows for removal of some unnecessary whitespace from the CSS.
*
* @param string $needle
* @param string $haystack
*/
private static function assertContainsCss($needle, $haystack)
{
static::assertRegExp(
static::getCssNeedleRegExp($needle),
$haystack,
'Plain text needle: "' . $needle . '"'
);
}
/**
* Like `assertNotContains` and also enforces the assertion with removal of some unnecessary whitespace from the
* CSS.
*
* @param string $needle
* @param string $haystack
*/
private static function assertNotContainsCss($needle, $haystack)
{
static::assertNotRegExp(
static::getCssNeedleRegExp($needle),
$haystack,
'Plain text needle: "' . $needle . '"'
);
}
/**
* Asserts that a string of CSS occurs exactly a certain number of times in the result, allowing for removal of some
* unnecessary whitespace.
*
* @param int $expectedCount
* @param string $needle
* @param string $haystack
*/
private static function assertContainsCssCount(
$expectedCount,
$needle,
$haystack
) {
static::assertSame(
$expectedCount,
\preg_match_all(static::getCssNeedleRegExp($needle), $haystack),
'Plain text needle: "' . $needle . "\"\nHaystack: \"" . $haystack . '"'
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
<?php
namespace Pelago\Tests\Unit\Emogrifier;
use Pelago\Emogrifier\CssConcatenator;
/**
* Test case.
*
* @author Jake Hotson <jake.github@qzdesign.co.uk>
*/
class CssConcatenatorTest extends \PHPUnit_Framework_TestCase
{
/**
* @var CssConcatenator
*/
private $subject = null;
/**
* @return void
*/
protected function setUp()
{
$this->subject = new CssConcatenator();
}
/**
* @test
*/
public function getCssInitiallyReturnsEmptyString()
{
$result = $this->subject->getCss();
static::assertSame('', $result);
}
/**
* @test
*/
public function appendSetsFirstRule()
{
$this->subject->append(['p'], 'color: green;');
$result = $this->subject->getCss();
static::assertSame('p{color: green;}', $result);
}
/**
* @test
*/
public function appendWithMediaQuerySetsFirstRuleInMediaRule()
{
$this->subject->append(['p'], 'color: green;', '@media screen');
$result = $this->subject->getCss();
static::assertSame('@media screen{p{color: green;}}', $result);
}
/**
* @return string[][]
*/
public function equivalentSelectorsDataProvider()
{
return [
'one selector' => [['p'], ['p']],
'two selectors' => [
['p', 'ul'],
['p', 'ul'],
],
'two selectors in different order' => [
['p', 'ul'],
['ul', 'p'],
],
];
}
/**
* @test
*
* @param string[] $selectors1
* @param string[] $selectors2
*
* @dataProvider equivalentSelectorsDataProvider
*/
public function appendCombinesRulesWithEquivalentSelectors(array $selectors1, array $selectors2)
{
$this->subject->append($selectors1, 'color: green;');
$this->subject->append($selectors2, 'font-size: 16px;');
$result = $this->subject->getCss();
$expectedResult = \implode(',', $selectors1) . '{color: green;font-size: 16px;}';
static::assertSame($expectedResult, $result);
}
/**
* @test
*/
public function appendInsertsSemicolonCombiningRulesWithoutTrailingSemicolon()
{
$this->subject->append(['p'], 'color: green');
$this->subject->append(['p'], 'font-size: 16px');
$result = $this->subject->getCss();
static::assertSame('p{color: green;font-size: 16px}', $result);
}
/**
* @return string[][]
*/
public function differentSelectorsDataProvider()
{
return [
'single selectors' => [
['p'],
['ul'],
['p', 'ul'],
],
'single selector and an entirely different pair' => [
['p'],
['ul', 'ol'],
['p', 'ul', 'ol'],
],
'single selector and a superset pair' => [
['p'],
['p', 'ul'],
['p', 'ul'],
],
'pair of selectors and an entirely different single' => [
['p', 'ul'],
['ol'],
['p', 'ul', 'ol'],
],
'pair of selectors and a subset single' => [
['p', 'ul'],
['ul'],
['p', 'ul'],
],
'entirely different pairs of selectors' => [
['p', 'ul'],
['ol', 'h1'],
['p', 'ul', 'ol', 'h1'],
],
'pairs of selectors with one common' => [
['p', 'ul'],
['ul', 'ol'],
['p', 'ul', 'ol'],
],
];
}
/**
* @test
*
* @param string[] $selectors1
* @param string[] $selectors2
* @param string[] $combinedSelectors
*
* @dataProvider differentSelectorsDataProvider
*/
public function appendCombinesSameRulesWithDifferentSelectors(
array $selectors1,
array $selectors2,
array $combinedSelectors
) {
$this->subject->append($selectors1, 'color: green;');
$this->subject->append($selectors2, 'color: green;');
$result = $this->subject->getCss();
$expectedResult = \implode(',', $combinedSelectors) . '{color: green;}';
static::assertSame($expectedResult, $result);
}
/**
* @test
*
* @param string[] $selectors1
* @param string[] $selectors2
*
* @dataProvider differentSelectorsDataProvider
*/
public function appendNotCombinesDifferentRulesWithDifferentSelectors(array $selectors1, array $selectors2)
{
$this->subject->append($selectors1, 'color: green;');
$this->subject->append($selectors2, 'font-size: 16px;');
$result = $this->subject->getCss();
$expectedResult = \implode(',', $selectors1) . '{color: green;}'
. \implode(',', $selectors2) . '{font-size: 16px;}';
static::assertSame($expectedResult, $result);
}
/**
* @test
*/
public function appendCombinesRulesForSameMediaQueryInMediaRule()
{
$this->subject->append(['p'], 'color: green;', '@media screen');
$this->subject->append(['ul'], 'font-size: 16px;', '@media screen');
$result = $this->subject->getCss();
static::assertSame('@media screen{p{color: green;}ul{font-size: 16px;}}', $result);
}
/**
* @test
*
* @param string[] $selectors1
* @param string[] $selectors2
*
* @dataProvider equivalentSelectorsDataProvider
*/
public function appendCombinesRulesWithEquivalentSelectorsWithinMediaRule(array $selectors1, array $selectors2)
{
$this->subject->append($selectors1, 'color: green;', '@media screen');
$this->subject->append($selectors2, 'font-size: 16px;', '@media screen');
$result = $this->subject->getCss();
$expectedResult = '@media screen{' . \implode(',', $selectors1) . '{color: green;font-size: 16px;}}';
static::assertSame($expectedResult, $result);
}
/**
* @test
*
* @param string[] $selectors1
* @param string[] $selectors2
* @param string[] $combinedSelectors
*
* @dataProvider differentSelectorsDataProvider
*/
public function appendCombinesSameRulesWithDifferentSelectorsWithinMediaRule(
array $selectors1,
array $selectors2,
array $combinedSelectors
) {
$this->subject->append($selectors1, 'color: green;', '@media screen');
$this->subject->append($selectors2, 'color: green;', '@media screen');
$result = $this->subject->getCss();
$expectedResult = '@media screen{' . \implode(',', $combinedSelectors) . '{color: green;}}';
static::assertSame($expectedResult, $result);
}
/**
* @test
*/
public function appendNotCombinesRulesForDifferentMediaQueryInMediaRule()
{
$this->subject->append(['p'], 'color: green;', '@media screen');
$this->subject->append(['p'], 'color: green;', '@media print');
$result = $this->subject->getCss();
static::assertSame('@media screen{p{color: green;}}@media print{p{color: green;}}', $result);
}
/**
* @return mixed[][]
*/
public function combinableRulesDataProvider()
{
return [
'same selectors' => [['p'], 'color: green;', ['p'], 'font-size: 16px;', ''],
'same declarations block' => [['p'], 'color: green;', ['ul'], 'color: green;', ''],
'same media query' => [['p'], 'color: green;', ['ul'], 'font-size: 16px;', '@media screen'],
];
}
/**
* @test
*
* @param array $rule1Selectors
* @param string $rule1DeclarationsBlock
* @param array $rule2Selectors
* @param string $rule2DeclarationsBlock
* @param string $media
*
* @dataProvider combinableRulesDataProvider
*/
public function appendNotCombinesNonadjacentRules(
array $rule1Selectors,
$rule1DeclarationsBlock,
array $rule2Selectors,
$rule2DeclarationsBlock,
$media
) {
$this->subject->append($rule1Selectors, $rule1DeclarationsBlock, $media);
$this->subject->append(['.intervening'], '-intervening-property: 0;');
$this->subject->append($rule2Selectors, $rule2DeclarationsBlock, $media);
$result = $this->subject->getCss();
$expectedRule1Css = \implode(',', $rule1Selectors) . '{' . $rule1DeclarationsBlock . '}';
$expectedRule2Css = \implode(',', $rule2Selectors) . '{' . $rule2DeclarationsBlock . '}';
if ($media !== '') {
$expectedRule1Css = $media . '{' . $expectedRule1Css . '}';
$expectedRule2Css = $media . '{' . $expectedRule2Css . '}';
}
$expectedResult = $expectedRule1Css . '.intervening{-intervening-property: 0;}' . $expectedRule2Css;
static::assertSame($expectedResult, $result);
}
}

View File

@@ -0,0 +1,385 @@
<?php
namespace Pelago\Tests\Unit\Emogrifier\HtmlProcessor;
use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
use Pelago\Tests\Unit\Emogrifier\HtmlProcessor\Fixtures\TestingHtmlProcessor;
/**
* Test case.
*
* @author Oliver Klee <github@oliverklee.de>
*/
class AbstractHtmlProcessorTest extends \PHPUnit_Framework_TestCase
{
/**
* @test
*/
public function fixtureIsAbstractHtmlProcessor()
{
static::assertInstanceOf(AbstractHtmlProcessor::class, new TestingHtmlProcessor('<html></html>'));
}
/**
* @test
*/
public function reformatsHtml()
{
$rawHtml = '<!DOCTYPE HTML>' .
'<html>' .
'<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' .
'<body></body>' .
'</html>';
$formattedHtml = "<!DOCTYPE HTML>\n" .
"<html>\n" .
'<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' . "\n" .
"<body></body>\n" .
"</html>\n";
$subject = new TestingHtmlProcessor($rawHtml);
static::assertSame($formattedHtml, $subject->render());
}
/**
* @return array[]
*/
public function nonHtmlDataProvider()
{
return [
'empty string' => [''],
'null' => [null],
'integer' => [2],
'float' => [3.14159],
'object' => [new \stdClass()],
];
}
/**
* @test
* @expectedException \InvalidArgumentException
*
* @param mixed $html
*
* @dataProvider nonHtmlDataProvider
*/
public function constructorWithNoHtmlDataThrowsException($html)
{
new TestingHtmlProcessor($html);
}
/**
* @return string[][]
*/
public function invalidHtmlDataProvider()
{
return [
'broken nesting gets nested' => ['<b><i></b></i>', '<b><i></i></b>'],
'partial opening tag gets closed' => ['<b', '<b></b>'],
'only opening tag gets closed' => ['<b>', '<b></b>'],
'only closing tag gets removed' => ['foo</b> bar', 'foo bar'],
];
}
/**
* @test
*
* @param string $input
* @param string $expectedHtml
*
* @dataProvider invalidHtmlDataProvider
*/
public function renderRepairsBrokenHtml($input, $expectedHtml)
{
$subject = new TestingHtmlProcessor($input);
$result = $subject->render();
static::assertContains($expectedHtml, $result);
}
/**
* @return string[][]
*/
public function contentWithoutHtmlTagDataProvider()
{
return [
'doctype only' => ['<!DOCTYPE html>'],
'body content only' => ['<p>Hello</p>'],
'HEAD element' => ['<head></head>'],
'BODY element' => ['<body></body>'],
'HEAD AND BODY element' => ['<head></head><body></body>'],
];
}
/**
* @test
*
* @param string $html
*
* @dataProvider contentWithoutHtmlTagDataProvider
*/
public function addsMissingHtmlTag($html)
{
$subject = new TestingHtmlProcessor($html);
$result = $subject->render();
static::assertContains('<html>', $result);
}
/**
* @return string[][]
*/
public function contentWithoutHeadTagDataProvider()
{
return [
'doctype only' => ['<!DOCTYPE html>'],
'body content only' => ['<p>Hello</p>'],
'BODY element' => ['<body></body>'],
];
}
/**
* @test
*
* @param string $html
*
* @dataProvider contentWithoutHeadTagDataProvider
*/
public function addsMissingHeadTag($html)
{
$subject = new TestingHtmlProcessor($html);
$result = $subject->render();
static::assertContains('<head>', $result);
}
/**
* @return string[][]
*/
public function contentWithoutBodyTagDataProvider()
{
return [
'doctype only' => ['<!DOCTYPE html>'],
'HEAD element' => ['<head></head>'],
'body content only' => ['<p>Hello</p>'],
];
}
/**
* @test
*
* @param string $html
*
* @dataProvider contentWithoutBodyTagDataProvider
*/
public function addsMissingBodyTag($html)
{
$subject = new TestingHtmlProcessor($html);
$result = $subject->render();
static::assertContains('<body>', $result);
}
/**
* @test
*/
public function putsMissingBodyElementAroundBodyContent()
{
$subject = new TestingHtmlProcessor('<p>Hello</p>');
$result = $subject->render();
static::assertContains('<body><p>Hello</p></body>', $result);
}
/**
* @return string[][]
*/
public function specialCharactersDataProvider()
{
return [
'template markers with dollar signs & square brackets' => ['$[USER:NAME]$'],
'UTF-8 umlauts' => ['Küss die Hand, schöne Frau.'],
'HTML entities' => ['a &amp; b &gt; c'],
];
}
/**
* @test
*
* @param string $codeNotToBeChanged
*
* @dataProvider specialCharactersDataProvider
*/
public function keepsSpecialCharacters($codeNotToBeChanged)
{
$html = '<html><p>' . $codeNotToBeChanged . '</p></html>';
$subject = new TestingHtmlProcessor($html);
$result = $subject->render();
static::assertContains($codeNotToBeChanged, $result);
}
/**
* @test
*/
public function addMissingHtml5DocumentType()
{
$subject = new TestingHtmlProcessor('<html></html>');
$result = $subject->render();
static::assertContains('<!DOCTYPE html>', $result);
}
/**
* @return string[][]
*/
public function documentTypeDataProvider()
{
return [
'HTML5' => ['<!DOCTYPE html>'],
'XHTML 1.0 strict' => [
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' .
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
],
'XHTML 1.0 transitional' => [
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ' .
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
],
'HTML 4 transitional' => [
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' .
'"http://www.w3.org/TR/REC-html40/loose.dtd">',
],
];
}
/**
* @test
*
* @param string $documentType
*
* @dataProvider documentTypeDataProvider
*/
public function keepsExistingDocumentType($documentType)
{
$html = $documentType . '<html></html>';
$subject = new TestingHtmlProcessor($html);
$result = $subject->render();
static::assertContains($documentType, $result);
}
/**
* @test
*/
public function addsMissingContentTypeMetaTag()
{
$subject = new TestingHtmlProcessor('<p>Hello</p>');
$result = $subject->render();
static::assertContains('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">', $result);
}
/**
* @test
*/
public function notAddsSecondContentTypeMetaTag()
{
$html = '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>';
$subject = new TestingHtmlProcessor($html);
$result = $subject->render();
$numberOfContentTypeMetaTags = \substr_count($result, 'Content-Type');
static::assertSame(1, $numberOfContentTypeMetaTags);
}
/**
* @test
*
* @param string $documentType
*
* @dataProvider documentTypeDataProvider
*/
public function convertsXmlSelfClosingTagsToNonXmlSelfClosingTag($documentType)
{
$subject = new TestingHtmlProcessor($documentType . '<html><body><br/></body></html>');
$result = $subject->render();
static::assertContains('<body><br></body>', $result);
}
/**
* @test
*
* @param string $documentType
*
* @dataProvider documentTypeDataProvider
*/
public function keepsNonXmlSelfClosingTags($documentType)
{
$subject = new TestingHtmlProcessor($documentType . '<html><body><br></body></html>');
$result = $subject->render();
static::assertContains('<body><br></body>', $result);
}
/**
* @test
*/
public function renderBodyContentForEmptyBodyReturnsEmptyString()
{
$subject = new TestingHtmlProcessor('<html><body></body></html>');
$result = $subject->renderBodyContent();
static::assertSame('', $result);
}
/**
* @test
*/
public function renderBodyContentReturnsBodyContent()
{
$bodyContent = '<p>Hello world</p>';
$subject = new TestingHtmlProcessor('<html><body>' . $bodyContent . '</body></html>');
$result = $subject->renderBodyContent();
static::assertSame($bodyContent, $result);
}
/**
* @test
*/
public function getDomDocumentReturnsDomDocument()
{
$subject = new TestingHtmlProcessor('<html></html>');
static::assertInstanceOf(\DOMDocument::class, $subject->getDomDocument());
}
/**
* @test
*/
public function getDomDocumentWithNormalizedHtmlRepresentsTheGivenHtml()
{
$html = "<!DOCTYPE html>\n<html>\n<head>" .
'<meta http-equiv="Content-Type" content="text/html; charset=utf-8">' .
"</head>\n<body>\n<br>\n</body>\n</html>\n";
$subject = new TestingHtmlProcessor($html);
$domDocument = $subject->getDomDocument();
self::assertSame($html, $domDocument->saveHTML());
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace Pelago\Tests\Unit\Emogrifier\HtmlProcessor;
use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
use Pelago\Emogrifier\HtmlProcessor\CssToAttributeConverter;
/**
* Test case.
*
* @author Oliver Klee <github@oliverklee.de>
*/
class CssToAttributeConverterTest extends \PHPUnit_Framework_TestCase
{
/**
* @test
*/
public function classIsAbstractHtmlProcessor()
{
static::assertInstanceOf(AbstractHtmlProcessor::class, new CssToAttributeConverter('<html></html>'));
}
/**
* @test
*/
public function renderWithoutConvertCssToVisualAttributesCallNotAddsVisuablAttributes()
{
$html = '<html style="text-align: right;"></html>';
$subject = new CssToAttributeConverter($html);
static::assertContains('<html style="text-align: right;">', $subject->render());
}
/**
* @test
*/
public function convertCssToVisualAttributesUsesFluentInterface()
{
$html = '<html style="text-align: right;"></html>';
$subject = new CssToAttributeConverter($html);
static::assertSame($subject, $subject->convertCssToVisualAttributes());
}
/**
* @return string[][]
*/
public function matchingCssToHtmlMappingDataProvider()
{
return [
'background-color => bgcolor' => ['<p style="background-color: red;">hi</p>', 'bgcolor="red"'],
'background-color with !important => bgcolor' => [
'<p style="background-color: red !important;">hi</p>',
'bgcolor="red"',
],
'p.text-align => align' => ['<p style="text-align: left;">hi</p>', 'align="left"'],
'div.text-align => align' => ['<div style="text-align: left;">hi</div>', 'align="left"'],
'td.text-align => align' => [
'<table><tr><td style="text-align: left;">hi</td></tr></table>',
'align="left',
],
'text-align: left => align=left' => ['<p style="text-align: left;">hi</p>', 'align="left"'],
'text-align: right => align=right' => ['<p style="text-align: right;">hi</p>', 'align="right"'],
'text-align: center => align=center' => ['<p style="text-align: center;">hi</p>', 'align="center"'],
'text-align: justify => align:justify' => ['<p style="text-align: justify;">hi</p>', 'align="justify"'],
'img.float: right => align=right' => ['<img style="float: right;">', 'align="right"'],
'img.float: left => align=left' => ['<img style="float: left;">', 'align="left"'],
'table.float: right => align=right' => ['<table style="float: right;"></table>', 'align="right"'],
'table.float: left => align=left' => ['<table style="float: left;"></table>', 'align="left"'],
'table.border-spacing: 0 => cellspacing=0' => [
'<table style="border-spacing: 0;"></table>',
'cellspacing="0"',
],
'background => bgcolor' => ['<p style="background: red top;">Bonjour</p>', 'bgcolor="red"'],
'width with px' => ['<p style="width: 100px;">Hi</p>', 'width="100"'],
'width with %' => ['<p style="width: 50%;">Hi</p>', 'width="50%"'],
'height with px' => ['<p style="height: 100px;">Hi</p>', 'height="100"'],
'height with %' => ['<p style="height: 50%;">Hi</p>', 'height="50%"'],
'img.margin: 0 auto (horizontal centering) => align=center' => [
'<img style="margin: 0 auto;">',
'align="center"',
],
'img.margin: auto (horizontal centering) => align=center' => [
'<img style="margin: auto;">',
'align="center"',
],
'img.margin: 10 auto 30 auto (horizontal centering) => align=center' => [
'<img style="margin: 10 auto 30 auto;">',
'align="center"',
],
'table.margin: 0 auto (horizontal centering) => align=center' => [
'<table style="margin: 0 auto;"></table>',
'align="center"',
],
'table.margin: auto (horizontal centering) => align=center' => [
'<table style="margin: auto;"></table>',
'align="center"',
],
'table.margin: 10 auto 30 auto (horizontal centering) => align=center' => [
'<table style="margin: 10 auto 30 auto;"></table>',
'align="center"',
],
'img.border: none => border=0' => ['<img style="border: none;">', 'border="0"'],
'img.border: 0 => border=0' => ['<img style="border: none;">', 'border="0"'],
'table.border: none => border=0' => ['<table style="border: none;"></table>', 'border="0"'],
'table.border: 0 => border=0' => ['<table style="border: 0;"></table>', 'border="0"'],
];
}
/**
* @test
*
* @param string $body The HTML
* @param string $attributes The attributes that are expected on the element
*
* @dataProvider matchingCssToHtmlMappingDataProvider
*/
public function convertCssToVisualAttributesMapsSuitableCssToHtml($body, $attributes)
{
$subject = new CssToAttributeConverter('<html><body>' . $body . '</body></html>');
$subject->convertCssToVisualAttributes();
$html = $subject->render();
static::assertContains($attributes, $html);
}
/**
* @return string[][]
*/
public function notMatchingCssToHtmlMappingDataProvider()
{
return [
'background URL' => ['<p style="background: url(bg.png);">Hello</p>'],
'background URL with position' => ['<p style="background: url(bg.png) top;">Hello</p>'],
'p.margin: 10 5 30 auto (no horizontal centering)' => ['<img style="margin: 10 5 30 auto;">'],
'p.margin: auto' => ['<p style="margin: auto;">Hi</p>'],
'p.border: none' => ['<p style="border: none;">Hi</p>'],
'img.border: 1px solid black' => ['<img style="border: 1px solid black;">'],
'span.text-align' => ['<span style="text-align: justify;">Hi</span>'],
'text-align: inherit' => ['<p style="text-align: inherit;">Hi</p>'],
'span.float' => ['<span style="float: right;">Hi</span>'],
'float: none' => ['<table style="float: none;"></table>'],
'p.border-spacing' => ['<p style="border-spacing: 5px;">Hi</p>'],
'height: auto' => ['<img src="logo.png" alt="" style="height: auto;">'],
'width: auto' => ['<img src="logo.png" alt="" style="width: auto;">'],
];
}
/**
* @test
*
* @param string $body the HTML
*
* @dataProvider notMatchingCssToHtmlMappingDataProvider
*/
public function convertCssToVisualAttributesNotMapsUnsuitableCssToHtml($body)
{
$subject = new CssToAttributeConverter('<html><body>' . $body . '</body></html>');
$subject->convertCssToVisualAttributes();
$html = $subject->render();
static::assertContains($body, $html);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Pelago\Tests\Unit\Emogrifier\HtmlProcessor\Fixtures;
use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
/**
* Fixture class for AbstractHtmlProcessor.
*
* @author Oliver Klee <github@oliverklee.de>
*/
class TestingHtmlProcessor extends AbstractHtmlProcessor
{
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Pelago\Tests\Unit\Emogrifier\HtmlProcessor;
use Pelago\Emogrifier\HtmlProcessor\AbstractHtmlProcessor;
use Pelago\Emogrifier\HtmlProcessor\HtmlNormalizer;
/**
* Test case.
*
* @author Oliver Klee <github@oliverklee.de>
*/
class HtmlNormalizerTest extends \PHPUnit_Framework_TestCase
{
/**
* @test
*/
public function classIsAbstractHtmlProcessor()
{
static::assertInstanceOf(AbstractHtmlProcessor::class, new HtmlNormalizer('<html></html>'));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,333 @@
<?php
namespace Pelago\Tests\Unit\Support\Traits;
use Pelago\Tests\Support\Traits\AssertCss;
/**
* Test case.
*
* @author Jake Hotson <jake.github@qzdesign.co.uk>
*/
class AssertCssTest extends \PHPUnit_Framework_TestCase
{
use AssertCss;
/**
* @test
*/
public function getCssNeedleRegExpEscapesAllSpecialCharacters()
{
$needle = '.\\+*?[^]$(){}=!<>|:-/';
$result = static::getCssNeedleRegExp($needle);
$resultWithWhitespaceMatchersRemoved = \str_replace('\\s*+', '', $result);
static::assertSame(
'/' . \preg_quote($needle, '/') . '/',
$resultWithWhitespaceMatchersRemoved
);
}
/**
* @test
*/
public function getCssNeedleRegExpNotEscapesNonSpecialCharacters()
{
$needle = \implode('', \array_merge(\range('a', 'z'), \range('A', 'Z'), \range('0 ', '9 ')))
. "\r\n\t\"£%&_;'@~,";
$result = static::getCssNeedleRegExp($needle);
$resultWithWhitespaceMatchersRemoved = \str_replace('\\s*+', '', $result);
static::assertSame(
'/' . $needle . '/',
$resultWithWhitespaceMatchersRemoved
);
}
/**
* @return string[][]
*/
public function contentWithOptionalWhitespaceDataProvider()
{
return [
'"{" alone' => ['{', ''],
'"}" alone' => ['}', ''],
'"," alone' => [',', ''],
'"{" with non-special character' => ['{', 'a'],
'"{" with two non-special characters' => ['{', 'a0'],
'"{" with special character' => ['{', '.'],
'"{" with two special characters' => ['{', '.+'],
'"{" with special character and non-special character' => ['{', '.a'],
];
}
/**
* @test
*
* @param string $contentToInsertAround
* @param string $otherContent
*
* @dataProvider contentWithOptionalWhitespaceDataProvider
*/
public function getCssNeedleRegExpInsertsOptionalWhitespace($contentToInsertAround, $otherContent)
{
$result = static::getCssNeedleRegExp($otherContent . $contentToInsertAround . $otherContent);
$quotedOtherContent = \preg_quote($otherContent, '/');
$expectedResult = '/' . $quotedOtherContent . '\\s*+' . \preg_quote($contentToInsertAround, '/') . '\\s*+'
. $quotedOtherContent . '/';
static::assertSame($expectedResult, $result);
}
/**
* @test
*/
public function getCssNeedleRegExpReplacesWhitespaceAtStartWithOptionalWhitespace()
{
$result = static::getCssNeedleRegExp(' a');
static::assertSame('/\\s*+a/', $result);
}
/**
* @return string[][]
*/
public function styleTagDataProvider()
{
return [
'without space after' => ['<style>a'],
'one space after' => ['<style> a'],
'two spaces after' => ['<style> a'],
'linefeed after' => ["<style>\na"],
'Windows line ending after' => ["<style>\r\na"],
];
}
/**
* @test
*
* @param string $needle
*
* @dataProvider styleTagDataProvider
*/
public function getCssNeedleRegExpInsertsOptionalWhitespaceAfterStyleTag($needle)
{
$result = static::getCssNeedleRegExp($needle);
static::assertSame('/\\<style\\>\\s*+a/', $result);
}
/**
* @return string[][]
*/
public function needleFoundDataProvider()
{
$cssStrings = [
'unminified CSS' => 'html, body { color: green; }',
'minified CSS' => 'html,body{color: green;}',
'CSS with extra spaces' => ' html , body { color: green; }',
'CSS with linefeeds' => "\nhtml\n,\nbody\n{\ncolor: green;\n}",
'CSS with Windows line endings' => "\r\nhtml\r\n,\r\nbody\r\n{\r\ncolor: green;\r\n}",
];
$datasets = [];
foreach ($cssStrings as $needleDescription => $needle) {
foreach ($cssStrings as $haystackDescription => $haystack) {
$description = $needleDescription . ' in ' . $haystackDescription;
$datasets[$description] = [$needle, $haystack];
$datasets[$description . ' in <style> tag'] = [
'<style>' . $needle . '</style>',
'<style>' . $haystack . '</style>',
];
}
}
return $datasets;
}
/**
* @return string[][]
*/
public function needleNotFoundDataProvider()
{
return [
'CSS part with "{" not in CSS' => ['p {', 'body { color: green; }'],
'CSS part with "}" not in CSS' => ['color: red; }', 'body { color: green; }'],
'CSS part with "," not in CSS' => ['html, body', 'body { color: green; }'],
];
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleFoundDataProvider
*/
public function assertContainsCssPassesTestIfNeedleFound($needle, $haystack)
{
static::assertContainsCss($needle, $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleNotFoundDataProvider
*
* @expectedException \PHPUnit_Framework_ExpectationFailedException
*/
public function assertContainsCssFailsTestIfNeedleNotFound($needle, $haystack)
{
static::assertContainsCss($needle, $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleNotFoundDataProvider
*/
public function assertNotContainsCssPassesTestIfNeedleNotFound($needle, $haystack)
{
static::assertNotContainsCss($needle, $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleFoundDataProvider
*
* @expectedException \PHPUnit_Framework_ExpectationFailedException
*/
public function assertNotContainsCssFailsTestIfNeedleFound($needle, $haystack)
{
static::assertNotContainsCss($needle, $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleNotFoundDataProvider
*/
public function assertContainsCssCountPassesTestExpectingZeroIfNeedleNotFound($needle, $haystack)
{
static::assertContainsCssCount(0, $needle, $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleFoundDataProvider
*
* @expectedException \PHPUnit_Framework_ExpectationFailedException
*/
public function assertContainsCssCountFailsTestExpectingZeroIfNeedleFound($needle, $haystack)
{
static::assertContainsCssCount(0, $needle, $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleFoundDataProvider
*/
public function assertContainsCssCountPassesTestExpectingOneIfNeedleFound($needle, $haystack)
{
static::assertContainsCssCount(1, $needle, $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleNotFoundDataProvider
*
* @expectedException \PHPUnit_Framework_ExpectationFailedException
*/
public function assertContainsCssCountFailsTestExpectingOneIfNeedleNotFound($needle, $haystack)
{
static::assertContainsCssCount(1, $needle, $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleFoundDataProvider
*
* @expectedException \PHPUnit_Framework_ExpectationFailedException
*/
public function assertContainsCssCountFailsTestExpectingOneIfNeedleFoundTwice($needle, $haystack)
{
static::assertContainsCssCount(1, $needle, $haystack . $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleFoundDataProvider
*/
public function assertContainsCssCountPassesTestExpectingTwoIfNeedleFoundTwice($needle, $haystack)
{
static::assertContainsCssCount(2, $needle, $haystack . $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleNotFoundDataProvider
*
* @expectedException \PHPUnit_Framework_ExpectationFailedException
*/
public function assertContainsCssCountFailsTestExpectingTwoIfNeedleNotFound($needle, $haystack)
{
static::assertContainsCssCount(2, $needle, $haystack);
}
/**
* @test
*
* @param string $needle
* @param string $haystack
*
* @dataProvider needleFoundDataProvider
*
* @expectedException \PHPUnit_Framework_ExpectationFailedException
*/
public function assertContainsCssCountFailsTestExpectingTwoIfNeedleFoundOnlyOnce($needle, $haystack)
{
static::assertContainsCssCount(2, $needle, $haystack);
}
}

3
lib/symfony/css-selector/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@@ -0,0 +1,13 @@
CHANGELOG
=========
2.8.0
-----
* Added the `CssSelectorConverter` class as a non-static API for the component.
* Deprecated the `CssSelector` static API of the component.
2.1.0
-----
* none

View File

@@ -0,0 +1,65 @@
<?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;
use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser;
use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser;
use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser;
use Symfony\Component\CssSelector\Parser\Shortcut\HashParser;
use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension;
use Symfony\Component\CssSelector\XPath\Translator;
/**
* CssSelectorConverter is the main entry point of the component and can convert CSS
* selectors to XPath expressions.
*
* @author Christophe Coevoet <stof@notk.org>
*/
class CssSelectorConverter
{
private $translator;
/**
* @param bool $html Whether HTML support should be enabled. Disable it for XML documents
*/
public function __construct($html = true)
{
$this->translator = new Translator();
if ($html) {
$this->translator->registerExtension(new HtmlExtension($this->translator));
}
$this->translator
->registerParserShortcut(new EmptyStringParser())
->registerParserShortcut(new ElementParser())
->registerParserShortcut(new ClassParser())
->registerParserShortcut(new HashParser())
;
}
/**
* Translates a CSS expression to its XPath equivalent.
*
* Optionally, a prefix can be added to the resulting XPath
* expression with the $prefix parameter.
*
* @param string $cssExpr The CSS expression
* @param string $prefix An optional prefix for the XPath expression
*
* @return string
*/
public function toXPath($cssExpr, $prefix = 'descendant-or-self::')
{
return $this->translator->cssToXPath($cssExpr, $prefix);
}
}

View File

@@ -0,0 +1,24 @@
<?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\Exception;
/**
* Interface for exceptions.
*
* 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>
*/
interface ExceptionInterface
{
}

View File

@@ -0,0 +1,24 @@
<?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\Exception;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* 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>
*/
class ExpressionErrorException extends ParseException
{
}

View File

@@ -0,0 +1,24 @@
<?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\Exception;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* 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>
*/
class InternalErrorException extends ParseException
{
}

View File

@@ -0,0 +1,24 @@
<?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\Exception;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ParseException extends \Exception implements ExceptionInterface
{
}

View File

@@ -0,0 +1,73 @@
<?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\Exception;
use Symfony\Component\CssSelector\Parser\Token;
/**
* ParseException is thrown when a CSS selector syntax is not valid.
*
* 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>
*/
class SyntaxErrorException extends ParseException
{
/**
* @param string $expectedValue
* @param Token $foundToken
*
* @return self
*/
public static function unexpectedToken($expectedValue, Token $foundToken)
{
return new self(sprintf('Expected %s, but %s found.', $expectedValue, $foundToken));
}
/**
* @param string $pseudoElement
* @param string $unexpectedLocation
*
* @return self
*/
public static function pseudoElementFound($pseudoElement, $unexpectedLocation)
{
return new self(sprintf('Unexpected pseudo-element "::%s" found %s.', $pseudoElement, $unexpectedLocation));
}
/**
* @param int $position
*
* @return self
*/
public static function unclosedString($position)
{
return new self(sprintf('Unclosed/invalid string at %s.', $position));
}
/**
* @return self
*/
public static function nestedNot()
{
return new self('Got nested ::not().');
}
/**
* @return self
*/
public static function stringAsFunctionArgument()
{
return new self('String not allowed as function argument.');
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,42 @@
<?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\Node;
/**
* Abstract base node class.
*
* 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
*/
abstract class AbstractNode implements NodeInterface
{
/**
* @var string
*/
private $nodeName;
/**
* @return string
*/
public function getNodeName()
{
if (null === $this->nodeName) {
$this->nodeName = preg_replace('~.*\\\\([^\\\\]+)Node$~', '$1', \get_called_class());
}
return $this->nodeName;
}
}

View File

@@ -0,0 +1,107 @@
<?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\Node;
/**
* Represents a "<selector>[<namespace>|<attribute> <operator> <value>]" node.
*
* 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 AttributeNode extends AbstractNode
{
private $selector;
private $namespace;
private $attribute;
private $operator;
private $value;
/**
* @param NodeInterface $selector
* @param string $namespace
* @param string $attribute
* @param string $operator
* @param string $value
*/
public function __construct(NodeInterface $selector, $namespace, $attribute, $operator, $value)
{
$this->selector = $selector;
$this->namespace = $namespace;
$this->attribute = $attribute;
$this->operator = $operator;
$this->value = $value;
}
/**
* @return NodeInterface
*/
public function getSelector()
{
return $this->selector;
}
/**
* @return string
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* @return string
*/
public function getAttribute()
{
return $this->attribute;
}
/**
* @return string
*/
public function getOperator()
{
return $this->operator;
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* {@inheritdoc}
*/
public function getSpecificity()
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
/**
* {@inheritdoc}
*/
public function __toString()
{
$attribute = $this->namespace ? $this->namespace.'|'.$this->attribute : $this->attribute;
return 'exists' === $this->operator
? sprintf('%s[%s[%s]]', $this->getNodeName(), $this->selector, $attribute)
: sprintf("%s[%s[%s %s '%s']]", $this->getNodeName(), $this->selector, $attribute, $this->operator, $this->value);
}
}

View File

@@ -0,0 +1,70 @@
<?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\Node;
/**
* Represents a "<selector>.<name>" node.
*
* 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 ClassNode extends AbstractNode
{
private $selector;
private $name;
/**
* @param NodeInterface $selector
* @param string $name
*/
public function __construct(NodeInterface $selector, $name)
{
$this->selector = $selector;
$this->name = $name;
}
/**
* @return NodeInterface
*/
public function getSelector()
{
return $this->selector;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getSpecificity()
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name);
}
}

View File

@@ -0,0 +1,83 @@
<?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\Node;
/**
* Represents a combined node.
*
* 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 CombinedSelectorNode extends AbstractNode
{
private $selector;
private $combinator;
private $subSelector;
/**
* @param NodeInterface $selector
* @param string $combinator
* @param NodeInterface $subSelector
*/
public function __construct(NodeInterface $selector, $combinator, NodeInterface $subSelector)
{
$this->selector = $selector;
$this->combinator = $combinator;
$this->subSelector = $subSelector;
}
/**
* @return NodeInterface
*/
public function getSelector()
{
return $this->selector;
}
/**
* @return string
*/
public function getCombinator()
{
return $this->combinator;
}
/**
* @return NodeInterface
*/
public function getSubSelector()
{
return $this->subSelector;
}
/**
* {@inheritdoc}
*/
public function getSpecificity()
{
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
/**
* {@inheritdoc}
*/
public function __toString()
{
$combinator = ' ' === $this->combinator ? '<followed>' : $this->combinator;
return sprintf('%s[%s %s %s]', $this->getNodeName(), $this->selector, $combinator, $this->subSelector);
}
}

View File

@@ -0,0 +1,72 @@
<?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\Node;
/**
* Represents a "<namespace>|<element>" node.
*
* 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 ElementNode extends AbstractNode
{
private $namespace;
private $element;
/**
* @param string|null $namespace
* @param string|null $element
*/
public function __construct($namespace = null, $element = null)
{
$this->namespace = $namespace;
$this->element = $element;
}
/**
* @return string|null
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* @return string|null
*/
public function getElement()
{
return $this->element;
}
/**
* {@inheritdoc}
*/
public function getSpecificity()
{
return new Specificity(0, 0, $this->element ? 1 : 0);
}
/**
* {@inheritdoc}
*/
public function __toString()
{
$element = $this->element ?: '*';
return sprintf('%s[%s]', $this->getNodeName(), $this->namespace ? $this->namespace.'|'.$element : $element);
}
}

View File

@@ -0,0 +1,87 @@
<?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\Node;
use Symfony\Component\CssSelector\Parser\Token;
/**
* Represents a "<selector>:<name>(<arguments>)" node.
*
* 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 FunctionNode extends AbstractNode
{
private $selector;
private $name;
private $arguments;
/**
* @param NodeInterface $selector
* @param string $name
* @param Token[] $arguments
*/
public function __construct(NodeInterface $selector, $name, array $arguments = [])
{
$this->selector = $selector;
$this->name = strtolower($name);
$this->arguments = $arguments;
}
/**
* @return NodeInterface
*/
public function getSelector()
{
return $this->selector;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return Token[]
*/
public function getArguments()
{
return $this->arguments;
}
/**
* {@inheritdoc}
*/
public function getSpecificity()
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
/**
* {@inheritdoc}
*/
public function __toString()
{
$arguments = implode(', ', array_map(function (Token $token) {
return "'".$token->getValue()."'";
}, $this->arguments));
return sprintf('%s[%s:%s(%s)]', $this->getNodeName(), $this->selector, $this->name, $arguments ? '['.$arguments.']' : '');
}
}

View File

@@ -0,0 +1,70 @@
<?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\Node;
/**
* Represents a "<selector>#<id>" node.
*
* 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 HashNode extends AbstractNode
{
private $selector;
private $id;
/**
* @param NodeInterface $selector
* @param string $id
*/
public function __construct(NodeInterface $selector, $id)
{
$this->selector = $selector;
$this->id = $id;
}
/**
* @return NodeInterface
*/
public function getSelector()
{
return $this->selector;
}
/**
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* {@inheritdoc}
*/
public function getSpecificity()
{
return $this->selector->getSpecificity()->plus(new Specificity(1, 0, 0));
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return sprintf('%s[%s#%s]', $this->getNodeName(), $this->selector, $this->id);
}
}

View File

@@ -0,0 +1,66 @@
<?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\Node;
/**
* Represents a "<selector>:not(<identifier>)" node.
*
* 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 NegationNode extends AbstractNode
{
private $selector;
private $subSelector;
public function __construct(NodeInterface $selector, NodeInterface $subSelector)
{
$this->selector = $selector;
$this->subSelector = $subSelector;
}
/**
* @return NodeInterface
*/
public function getSelector()
{
return $this->selector;
}
/**
* @return NodeInterface
*/
public function getSubSelector()
{
return $this->subSelector;
}
/**
* {@inheritdoc}
*/
public function getSpecificity()
{
return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity());
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return sprintf('%s[%s:not(%s)]', $this->getNodeName(), $this->selector, $this->subSelector);
}
}

View File

@@ -0,0 +1,46 @@
<?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\Node;
/**
* Interface for nodes.
*
* 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
*/
interface NodeInterface
{
/**
* Returns node's name.
*
* @return string
*/
public function getNodeName();
/**
* Returns node's specificity.
*
* @return Specificity
*/
public function getSpecificity();
/**
* Returns node's string representation.
*
* @return string
*/
public function __toString();
}

View File

@@ -0,0 +1,70 @@
<?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\Node;
/**
* Represents a "<selector>:<identifier>" node.
*
* 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 PseudoNode extends AbstractNode
{
private $selector;
private $identifier;
/**
* @param NodeInterface $selector
* @param string $identifier
*/
public function __construct(NodeInterface $selector, $identifier)
{
$this->selector = $selector;
$this->identifier = strtolower($identifier);
}
/**
* @return NodeInterface
*/
public function getSelector()
{
return $this->selector;
}
/**
* @return string
*/
public function getIdentifier()
{
return $this->identifier;
}
/**
* {@inheritdoc}
*/
public function getSpecificity()
{
return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0));
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return sprintf('%s[%s:%s]', $this->getNodeName(), $this->selector, $this->identifier);
}
}

View File

@@ -0,0 +1,70 @@
<?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\Node;
/**
* Represents a "<selector>(::|:)<pseudoElement>" node.
*
* 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 SelectorNode extends AbstractNode
{
private $tree;
private $pseudoElement;
/**
* @param NodeInterface $tree
* @param string|null $pseudoElement
*/
public function __construct(NodeInterface $tree, $pseudoElement = null)
{
$this->tree = $tree;
$this->pseudoElement = $pseudoElement ? strtolower($pseudoElement) : null;
}
/**
* @return NodeInterface
*/
public function getTree()
{
return $this->tree;
}
/**
* @return string|null
*/
public function getPseudoElement()
{
return $this->pseudoElement;
}
/**
* {@inheritdoc}
*/
public function getSpecificity()
{
return $this->tree->getSpecificity()->plus(new Specificity(0, 0, $this->pseudoElement ? 1 : 0));
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return sprintf('%s[%s%s]', $this->getNodeName(), $this->tree, $this->pseudoElement ? '::'.$this->pseudoElement : '');
}
}

View File

@@ -0,0 +1,88 @@
<?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\Node;
/**
* Represents a node specificity.
*
* This component is a port of the Python cssselect library,
* which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect.
*
* @see http://www.w3.org/TR/selectors/#specificity
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*
* @internal
*/
class Specificity
{
const A_FACTOR = 100;
const B_FACTOR = 10;
const C_FACTOR = 1;
private $a;
private $b;
private $c;
/**
* @param int $a
* @param int $b
* @param int $c
*/
public function __construct($a, $b, $c)
{
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
/**
* @return self
*/
public function plus(self $specificity)
{
return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c);
}
/**
* Returns global specificity value.
*
* @return int
*/
public function getValue()
{
return $this->a * self::A_FACTOR + $this->b * self::B_FACTOR + $this->c * self::C_FACTOR;
}
/**
* Returns -1 if the object specificity is lower than the argument,
* 0 if they are equal, and 1 if the argument is lower.
*
* @return int
*/
public function compareTo(self $specificity)
{
if ($this->a !== $specificity->a) {
return $this->a > $specificity->a ? 1 : -1;
}
if ($this->b !== $specificity->b) {
return $this->b > $specificity->b ? 1 : -1;
}
if ($this->c !== $specificity->c) {
return $this->c > $specificity->c ? 1 : -1;
}
return 0;
}
}

View File

@@ -0,0 +1,48 @@
<?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\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* 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 CommentHandler implements HandlerInterface
{
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
{
if ('/*' !== $reader->getSubstring(2)) {
return false;
}
$offset = $reader->getOffset('*/');
if (false === $offset) {
$reader->moveToEnd();
} else {
$reader->moveForward($offset + 2);
}
return true;
}
}

View File

@@ -0,0 +1,33 @@
<?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\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector handler 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
*/
interface HandlerInterface
{
/**
* @return bool
*/
public function handle(Reader $reader, TokenStream $stream);
}

View File

@@ -0,0 +1,58 @@
<?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\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* 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 HashHandler implements HandlerInterface
{
private $patterns;
private $escaping;
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
{
$this->patterns = $patterns;
$this->escaping = $escaping;
}
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
{
$match = $reader->findPattern($this->patterns->getHashPattern());
if (!$match) {
return false;
}
$value = $this->escaping->escapeUnicode($match[1]);
$stream->push(new Token(Token::TYPE_HASH, $value, $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}

View File

@@ -0,0 +1,58 @@
<?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\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* 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 IdentifierHandler implements HandlerInterface
{
private $patterns;
private $escaping;
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
{
$this->patterns = $patterns;
$this->escaping = $escaping;
}
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
{
$match = $reader->findPattern($this->patterns->getIdentifierPattern());
if (!$match) {
return false;
}
$value = $this->escaping->escapeUnicode($match[0]);
$stream->push(new Token(Token::TYPE_IDENTIFIER, $value, $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}

View File

@@ -0,0 +1,54 @@
<?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\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* 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 NumberHandler implements HandlerInterface
{
private $patterns;
public function __construct(TokenizerPatterns $patterns)
{
$this->patterns = $patterns;
}
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
{
$match = $reader->findPattern($this->patterns->getNumberPattern());
if (!$match) {
return false;
}
$stream->push(new Token(Token::TYPE_NUMBER, $match[0], $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}

View File

@@ -0,0 +1,77 @@
<?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\Handler;
use Symfony\Component\CssSelector\Exception\InternalErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector comment handler.
*
* 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 StringHandler implements HandlerInterface
{
private $patterns;
private $escaping;
public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping)
{
$this->patterns = $patterns;
$this->escaping = $escaping;
}
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
{
$quote = $reader->getSubstring(1);
if (!\in_array($quote, ["'", '"'])) {
return false;
}
$reader->moveForward(1);
$match = $reader->findPattern($this->patterns->getQuotedStringPattern($quote));
if (!$match) {
throw new InternalErrorException(sprintf('Should have found at least an empty match at %s.', $reader->getPosition()));
}
// check unclosed strings
if (\strlen($match[0]) === $reader->getRemainingLength()) {
throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
}
// check quotes pairs validity
if ($quote !== $reader->getSubstring(1, \strlen($match[0]))) {
throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
}
$string = $this->escaping->escapeUnicodeAndNewLine($match[0]);
$stream->push(new Token(Token::TYPE_STRING, $string, $reader->getPosition()));
$reader->moveForward(\strlen($match[0]) + 1);
return true;
}
}

View File

@@ -0,0 +1,46 @@
<?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\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector whitespace handler.
*
* 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 WhitespaceHandler implements HandlerInterface
{
/**
* {@inheritdoc}
*/
public function handle(Reader $reader, TokenStream $stream)
{
$match = $reader->findPattern('~^[ \t\r\n\f]+~');
if (false === $match) {
return false;
}
$stream->push(new Token(Token::TYPE_WHITESPACE, $match[0], $reader->getPosition()));
$reader->moveForward(\strlen($match[0]));
return true;
}
}

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 [2, 1];
case 'even' === $joined:
return [2, 0];
case 'n' === $joined:
return [1, 0];
case false === strpos($joined, 'n'):
return [0, $int($joined)];
}
$split = explode('n', $joined);
$first = isset($split[0]) ? $split[0] : null;
return [
$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 = [];
while (true) {
$selectors[] = $this->parserSelectorNode($stream);
if ($stream->getPeek()->isDelimiter([','])) {
$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([','])) {
break;
}
if (null !== $pseudoElement) {
throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector');
}
if ($peek->isDelimiter(['+', '>', '~'])) {
$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([',', '+', '>', '~'])
|| ($insideNegation && $peek->isDelimiter([')']))
) {
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(['.'])) {
$stream->getNext();
$result = new Node\ClassNode($result, $stream->getNextIdentifier());
} elseif ($peek->isDelimiter(['['])) {
$stream->getNext();
$result = $this->parseAttributeNode($result, $stream);
} elseif ($peek->isDelimiter([':'])) {
$stream->getNext();
if ($stream->getPeek()->isDelimiter([':'])) {
$stream->getNext();
$pseudoElement = $stream->getNextIdentifier();
continue;
}
$identifier = $stream->getNextIdentifier();
if (\in_array(strtolower($identifier), ['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(['('])) {
$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([')'])) {
throw SyntaxErrorException::unexpectedToken('")"', $next);
}
$result = new Node\NegationNode($result, $argument);
} else {
$arguments = [];
$next = null;
while (true) {
$stream->skipWhitespace();
$next = $stream->getNext();
if ($next->isIdentifier()
|| $next->isString()
|| $next->isNumber()
|| $next->isDelimiter(['+', '-'])
) {
$arguments[] = $next;
} elseif ($next->isDelimiter([')'])) {
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 [$result, $pseudoElement];
}
/**
* Parses next element node.
*
* @return Node\ElementNode
*/
private function parseElementNode(TokenStream $stream)
{
$peek = $stream->getPeek();
if ($peek->isIdentifier() || $peek->isDelimiter(['*'])) {
if ($peek->isIdentifier()) {
$namespace = $stream->getNext()->getValue();
} else {
$stream->getNext();
$namespace = null;
}
if ($stream->getPeek()->isDelimiter(['|'])) {
$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(['|'])) {
throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek());
}
if ($stream->getPeek()->isDelimiter(['|'])) {
$stream->getNext();
if ($stream->getPeek()->isDelimiter(['='])) {
$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([']'])) {
return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null);
} elseif ($next->isDelimiter(['='])) {
$operator = '=';
} elseif ($next->isDelimiter(['^', '$', '*', '~', '|', '!'])
&& $stream->getPeek()->isDelimiter(['='])
) {
$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([']'])) {
throw SyntaxErrorException::unexpectedToken('"]"', $next);
}
return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue());
}
}

View File

@@ -0,0 +1,36 @@
<?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\Node\SelectorNode;
/**
* CSS selector parser 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
*/
interface ParserInterface
{
/**
* Parses given selector source into an array of tokens.
*
* @param string $source
*
* @return SelectorNode[]
*/
public function parse($source);
}

View File

@@ -0,0 +1,114 @@
<?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;
/**
* CSS selector reader.
*
* 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 Reader
{
private $source;
private $length;
private $position = 0;
/**
* @param string $source
*/
public function __construct($source)
{
$this->source = $source;
$this->length = \strlen($source);
}
/**
* @return bool
*/
public function isEOF()
{
return $this->position >= $this->length;
}
/**
* @return int
*/
public function getPosition()
{
return $this->position;
}
/**
* @return int
*/
public function getRemainingLength()
{
return $this->length - $this->position;
}
/**
* @param int $length
* @param int $offset
*
* @return string
*/
public function getSubstring($length, $offset = 0)
{
return substr($this->source, $this->position + $offset, $length);
}
/**
* @param string $string
*
* @return int
*/
public function getOffset($string)
{
$position = strpos($this->source, $string, $this->position);
return false === $position ? false : $position - $this->position;
}
/**
* @param string $pattern
*
* @return array|false
*/
public function findPattern($pattern)
{
$source = substr($this->source, $this->position);
if (preg_match($pattern, $source, $matches)) {
return $matches;
}
return false;
}
/**
* @param int $length
*/
public function moveForward($length)
{
$this->position += $length;
}
public function moveToEnd()
{
$this->position = $this->length;
}
}

View File

@@ -0,0 +1,51 @@
<?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\Shortcut;
use Symfony\Component\CssSelector\Node\ClassNode;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector class parser shortcut.
*
* 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 ClassParser implements ParserInterface
{
/**
* {@inheritdoc}
*/
public function parse($source)
{
// Matches an optional namespace, optional element, and required class
// $source = 'test|input.ab6bd_field';
// $matches = array (size=4)
// 0 => string 'test|input.ab6bd_field' (length=22)
// 1 => string 'test' (length=4)
// 2 => string 'input' (length=5)
// 3 => string 'ab6bd_field' (length=11)
if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+\.([\w-]++)$/i', trim($source), $matches)) {
return [
new SelectorNode(new ClassNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])),
];
}
return [];
}
}

View File

@@ -0,0 +1,47 @@
<?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\Shortcut;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector element parser shortcut.
*
* 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 ElementParser implements ParserInterface
{
/**
* {@inheritdoc}
*/
public function parse($source)
{
// Matches an optional namespace, required element or `*`
// $source = 'testns|testel';
// $matches = array (size=3)
// 0 => string 'testns|testel' (length=13)
// 1 => string 'testns' (length=6)
// 2 => string 'testel' (length=6)
if (preg_match('/^(?:([a-z]++)\|)?([\w-]++|\*)$/i', trim($source), $matches)) {
return [new SelectorNode(new ElementNode($matches[1] ?: null, $matches[2]))];
}
return [];
}
}

View File

@@ -0,0 +1,46 @@
<?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\Shortcut;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector class parser shortcut.
*
* This shortcut ensure compatibility with previous version.
* - The parser fails to parse an empty string.
* - In the previous version, an empty string matches each tags.
*
* 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 EmptyStringParser implements ParserInterface
{
/**
* {@inheritdoc}
*/
public function parse($source)
{
// Matches an empty string
if ('' == $source) {
return [new SelectorNode(new ElementNode(null, '*'))];
}
return [];
}
}

View File

@@ -0,0 +1,51 @@
<?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\Shortcut;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\HashNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
use Symfony\Component\CssSelector\Parser\ParserInterface;
/**
* CSS selector hash parser shortcut.
*
* 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 HashParser implements ParserInterface
{
/**
* {@inheritdoc}
*/
public function parse($source)
{
// Matches an optional namespace, optional element, and required id
// $source = 'test|input#ab6bd_field';
// $matches = array (size=4)
// 0 => string 'test|input#ab6bd_field' (length=22)
// 1 => string 'test' (length=4)
// 2 => string 'input' (length=5)
// 3 => string 'ab6bd_field' (length=11)
if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+#([\w-]++)$/i', trim($source), $matches)) {
return [
new SelectorNode(new HashNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])),
];
}
return [];
}
}

View File

@@ -0,0 +1,149 @@
<?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;
/**
* CSS selector token.
*
* 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 Token
{
const TYPE_FILE_END = 'eof';
const TYPE_DELIMITER = 'delimiter';
const TYPE_WHITESPACE = 'whitespace';
const TYPE_IDENTIFIER = 'identifier';
const TYPE_HASH = 'hash';
const TYPE_NUMBER = 'number';
const TYPE_STRING = 'string';
private $type;
private $value;
private $position;
/**
* @param int $type
* @param string $value
* @param int $position
*/
public function __construct($type, $value, $position)
{
$this->type = $type;
$this->value = $value;
$this->position = $position;
}
/**
* @return int
*/
public function getType()
{
return $this->type;
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* @return int
*/
public function getPosition()
{
return $this->position;
}
/**
* @return bool
*/
public function isFileEnd()
{
return self::TYPE_FILE_END === $this->type;
}
/**
* @return bool
*/
public function isDelimiter(array $values = [])
{
if (self::TYPE_DELIMITER !== $this->type) {
return false;
}
if (empty($values)) {
return true;
}
return \in_array($this->value, $values);
}
/**
* @return bool
*/
public function isWhitespace()
{
return self::TYPE_WHITESPACE === $this->type;
}
/**
* @return bool
*/
public function isIdentifier()
{
return self::TYPE_IDENTIFIER === $this->type;
}
/**
* @return bool
*/
public function isHash()
{
return self::TYPE_HASH === $this->type;
}
/**
* @return bool
*/
public function isNumber()
{
return self::TYPE_NUMBER === $this->type;
}
/**
* @return bool
*/
public function isString()
{
return self::TYPE_STRING === $this->type;
}
/**
* @return string
*/
public function __toString()
{
if ($this->value) {
return sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position);
}
return sprintf('<%s at %s>', $this->type, $this->position);
}
}

View File

@@ -0,0 +1,175 @@
<?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\InternalErrorException;
use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
/**
* CSS selector token stream.
*
* 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 TokenStream
{
/**
* @var Token[]
*/
private $tokens = [];
/**
* @var Token[]
*/
private $used = [];
/**
* @var int
*/
private $cursor = 0;
/**
* @var Token|null
*/
private $peeked;
/**
* @var bool
*/
private $peeking = false;
/**
* Pushes a token.
*
* @return $this
*/
public function push(Token $token)
{
$this->tokens[] = $token;
return $this;
}
/**
* Freezes stream.
*
* @return $this
*/
public function freeze()
{
return $this;
}
/**
* Returns next token.
*
* @return Token
*
* @throws InternalErrorException If there is no more token
*/
public function getNext()
{
if ($this->peeking) {
$this->peeking = false;
$this->used[] = $this->peeked;
return $this->peeked;
}
if (!isset($this->tokens[$this->cursor])) {
throw new InternalErrorException('Unexpected token stream end.');
}
return $this->tokens[$this->cursor++];
}
/**
* Returns peeked token.
*
* @return Token
*/
public function getPeek()
{
if (!$this->peeking) {
$this->peeked = $this->getNext();
$this->peeking = true;
}
return $this->peeked;
}
/**
* Returns used tokens.
*
* @return Token[]
*/
public function getUsed()
{
return $this->used;
}
/**
* Returns nex identifier token.
*
* @return string The identifier token value
*
* @throws SyntaxErrorException If next token is not an identifier
*/
public function getNextIdentifier()
{
$next = $this->getNext();
if (!$next->isIdentifier()) {
throw SyntaxErrorException::unexpectedToken('identifier', $next);
}
return $next->getValue();
}
/**
* Returns nex identifier or star delimiter token.
*
* @return string|null The identifier token value or null if star found
*
* @throws SyntaxErrorException If next token is not an identifier or a star delimiter
*/
public function getNextIdentifierOrStar()
{
$next = $this->getNext();
if ($next->isIdentifier()) {
return $next->getValue();
}
if ($next->isDelimiter(['*'])) {
return;
}
throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next);
}
/**
* Skips next whitespace if any.
*/
public function skipWhitespace()
{
$peek = $this->getPeek();
if ($peek->isWhitespace()) {
$this->getNext();
}
}
}

View File

@@ -0,0 +1,75 @@
<?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\Tokenizer;
use Symfony\Component\CssSelector\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* CSS selector tokenizer.
*
* 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 Tokenizer
{
/**
* @var Handler\HandlerInterface[]
*/
private $handlers;
public function __construct()
{
$patterns = new TokenizerPatterns();
$escaping = new TokenizerEscaping($patterns);
$this->handlers = [
new Handler\WhitespaceHandler(),
new Handler\IdentifierHandler($patterns, $escaping),
new Handler\HashHandler($patterns, $escaping),
new Handler\StringHandler($patterns, $escaping),
new Handler\NumberHandler($patterns),
new Handler\CommentHandler(),
];
}
/**
* Tokenize selector source code.
*
* @return TokenStream
*/
public function tokenize(Reader $reader)
{
$stream = new TokenStream();
while (!$reader->isEOF()) {
foreach ($this->handlers as $handler) {
if ($handler->handle($reader, $stream)) {
continue 2;
}
}
$stream->push(new Token(Token::TYPE_DELIMITER, $reader->getSubstring(1), $reader->getPosition()));
$reader->moveForward(1);
}
return $stream
->push(new Token(Token::TYPE_FILE_END, null, $reader->getPosition()))
->freeze();
}
}

View File

@@ -0,0 +1,78 @@
<?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\Tokenizer;
/**
* CSS selector tokenizer escaping applier.
*
* 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 TokenizerEscaping
{
private $patterns;
public function __construct(TokenizerPatterns $patterns)
{
$this->patterns = $patterns;
}
/**
* @param string $value
*
* @return string
*/
public function escapeUnicode($value)
{
$value = $this->replaceUnicodeSequences($value);
return preg_replace($this->patterns->getSimpleEscapePattern(), '$1', $value);
}
/**
* @param string $value
*
* @return string
*/
public function escapeUnicodeAndNewLine($value)
{
$value = preg_replace($this->patterns->getNewLineEscapePattern(), '', $value);
return $this->escapeUnicode($value);
}
/**
* @param string $value
*
* @return string
*/
private function replaceUnicodeSequences($value)
{
return preg_replace_callback($this->patterns->getUnicodeEscapePattern(), function ($match) {
$c = hexdec($match[1]);
if (0x80 > $c %= 0x200000) {
return \chr($c);
}
if (0x800 > $c) {
return \chr(0xC0 | $c >> 6).\chr(0x80 | $c & 0x3F);
}
if (0x10000 > $c) {
return \chr(0xE0 | $c >> 12).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F);
}
}, $value);
}
}

View File

@@ -0,0 +1,112 @@
<?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\Tokenizer;
/**
* CSS selector tokenizer patterns builder.
*
* 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 TokenizerPatterns
{
private $unicodeEscapePattern;
private $simpleEscapePattern;
private $newLineEscapePattern;
private $escapePattern;
private $stringEscapePattern;
private $nonAsciiPattern;
private $nmCharPattern;
private $nmStartPattern;
private $identifierPattern;
private $hashPattern;
private $numberPattern;
private $quotedStringPattern;
public function __construct()
{
$this->unicodeEscapePattern = '\\\\([0-9a-f]{1,6})(?:\r\n|[ \n\r\t\f])?';
$this->simpleEscapePattern = '\\\\(.)';
$this->newLineEscapePattern = '\\\\(?:\n|\r\n|\r|\f)';
$this->escapePattern = $this->unicodeEscapePattern.'|\\\\[^\n\r\f0-9a-f]';
$this->stringEscapePattern = $this->newLineEscapePattern.'|'.$this->escapePattern;
$this->nonAsciiPattern = '[^\x00-\x7F]';
$this->nmCharPattern = '[_a-z0-9-]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
$this->nmStartPattern = '[_a-z]|'.$this->escapePattern.'|'.$this->nonAsciiPattern;
$this->identifierPattern = '-?(?:'.$this->nmStartPattern.')(?:'.$this->nmCharPattern.')*';
$this->hashPattern = '#((?:'.$this->nmCharPattern.')+)';
$this->numberPattern = '[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)';
$this->quotedStringPattern = '([^\n\r\f%s]|'.$this->stringEscapePattern.')*';
}
/**
* @return string
*/
public function getNewLineEscapePattern()
{
return '~^'.$this->newLineEscapePattern.'~';
}
/**
* @return string
*/
public function getSimpleEscapePattern()
{
return '~^'.$this->simpleEscapePattern.'~';
}
/**
* @return string
*/
public function getUnicodeEscapePattern()
{
return '~^'.$this->unicodeEscapePattern.'~i';
}
/**
* @return string
*/
public function getIdentifierPattern()
{
return '~^'.$this->identifierPattern.'~i';
}
/**
* @return string
*/
public function getHashPattern()
{
return '~^'.$this->hashPattern.'~i';
}
/**
* @return string
*/
public function getNumberPattern()
{
return '~^'.$this->numberPattern.'~';
}
/**
* @param string $quote
*
* @return string
*/
public function getQuotedStringPattern($quote)
{
return '~^'.sprintf($this->quotedStringPattern, $quote).'~i';
}
}

View File

@@ -0,0 +1,20 @@
CssSelector Component
=====================
The CssSelector component converts CSS selectors to XPath expressions.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/css_selector.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
Credits
-------
This component is a port of the Python cssselect library
[v0.7.1](https://github.com/SimonSapin/cssselect/releases/tag/v0.7.1),
which is distributed under the BSD license.

View File

@@ -0,0 +1,76 @@
<?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\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\CssSelectorConverter;
class CssSelectorConverterTest extends TestCase
{
public function testCssToXPath()
{
$converter = new CssSelectorConverter();
$this->assertEquals('descendant-or-self::*', $converter->toXPath(''));
$this->assertEquals('descendant-or-self::h1', $converter->toXPath('h1'));
$this->assertEquals("descendant-or-self::h1[@id = 'foo']", $converter->toXPath('h1#foo'));
$this->assertEquals("descendant-or-self::h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]", $converter->toXPath('h1.foo'));
$this->assertEquals('descendant-or-self::foo:h1', $converter->toXPath('foo|h1'));
$this->assertEquals('descendant-or-self::h1', $converter->toXPath('H1'));
}
public function testCssToXPathXml()
{
$converter = new CssSelectorConverter(false);
$this->assertEquals('descendant-or-self::H1', $converter->toXPath('H1'));
}
/**
* @expectedException \Symfony\Component\CssSelector\Exception\ParseException
* @expectedExceptionMessage Expected identifier, but <eof at 3> found.
*/
public function testParseExceptions()
{
$converter = new CssSelectorConverter();
$converter->toXPath('h1:');
}
/** @dataProvider getCssToXPathWithoutPrefixTestData */
public function testCssToXPathWithoutPrefix($css, $xpath)
{
$converter = new CssSelectorConverter();
$this->assertEquals($xpath, $converter->toXPath($css, ''), '->parse() parses an input string and returns a node');
}
public function getCssToXPathWithoutPrefixTestData()
{
return [
['h1', 'h1'],
['foo|h1', 'foo:h1'],
['h1, h2, h3', 'h1 | h2 | h3'],
['h1:nth-child(3n+1)', "*/*[(name() = 'h1') and (position() - 1 >= 0 and (position() - 1) mod 3 = 0)]"],
['h1 > p', 'h1/p'],
['h1#foo', "h1[@id = 'foo']"],
['h1.foo', "h1[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"],
['h1[class*="foo bar"]', "h1[@class and contains(@class, 'foo bar')]"],
['h1[foo|class*="foo bar"]', "h1[@foo:class and contains(@foo:class, 'foo bar')]"],
['h1[class]', 'h1[@class]'],
['h1 .foo', "h1/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"],
['h1 #foo', "h1/descendant-or-self::*/*[@id = 'foo']"],
['h1 [class*=foo]', "h1/descendant-or-self::*/*[@class and contains(@class, 'foo')]"],
['div>.foo', "div/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"],
['div > .foo', "div/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]"],
];
}
}

View File

@@ -0,0 +1,34 @@
<?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\Tests\Node;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Node\NodeInterface;
abstract class AbstractNodeTest extends TestCase
{
/** @dataProvider getToStringConversionTestData */
public function testToStringConversion(NodeInterface $node, $representation)
{
$this->assertEquals($representation, (string) $node);
}
/** @dataProvider getSpecificityValueTestData */
public function testSpecificityValue(NodeInterface $node, $value)
{
$this->assertEquals($value, $node->getSpecificity()->getValue());
}
abstract public function getToStringConversionTestData();
abstract public function getSpecificityValueTestData();
}

View File

@@ -0,0 +1,37 @@
<?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\Tests\Node;
use Symfony\Component\CssSelector\Node\AttributeNode;
use Symfony\Component\CssSelector\Node\ElementNode;
class AttributeNodeTest extends AbstractNodeTest
{
public function getToStringConversionTestData()
{
return [
[new AttributeNode(new ElementNode(), null, 'attribute', 'exists', null), 'Attribute[Element[*][attribute]]'],
[new AttributeNode(new ElementNode(), null, 'attribute', '$=', 'value'), "Attribute[Element[*][attribute $= 'value']]"],
[new AttributeNode(new ElementNode(), 'namespace', 'attribute', '$=', 'value'), "Attribute[Element[*][namespace|attribute $= 'value']]"],
];
}
public function getSpecificityValueTestData()
{
return [
[new AttributeNode(new ElementNode(), null, 'attribute', 'exists', null), 10],
[new AttributeNode(new ElementNode(null, 'element'), null, 'attribute', 'exists', null), 11],
[new AttributeNode(new ElementNode(), null, 'attribute', '$=', 'value'), 10],
[new AttributeNode(new ElementNode(), 'namespace', 'attribute', '$=', 'value'), 10],
];
}
}

View File

@@ -0,0 +1,33 @@
<?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\Tests\Node;
use Symfony\Component\CssSelector\Node\ClassNode;
use Symfony\Component\CssSelector\Node\ElementNode;
class ClassNodeTest extends AbstractNodeTest
{
public function getToStringConversionTestData()
{
return [
[new ClassNode(new ElementNode(), 'class'), 'Class[Element[*].class]'],
];
}
public function getSpecificityValueTestData()
{
return [
[new ClassNode(new ElementNode(), 'class'), 10],
[new ClassNode(new ElementNode(null, 'element'), 'class'), 11],
];
}
}

View File

@@ -0,0 +1,35 @@
<?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\Tests\Node;
use Symfony\Component\CssSelector\Node\CombinedSelectorNode;
use Symfony\Component\CssSelector\Node\ElementNode;
class CombinedSelectorNodeTest extends AbstractNodeTest
{
public function getToStringConversionTestData()
{
return [
[new CombinedSelectorNode(new ElementNode(), '>', new ElementNode()), 'CombinedSelector[Element[*] > Element[*]]'],
[new CombinedSelectorNode(new ElementNode(), ' ', new ElementNode()), 'CombinedSelector[Element[*] <followed> Element[*]]'],
];
}
public function getSpecificityValueTestData()
{
return [
[new CombinedSelectorNode(new ElementNode(), '>', new ElementNode()), 0],
[new CombinedSelectorNode(new ElementNode(null, 'element'), '>', new ElementNode()), 1],
[new CombinedSelectorNode(new ElementNode(null, 'element'), '>', new ElementNode(null, 'element')), 2],
];
}
}

View File

@@ -0,0 +1,35 @@
<?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\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
class ElementNodeTest extends AbstractNodeTest
{
public function getToStringConversionTestData()
{
return [
[new ElementNode(), 'Element[*]'],
[new ElementNode(null, 'element'), 'Element[element]'],
[new ElementNode('namespace', 'element'), 'Element[namespace|element]'],
];
}
public function getSpecificityValueTestData()
{
return [
[new ElementNode(), 0],
[new ElementNode(null, 'element'), 1],
[new ElementNode('namespace', 'element'), 1],
];
}
}

View File

@@ -0,0 +1,47 @@
<?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\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\FunctionNode;
use Symfony\Component\CssSelector\Parser\Token;
class FunctionNodeTest extends AbstractNodeTest
{
public function getToStringConversionTestData()
{
return [
[new FunctionNode(new ElementNode(), 'function'), 'Function[Element[*]:function()]'],
[new FunctionNode(new ElementNode(), 'function', [
new Token(Token::TYPE_IDENTIFIER, 'value', 0),
]), "Function[Element[*]:function(['value'])]"],
[new FunctionNode(new ElementNode(), 'function', [
new Token(Token::TYPE_STRING, 'value1', 0),
new Token(Token::TYPE_NUMBER, 'value2', 0),
]), "Function[Element[*]:function(['value1', 'value2'])]"],
];
}
public function getSpecificityValueTestData()
{
return [
[new FunctionNode(new ElementNode(), 'function'), 10],
[new FunctionNode(new ElementNode(), 'function', [
new Token(Token::TYPE_IDENTIFIER, 'value', 0),
]), 10],
[new FunctionNode(new ElementNode(), 'function', [
new Token(Token::TYPE_STRING, 'value1', 0),
new Token(Token::TYPE_NUMBER, 'value2', 0),
]), 10],
];
}
}

View File

@@ -0,0 +1,33 @@
<?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\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\HashNode;
class HashNodeTest extends AbstractNodeTest
{
public function getToStringConversionTestData()
{
return [
[new HashNode(new ElementNode(), 'id'), 'Hash[Element[*]#id]'],
];
}
public function getSpecificityValueTestData()
{
return [
[new HashNode(new ElementNode(), 'id'), 100],
[new HashNode(new ElementNode(null, 'id'), 'class'), 101],
];
}
}

View File

@@ -0,0 +1,33 @@
<?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\Tests\Node;
use Symfony\Component\CssSelector\Node\ClassNode;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\NegationNode;
class NegationNodeTest extends AbstractNodeTest
{
public function getToStringConversionTestData()
{
return [
[new NegationNode(new ElementNode(), new ClassNode(new ElementNode(), 'class')), 'Negation[Element[*]:not(Class[Element[*].class])]'],
];
}
public function getSpecificityValueTestData()
{
return [
[new NegationNode(new ElementNode(), new ClassNode(new ElementNode(), 'class')), 10],
];
}
}

View File

@@ -0,0 +1,32 @@
<?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\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\PseudoNode;
class PseudoNodeTest extends AbstractNodeTest
{
public function getToStringConversionTestData()
{
return [
[new PseudoNode(new ElementNode(), 'pseudo'), 'Pseudo[Element[*]:pseudo]'],
];
}
public function getSpecificityValueTestData()
{
return [
[new PseudoNode(new ElementNode(), 'pseudo'), 10],
];
}
}

View File

@@ -0,0 +1,34 @@
<?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\Tests\Node;
use Symfony\Component\CssSelector\Node\ElementNode;
use Symfony\Component\CssSelector\Node\SelectorNode;
class SelectorNodeTest extends AbstractNodeTest
{
public function getToStringConversionTestData()
{
return [
[new SelectorNode(new ElementNode()), 'Selector[Element[*]]'],
[new SelectorNode(new ElementNode(), 'pseudo'), 'Selector[Element[*]::pseudo]'],
];
}
public function getSpecificityValueTestData()
{
return [
[new SelectorNode(new ElementNode()), 0],
[new SelectorNode(new ElementNode(), 'pseudo'), 1],
];
}
}

View File

@@ -0,0 +1,63 @@
<?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\Tests\Node;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Node\Specificity;
class SpecificityTest extends TestCase
{
/** @dataProvider getValueTestData */
public function testValue(Specificity $specificity, $value)
{
$this->assertEquals($value, $specificity->getValue());
}
/** @dataProvider getValueTestData */
public function testPlusValue(Specificity $specificity, $value)
{
$this->assertEquals($value + 123, $specificity->plus(new Specificity(1, 2, 3))->getValue());
}
public function getValueTestData()
{
return [
[new Specificity(0, 0, 0), 0],
[new Specificity(0, 0, 2), 2],
[new Specificity(0, 3, 0), 30],
[new Specificity(4, 0, 0), 400],
[new Specificity(4, 3, 2), 432],
];
}
/** @dataProvider getCompareTestData */
public function testCompareTo(Specificity $a, Specificity $b, $result)
{
$this->assertEquals($result, $a->compareTo($b));
}
public function getCompareTestData()
{
return [
[new Specificity(0, 0, 0), new Specificity(0, 0, 0), 0],
[new Specificity(0, 0, 1), new Specificity(0, 0, 1), 0],
[new Specificity(0, 0, 2), new Specificity(0, 0, 1), 1],
[new Specificity(0, 0, 2), new Specificity(0, 0, 3), -1],
[new Specificity(0, 4, 0), new Specificity(0, 4, 0), 0],
[new Specificity(0, 6, 0), new Specificity(0, 5, 11), 1],
[new Specificity(0, 7, 0), new Specificity(0, 8, 0), -1],
[new Specificity(9, 0, 0), new Specificity(9, 0, 0), 0],
[new Specificity(11, 0, 0), new Specificity(10, 11, 0), 1],
[new Specificity(12, 11, 0), new Specificity(13, 0, 0), -1],
];
}
}

View File

@@ -0,0 +1,70 @@
<?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\Tests\Parser\Handler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
/**
* @author Jean-François Simon <contact@jfsimon.fr>
*/
abstract class AbstractHandlerTest extends TestCase
{
/** @dataProvider getHandleValueTestData */
public function testHandleValue($value, Token $expectedToken, $remainingContent)
{
$reader = new Reader($value);
$stream = new TokenStream();
$this->assertTrue($this->generateHandler()->handle($reader, $stream));
$this->assertEquals($expectedToken, $stream->getNext());
$this->assertRemainingContent($reader, $remainingContent);
}
/** @dataProvider getDontHandleValueTestData */
public function testDontHandleValue($value)
{
$reader = new Reader($value);
$stream = new TokenStream();
$this->assertFalse($this->generateHandler()->handle($reader, $stream));
$this->assertStreamEmpty($stream);
$this->assertRemainingContent($reader, $value);
}
abstract public function getHandleValueTestData();
abstract public function getDontHandleValueTestData();
abstract protected function generateHandler();
protected function assertStreamEmpty(TokenStream $stream)
{
$property = new \ReflectionProperty($stream, 'tokens');
$property->setAccessible(true);
$this->assertEquals([], $property->getValue($stream));
}
protected function assertRemainingContent(Reader $reader, $remainingContent)
{
if ('' === $remainingContent) {
$this->assertEquals(0, $reader->getRemainingLength());
$this->assertTrue($reader->isEOF());
} else {
$this->assertEquals(\strlen($remainingContent), $reader->getRemainingLength());
$this->assertEquals(0, $reader->getOffset($remainingContent));
}
}
}

View File

@@ -0,0 +1,55 @@
<?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\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\CommentHandler;
use Symfony\Component\CssSelector\Parser\Reader;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\TokenStream;
class CommentHandlerTest extends AbstractHandlerTest
{
/** @dataProvider getHandleValueTestData */
public function testHandleValue($value, Token $unusedArgument, $remainingContent)
{
$reader = new Reader($value);
$stream = new TokenStream();
$this->assertTrue($this->generateHandler()->handle($reader, $stream));
// comments are ignored (not pushed as token in stream)
$this->assertStreamEmpty($stream);
$this->assertRemainingContent($reader, $remainingContent);
}
public function getHandleValueTestData()
{
return [
// 2nd argument only exists for inherited method compatibility
['/* comment */', new Token(null, null, null), ''],
['/* comment */foo', new Token(null, null, null), 'foo'],
];
}
public function getDontHandleValueTestData()
{
return [
['>'],
['+'],
[' '],
];
}
protected function generateHandler()
{
return new CommentHandler();
}
}

View File

@@ -0,0 +1,49 @@
<?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\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\HashHandler;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
class HashHandlerTest extends AbstractHandlerTest
{
public function getHandleValueTestData()
{
return [
['#id', new Token(Token::TYPE_HASH, 'id', 0), ''],
['#123', new Token(Token::TYPE_HASH, '123', 0), ''],
['#id.class', new Token(Token::TYPE_HASH, 'id', 0), '.class'],
['#id element', new Token(Token::TYPE_HASH, 'id', 0), ' element'],
];
}
public function getDontHandleValueTestData()
{
return [
['id'],
['123'],
['<'],
['<'],
['#'],
];
}
protected function generateHandler()
{
$patterns = new TokenizerPatterns();
return new HashHandler($patterns, new TokenizerEscaping($patterns));
}
}

View File

@@ -0,0 +1,49 @@
<?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\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\IdentifierHandler;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
class IdentifierHandlerTest extends AbstractHandlerTest
{
public function getHandleValueTestData()
{
return [
['foo', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), ''],
['foo|bar', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '|bar'],
['foo.class', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '.class'],
['foo[attr]', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), '[attr]'],
['foo bar', new Token(Token::TYPE_IDENTIFIER, 'foo', 0), ' bar'],
];
}
public function getDontHandleValueTestData()
{
return [
['>'],
['+'],
[' '],
['*|foo'],
['/* comment */'],
];
}
protected function generateHandler()
{
$patterns = new TokenizerPatterns();
return new IdentifierHandler($patterns, new TokenizerEscaping($patterns));
}
}

View File

@@ -0,0 +1,50 @@
<?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\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\NumberHandler;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
class NumberHandlerTest extends AbstractHandlerTest
{
public function getHandleValueTestData()
{
return [
['12', new Token(Token::TYPE_NUMBER, '12', 0), ''],
['12.34', new Token(Token::TYPE_NUMBER, '12.34', 0), ''],
['+12.34', new Token(Token::TYPE_NUMBER, '+12.34', 0), ''],
['-12.34', new Token(Token::TYPE_NUMBER, '-12.34', 0), ''],
['12 arg', new Token(Token::TYPE_NUMBER, '12', 0), ' arg'],
['12]', new Token(Token::TYPE_NUMBER, '12', 0), ']'],
];
}
public function getDontHandleValueTestData()
{
return [
['hello'],
['>'],
['+'],
[' '],
['/* comment */'],
];
}
protected function generateHandler()
{
$patterns = new TokenizerPatterns();
return new NumberHandler($patterns);
}
}

View File

@@ -0,0 +1,50 @@
<?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\Tests\Parser\Handler;
use Symfony\Component\CssSelector\Parser\Handler\StringHandler;
use Symfony\Component\CssSelector\Parser\Token;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping;
use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns;
class StringHandlerTest extends AbstractHandlerTest
{
public function getHandleValueTestData()
{
return [
['"hello"', new Token(Token::TYPE_STRING, 'hello', 1), ''],
['"1"', new Token(Token::TYPE_STRING, '1', 1), ''],
['" "', new Token(Token::TYPE_STRING, ' ', 1), ''],
['""', new Token(Token::TYPE_STRING, '', 1), ''],
["'hello'", new Token(Token::TYPE_STRING, 'hello', 1), ''],
["'foo'bar", new Token(Token::TYPE_STRING, 'foo', 1), 'bar'],
];
}
public function getDontHandleValueTestData()
{
return [
['hello'],
['>'],
['1'],
[' '],
];
}
protected function generateHandler()
{
$patterns = new TokenizerPatterns();
return new StringHandler($patterns, new TokenizerEscaping($patterns));
}
}

Some files were not shown because too many files have changed in this diff Show More