*/
class EmogrifierTest extends \PHPUnit_Framework_TestCase
{
/**
* @var string
*/
const LF = '
';
/**
* @var string
*/
private $html4TransitionalDocumentType = '';
/**
* @var string
*/
private $xhtml1StrictDocumentType = '';
/**
* @var string
*/
private $html5DocumentType = '';
/**
* @var Emogrifier
*/
private $subject = null;
/**
* Sets up the test case.
*
* @return void
*/
protected function setUp()
{
$this->html4TransitionalDocumentType = '';
$this->xhtml1StrictDocumentType = '';
$this->subject = new Emogrifier();
}
/**
* @test
*
* @expectedException \BadMethodCallException
*/
public function emogrifyForNoDataSetReturnsThrowsException()
{
$this->subject->emogrify();
}
/**
* @test
*
* @expectedException \BadMethodCallException
*/
public function emogrifyForEmptyHtmlAndEmptyCssThrowsException()
{
$this->subject->setHtml('');
$this->subject->setCss('');
$this->subject->emogrify();
}
/**
* @test
*
* @expectedException \BadMethodCallException
*/
public function emogrifyBodyContentForNoDataSetReturnsThrowsException()
{
$this->subject->emogrifyBodyContent();
}
/**
* @test
*
* @expectedException \BadMethodCallException
*/
public function emogrifyBodyContentForEmptyHtmlAndEmptyCssThrowsException()
{
$this->subject->setHtml('');
$this->subject->setCss('');
$this->subject->emogrifyBodyContent();
}
/**
* @test
*/
public function emogrifyAddsHtmlTagIfNoHtmlTagAndNoHeadTagAreProvided()
{
$this->subject->setHtml('
Hello
');
$emogrifiedHtml = $this->subject->emogrify();
self::assertContains('', $emogrifiedHtml);
}
/**
* @test
*/
public function emogrifyAddsHtmlTagIfHeadTagIsProvidedButNoHtmlTaqg()
{
$this->subject->setHtml('HelloWorld
');
$emogrifiedHtml = $this->subject->emogrify();
self::assertContains('', $emogrifiedHtml);
}
/**
* @test
*/
public function emogrifyAddsHeadTagIfNoHtmlTagAndNoHeadTagAreProvided()
{
$this->subject->setHtml('Hello
');
$emogrifiedHtml = $this->subject->emogrify();
self::assertContains('', $emogrifiedHtml);
}
/**
* @test
*/
public function emogrifyAddsHtmlTagIfHtmlTagIsProvidedButNoHeadTaqg()
{
$this->subject->setHtml('World
');
$emogrifiedHtml = $this->subject->emogrify();
self::assertContains('', $emogrifiedHtml);
}
/**
* @test
*/
public function emogrifyKeepsDollarSignsAndSquareBrackets()
{
$templateMarker = '$[USER:NAME]$';
$html = $this->html5DocumentType . '' . $templateMarker . '
';
$this->subject->setHtml($html);
self::assertContains($templateMarker, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsUtf8UmlautsInHtml5()
{
$umlautString = 'Küss die Hand, schöne Frau.';
$html = $this->html5DocumentType . '' . $umlautString . '
';
$this->subject->setHtml($html);
self::assertContains($umlautString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsUtf8UmlautsInXhtml()
{
$umlautString = 'Öösel läks õunu täis ämber uhkelt ümber.';
$html = $this->xhtml1StrictDocumentType . '' . $umlautString . '';
$this->subject->setHtml($html);
self::assertContains($umlautString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsUtf8UmlautsInHtml4()
{
$umlautString = 'Öösel läks õunu täis ämber uhkelt ümber.';
$html = $this->html4TransitionalDocumentType . '' . $umlautString . '
';
$this->subject->setHtml($html);
self::assertContains($umlautString, $umlautString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsHtmlEntities()
{
$entityString = 'a & b > c';
$html = $this->html5DocumentType . '' . $entityString . '
';
$this->subject->setHtml($html);
self::assertContains($entityString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsHtmlEntitiesInXhtml()
{
$entityString = 'a & b > c';
$html = $this->xhtml1StrictDocumentType . '' . $entityString . '';
$this->subject->setHtml($html);
self::assertContains($entityString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsHtmlEntitiesInHtml4()
{
$entityString = 'a & b > c';
$html = $this->html4TransitionalDocumentType . '' . $entityString . '
';
$this->subject->setHtml($html);
self::assertContains($entityString, $entityString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsUtf8UmlautsWithoutDocumentType()
{
$umlautString = 'Küss die Hand, schöne Frau.';
$html = '' . $umlautString . '
';
$this->subject->setHtml($html);
self::assertContains($umlautString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsUtf8UmlautsWithoutDocumentTypeAndWithoutHtmlAndWithoutHead()
{
$umlautString = 'Küss die Hand, schöne Frau.';
$html = '' . $umlautString . '
';
$this->subject->setHtml($html);
self::assertContains($umlautString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsUtf8UmlautsWithoutDocumentTypeAndWithHtmlAndWithoutHead()
{
$umlautString = 'Küss die Hand, schöne Frau.';
$html = '' . $umlautString . '
';
$this->subject->setHtml($html);
self::assertContains($umlautString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsUtf8UmlautsWithoutDocumentTypeAndWithoutHtmlAndWithHead()
{
$umlautString = 'Küss die Hand, schöne Frau.';
$html = '' . $umlautString . '
';
$this->subject->setHtml($html);
self::assertContains($umlautString, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyForHtmlTagOnlyAndEmptyCssByDefaultAddsHtml5DocumentType()
{
$html = '';
$this->subject->setHtml($html);
$this->subject->setCss('');
self::assertContains($this->html5DocumentType, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyForHtmlTagWithXhtml1StrictDocumentTypeKeepsDocumentType()
{
$html = $this->xhtml1StrictDocumentType . '';
$this->subject->setHtml($html);
self::assertContains($this->xhtml1StrictDocumentType, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyForHtmlTagWithXhtml5DocumentTypeKeepsDocumentType()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
self::assertContains($this->html5DocumentType, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyAddsContentTypeMetaTag()
{
$html = $this->html5DocumentType . 'Hello
';
$this->subject->setHtml($html);
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyForExistingContentTypeMetaTagNotAddsSecondContentTypeMetaTag()
{
$html = $this->html5DocumentType . '' . 'Hello
';
$this->subject->setHtml($html);
$numberOfContentTypeMetaTags = substr_count($this->subject->emogrify(), 'Content-Type');
self::assertSame(1, $numberOfContentTypeMetaTags);
}
/**
* @test
*/
public function emogrifyByDefaultRemovesWbrTag()
{
$html = $this->html5DocumentType . 'foobar';
$this->subject->setHtml($html);
self::assertContains('foobar', $this->subject->emogrify());
}
/**
* @test
*/
public function addUnprocessableTagCausesGivenEmptyTagToBeRemoved()
{
$this->subject->addUnprocessableHtmlTag('p');
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
self::assertNotContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function addUnprocessableTagNotRemovesGivenTagWithContent()
{
$this->subject->addUnprocessableHtmlTag('p');
$html = $this->html5DocumentType . '
foobar
';
$this->subject->setHtml($html);
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function removeUnprocessableHtmlTagCausesTagToStayAgain()
{
$this->subject->addUnprocessableHtmlTag('p');
$this->subject->removeUnprocessableHtmlTag('p');
$html = $this->html5DocumentType . '
foo
bar
';
$this->subject->setHtml($html);
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyCanAddMatchingElementRuleOnHtmlElementFromCss()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$styleRule = 'color: #000;';
$this->subject->setCss('html {' . $styleRule . '}');
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyNotAddsNotMatchingElementRuleOnHtmlElementFromCss()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$this->subject->setCss('p {color:#000;}');
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyCanMatchTwoElements()
{
$html = $this->html5DocumentType . '
';
$this->subject->setHtml($html);
$styleRule = 'color: #000;';
$this->subject->setCss('p {' . $styleRule . '}');
self::assertSame(2, substr_count($this->subject->emogrify(), ''));
}
/**
* @test
*/
public function emogrifyCanAssignTwoStyleRulesFromSameMatcherToElement()
{
$html = $this->html5DocumentType . '
';
$this->subject->setHtml($html);
$styleRulesIn = 'color:#000; text-align:left;';
$styleRulesOut = 'color: #000; text-align: left;';
$this->subject->setCss('p {' . $styleRulesIn . '}');
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyCanMatchAttributeOnlySelector()
{
$html = $this->html5DocumentType . '
';
$this->subject->setHtml($html);
$this->subject->setCss('[hidden] { color:red; }');
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyCanAssignStyleRulesFromTwoIdenticalMatchersToElement()
{
$html = $this->html5DocumentType . '
';
$this->subject->setHtml($html);
$styleRule1 = 'color: #000;';
$styleRule2 = 'text-align: left;';
$this->subject->setCss('p {' . $styleRule1 . '} p {' . $styleRule2 . '}');
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyCanAssignStyleRulesFromTwoDifferentMatchersToElement()
{
$html = $this->html5DocumentType . '
';
$this->subject->setHtml($html);
$styleRule1 = 'color: #000;';
$styleRule2 = 'text-align: left;';
$this->subject->setCss('p {' . $styleRule1 . '} .x {' . $styleRule2 . '}');
self::assertContains('', $this->subject->emogrify());
}
/**
* Data provide for selectors.
*
* @return string[][]
*/
public function selectorDataProvider()
{
$styleRule = 'color: red;';
$styleAttribute = 'style="' . $styleRule . '"';
return array('universal selector HTML' => array('* {' . $styleRule . '} ', '##'), 'universal selector BODY' => array('* {' . $styleRule . '} ', '#
#'), 'universal selector P' => array('* {' . $styleRule . '} ', '#]*' . $styleAttribute . '>#'), 'type selector matches first P' => array('p {' . $styleRule . '} ', '#
#'), 'type selector matches second P' => array('p {' . $styleRule . '} ', '#
#'), 'descendant selector P SPAN' => array('p span {' . $styleRule . '} ', '##'), 'descendant selector BODY SPAN' => array('body span {' . $styleRule . '} ', '##'), 'child selector P > SPAN matches direct child' => array('p > span {' . $styleRule . '} ', '##'), 'child selector BODY > SPAN not matches grandchild' => array('body > span {' . $styleRule . '} ', '##'), 'adjacent selector P + P not matches first P' => array('p + p {' . $styleRule . '} ', '##'), 'adjacent selector P + P matches second P' => array('p + p {' . $styleRule . '} ', '#
#'), 'adjacent selector P + P matches third P' => array('p + p {' . $styleRule . '} ', '#
#'), 'ID selector #HTML' => array('#html {' . $styleRule . '} ', '##'), 'type and ID selector HTML#HTML' => array('html#html {' . $styleRule . '} ', '##'), 'class selector .P-1' => array('.p-1 {' . $styleRule . '} ', '#
#'), 'type and class selector P.P-1' => array('p.p-1 {' . $styleRule . '} ', '#
#'), 'attribute presence selector SPAN[title] matches element with matching attribute' => array('span[title] {' . $styleRule . '} ', '##'), 'attribute presence selector SPAN[title] not matches element without any attributes' => array('span[title] {' . $styleRule . '} ', '##'), 'attribute value selector [id="html"] matches element with matching attribute value' => array('[id="html"] {' . $styleRule . '} ', '##'), 'attribute value selector SPAN[title] matches element with matching attribute value' => array('span[title="bonjour"] {' . $styleRule . '} ', '##'), 'attribute value selector SPAN[title] not matches element with other attribute value' => array('span[title="bonjour"] {' . $styleRule . '} ', '##'), 'attribute value selector SPAN[title] not matches element without any attributes' => array('span[title="bonjour"] {' . $styleRule . '} ', '##'), 'BODY:first-child matches first child' => array('body:first-child {' . $styleRule . '} ', '##'), 'BODY:first-child not matches middle child' => array('body:first-child {' . $styleRule . '} ', '#
#'), 'BODY:first-child not matches last child' => array('body:first-child {' . $styleRule . '} ', '#
#'), 'BODY:last-child not matches first child' => array('body:last-child {' . $styleRule . '} ', '#
#'), 'BODY:last-child not matches middle child' => array('body:last-child {' . $styleRule . '} ', '#
#'), 'BODY:last-child matches last child' => array('body:last-child {' . $styleRule . '} ', '#
#'));
}
/**
* @test
*
* @param string $css the complete CSS
* @param string $htmlRegularExpression regular expression for the the HTML that needs to be contained in the HTML
*
* @dataProvider selectorDataProvider
*/
public function emogrifierMatchesSelectors($css, $htmlRegularExpression)
{
$html = $this->html5DocumentType . '' . '
' . ' some text
' . ' some text
' . ' some more text
' . ' ' . '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
$result = $this->subject->emogrify();
self::assertRegExp($htmlRegularExpression, $result);
}
/**
* Data provider for emogrifyDropsWhitespaceFromCssDeclarations.
*
* @return string[][]
*/
public function cssDeclarationWhitespaceDroppingDataProvider()
{
return array('no whitespace, trailing semicolon' => array('color:#000;', 'color: #000;'), 'no whitespace, no trailing semicolon' => array('color:#000', 'color: #000;'), 'space after colon, no trailing semicolon' => array('color: #000', 'color: #000;'), 'space before colon, no trailing semicolon' => array('color :#000', 'color: #000;'), 'space before property name, no trailing semicolon' => array(' color:#000', 'color: #000;'), 'space before trailing semicolon' => array(' color:#000 ;', 'color: #000;'), 'space after trailing semicolon' => array(' color:#000; ', 'color: #000;'), 'space after property value, no trailing semicolon' => array(' color:#000 ', 'color: #000;'), 'space after property value, trailing semicolon' => array(' color:#000; ', 'color: #000;'), 'newline before property name, trailing semicolon' => array('
color:#222;', 'color: #222;'), 'newline after property semicolon' => array('color:#222;
', 'color: #222;'), 'newline before colon, trailing semicolon' => array('color
:#333;', 'color: #333;'), 'newline after colon, trailing semicolon' => array('color:
#333;', 'color: #333;'), 'newline after semicolon' => array('color:#333
;', 'color: #333;'));
}
/**
* @test
*
* @param string $cssDeclaration the CSS declaration block (without the curly braces)
* @param string $expectedStyleAttributeContent the expected value of the style attribute
*
* @dataProvider cssDeclarationWhitespaceDroppingDataProvider
*/
public function emogrifyDropsLeadingAndTrailingWhitespaceFromCssDeclarations($cssDeclaration, $expectedStyleAttributeContent)
{
$html = $this->html5DocumentType . '';
$css = 'html {' . $cssDeclaration . '}';
$this->subject->setHtml($html);
$this->subject->setCss($css);
$result = $this->subject->emogrify();
self::assertContains('html style="' . $expectedStyleAttributeContent . '">', $result);
}
/**
* Data provider for emogrifyFormatsCssDeclarations.
*
* @return string[][]
*/
public function formattedCssDeclarationDataProvider()
{
return array('one declaration' => array('color: #000;', 'color: #000;'), 'one declaration with dash in property name' => array('font-weight: bold;', 'font-weight: bold;'), 'one declaration with space in property value' => array('margin: 0 4px;', 'margin: 0 4px;'), 'two declarations separated by semicolon' => array('color: #000;width: 3px;', 'color: #000; width: 3px;'), 'two declarations separated by semicolon and space' => array('color: #000; width: 3px;', 'color: #000; width: 3px;'), 'two declarations separated by semicolon and linefeed' => array('color: #000;' . self::LF . 'width: 3px;', 'color: #000; width: 3px;'), 'two declarations separated by semicolon and Windows line ending' => array('color: #000;
width: 3px;', 'color: #000; width: 3px;'), 'one declaration with leading dash in property name' => array('-webkit-text-size-adjust:none;', '-webkit-text-size-adjust: none;'));
}
/**
* @test
*
* @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
* @param string $expectedStyleAttributeContent the expected value of the style attribute
*
* @dataProvider formattedCssDeclarationDataProvider
*/
public function emogrifyFormatsCssDeclarations($cssDeclarationBlock, $expectedStyleAttributeContent)
{
$html = $this->html5DocumentType . '';
$css = 'html {' . $cssDeclarationBlock . '}';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains('html style="' . $expectedStyleAttributeContent . '">', $this->subject->emogrify());
}
/**
* Data provider for emogrifyInvalidDeclaration.
*
* @return string[][]
*/
public function invalidDeclarationDataProvider()
{
return array('missing dash in property name' => array('font weight: bold;'), 'invalid character in property name' => array('-9webkit-text-size-adjust:none;'), 'missing :' => array('-webkit-text-size-adjust none'), 'missing value' => array('-webkit-text-size-adjust :'));
}
/**
* @test
*
* @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
*
* @dataProvider invalidDeclarationDataProvider
*/
public function emogrifyDropsInvalidDeclaration($cssDeclarationBlock)
{
$html = $this->html5DocumentType . '';
$css = 'html {' . $cssDeclarationBlock . '}';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsExistingStyleAttributes()
{
$styleAttribute = 'style="color: #ccc;"';
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
self::assertContains($styleAttribute, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyAddsCssAfterExistingStyle()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$cssDeclarations = 'margin: 0 2px;';
$css = 'html {' . $cssDeclarations . '}';
$this->subject->setCss($css);
self::assertContains('style="' . $styleAttributeValue . ' ' . $cssDeclarations . '"', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyCanMatchMinifiedCss()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$this->subject->setCss('p{color:blue;}html{color:red;}');
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyLowercasesAttributeNamesFromStyleAttributes()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
self::assertContains('style="color: #ccc;"', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyLowerCasesAttributeNames()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$cssIn = 'html {mArGiN:0 2pX;}';
$cssOut = 'margin: 0 2pX;';
$this->subject->setCss($cssIn);
self::assertContains('style="' . $cssOut . '"', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyPreservesCaseForAttributeValuesFromPassedInCss()
{
$css = 'content: \'Hello World\';';
$html = $this->html5DocumentType . 'target
';
$this->subject->setHtml($html);
$this->subject->setCss('p {' . $css . '}');
self::assertContains('target
', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyPreservesCaseForAttributeValuesFromParsedStyleBlock()
{
$css = 'content: \'Hello World\';';
$html = $this->html5DocumentType . 'target
';
$this->subject->setHtml($html);
self::assertContains('target
', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyRemovesStyleNodes()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
self::assertNotContains('';
$this->subject->setHtml($html);
$hasError = false;
set_error_handler(function ($errorNumber, $errorMessage) use(&$hasError) {
if ($errorMessage === 'DOMXPath::query(): Invalid expression') {
return true;
}
$hasError = true;
return true;
});
$this->subject->emogrify();
restore_error_handler();
self::assertFalse($hasError);
}
/**
* Data provider for things that should be left out when applying the CSS.
*
* @return array[]
*/
public function unneededCssThingsDataProvider()
{
return array('CSS comments with one asterisk' => array('p {color: #000;/* black */}', 'black'), 'CSS comments with two asterisks' => array('p {color: #000;/** black */}', 'black'), '@import directive' => array('@import "foo.css";', '@import'), 'style in "aural" media type rule' => array('@media aural {p {color: #000;}}', '#000'), 'style in "braille" media type rule' => array('@media braille {p {color: #000;}}', '#000'), 'style in "embossed" media type rule' => array('@media embossed {p {color: #000;}}', '#000'), 'style in "handheld" media type rule' => array('@media handheld {p {color: #000;}}', '#000'), 'style in "projection" media type rule' => array('@media projection {p {color: #000;}}', '#000'), 'style in "speech" media type rule' => array('@media speech {p {color: #000;}}', '#000'), 'style in "tty" media type rule' => array('@media tty {p {color: #000;}}', '#000'), 'style in "tv" media type rule' => array('@media tv {p {color: #000;}}', '#000'));
}
/**
* @test
*
* @param string $css
* @param string $markerNotExpectedInHtml
*
* @dataProvider unneededCssThingsDataProvider
*/
public function emogrifyFiltersUnneededCssThings($css, $markerNotExpectedInHtml)
{
$html = $this->html5DocumentType . 'foo
';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertNotContains($markerNotExpectedInHtml, $this->subject->emogrify());
}
/**
* Data provider for media rules.
*
* @return array[]
*/
public function mediaRulesDataProvider()
{
return array('style in "only all" media type rule' => array('@media only all {p {color: #000;}}'), 'style in "only screen" media type rule' => array('@media only screen {p {color: #000;}}'), 'style in media type rule' => array('@media {p {color: #000;}}'), 'style in "screen" media type rule' => array('@media screen {p {color: #000;}}'), 'style in "print" media type rule' => array('@media print {p {color: #000;}}'), 'style in "all" media type rule' => array('@media all {p {color: #000;}}'));
}
/**
* @test
*
* @param string $css
*
* @dataProvider mediaRulesDataProvider
*/
public function emogrifyKeepsMediaRules($css)
{
$html = $this->html5DocumentType . 'foo
';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains($css, $this->subject->emogrify());
}
/**
* @test
*/
public function removeAllowedMediaTypeRemovesStylesForTheGivenMediaType()
{
$css = '@media screen { html {} }';
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
$this->subject->removeAllowedMediaType('screen');
self::assertNotContains($css, $this->subject->emogrify());
}
/**
* @test
*/
public function addAllowedMediaTypeKeepsStylesForTheGivenMediaType()
{
$css = '@media braille { html { some-property: value; } }';
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
$this->subject->addAllowedMediaType('braille');
self::assertContains($css, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyAddsMissingHeadElement()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$this->subject->setCss('@media all { html {} }');
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepExistingHeadElementContent()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$this->subject->setCss('@media all { html {} }');
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepExistingHeadElementAddStyleElement()
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$this->subject->setCss('@media all { html {} }');
self::assertContains('';
$this->subject->setHtml($html);
self::assertContains($css, $this->subject->emogrify());
}
/**
* @test
*
* @param string $css
*
* @dataProvider validMediaPreserveDataProvider
*/
public function emogrifyWithValidMediaQueryNotContainsInlineCss($css)
{
$html = $this->html5DocumentType . PHP_EOL . '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertNotContains('style="color:red"', $this->subject->emogrify());
}
/**
* Invalid media query which need to be strip
*
* @return array[]
*/
public function invalidMediaPreserveDataProvider()
{
return array('style in "braille" type rule' => array('@media braille { h1 { color:red; } }'), 'style in "embossed" type rule' => array('@media embossed { h1 { color:red; } }'), 'style in "handheld" type rule' => array('@media handheld { h1 { color:red; } }'), 'style in "projection" type rule' => array('@media projection { h1 { color:red; } }'), 'style in "speech" type rule' => array('@media speech { h1 { color:red; } }'), 'style in "tty" type rule' => array('@media tty { h1 { color:red; } }'), 'style in "tv" type rule' => array('@media tv { h1 { color:red; } }'));
}
/**
* @test
*
* @param string $css
*
* @dataProvider invalidMediaPreserveDataProvider
*/
public function emogrifyWithInvalidMediaQueryaNotContainsInnerCss($css)
{
$html = $this->html5DocumentType . PHP_EOL . '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertNotContains($css, $this->subject->emogrify());
}
/**
* @test
*
* @param string $css
*
* @dataProvider invalidMediaPreserveDataProvider
*/
public function emogrifyWithInValidMediaQueryNotContainsInlineCss($css)
{
$html = $this->html5DocumentType . PHP_EOL . '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertNotContains('style="color: red"', $this->subject->emogrify());
}
/**
* @test
*
* @param string $css
*
* @dataProvider invalidMediaPreserveDataProvider
*/
public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInnerCss($css)
{
$html = $this->html5DocumentType . PHP_EOL . '';
$this->subject->setHtml($html);
self::assertNotContains($css, $this->subject->emogrify());
}
/**
* @test
*
* @param string $css
*
* @dataProvider invalidMediaPreserveDataProvider
*/
public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInlineCss($css)
{
$html = $this->html5DocumentType . PHP_EOL . '';
$this->subject->setHtml($html);
self::assertNotContains('style="color: red"', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyAppliesCssFromStyleNodes()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
self::assertContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyWhenDisabledNotAppliesCssFromStyleBlocks()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$this->subject->disableStyleBlocksParsing();
self::assertNotContains('', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyWhenStyleBlocksParsingDisabledKeepInlineStyles()
{
$styleAttributeValue = 'text-align: center;';
$html = $this->html5DocumentType . '' . 'paragraph
';
$expected = '';
$this->subject->setHtml($html);
$this->subject->disableStyleBlocksParsing();
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyWhenDisabledNotAppliesCssFromInlineStyles()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$this->subject->disableInlineStyleAttributesParsing();
self::assertNotContains('subject->emogrify());
}
/**
* @test
*/
public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . '
' . 'paragraph
';
$expected = '';
$this->subject->setHtml($html);
$this->subject->disableInlineStyleAttributesParsing();
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyAppliesCssWithUpperCaseSelector()
{
$html = $this->html5DocumentType . '
paragraph
';
$expected = '';
$this->subject->setHtml($html);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* Emogrify was handling case differently for passed in CSS vs CSS parsed from style blocks.
* @test
*/
public function emogrifyAppliesCssWithMixedCaseAttributesInStyleBlock()
{
$html = $this->html5DocumentType . '
' . '';
$expected = '';
$this->subject->setHtml($html);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* Passed in CSS sets the order, but style block CSS overrides values.
* @test
*/
public function emogrifyMergesCssWithMixedCaseAttribute()
{
$css = 'p { margin: 0; padding-TOP: 0; PADDING-bottom: 1PX;}';
$html = $this->html5DocumentType . '
' . '';
$expected = '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyMergesCssWithMixedUnits()
{
$css = 'p { margin: 1px; padding-bottom:0;}';
$html = $this->html5DocumentType . '
' . '';
$expected = '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyByDefaultRemovesElementsWithDisplayNoneFromExternalCss()
{
$css = 'div.foo { display: none; }';
$html = $this->html5DocumentType . '
';
$expected = '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyByDefaultRemovesElementsWithDisplayNoneInStyleAttribute()
{
$html = $this->html5DocumentType . '' . '';
$expected = '';
$this->subject->setHtml($html);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyAfterDisableInvisibleNodeRemovalPreservesInvisibleElements()
{
$css = 'div.foo { display: none; }';
$html = $this->html5DocumentType . '';
$expected = '';
$this->subject->setHtml($html);
$this->subject->setCss($css);
$this->subject->disableInvisibleNodeRemoval();
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyKeepsCssMediaQueriesWithCssCommentAfterMediaQuery()
{
$css = '@media only screen and (max-width: 480px) { body { color: #ffffff } /* some comment */ }';
$html = $this->html5DocumentType . '';
$expected = '@media only screen and (max-width: 480px)';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyForXhtmlDocumentTypeConvertsXmlSelfClosingTagsToNonXmlSelfClosingTag()
{
$this->subject->setHtml($this->xhtml1StrictDocumentType . '
');
self::assertContains('
', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyForHtml5DocumentTypeKeepsNonXmlSelfClosingTagsAsNonXmlSelfClosing()
{
$this->subject->setHtml($this->html5DocumentType . '
');
self::assertContains('
', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyForHtml5DocumentTypeConvertXmlSelfClosingTagsToNonXmlSelfClosingTag()
{
$this->subject->setHtml($this->html5DocumentType . '
');
self::assertContains('
', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyAutomaticallyClosesUnclosedTag()
{
$this->subject->setHtml($this->html5DocumentType . '
');
self::assertContains('
', $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyReturnsCompleteHtmlDocument()
{
$this->subject->setHtml($this->html5DocumentType . '
');
self::assertSame($this->html5DocumentType . self::LF . '' . self::LF . '
' . self::LF . '
' . self::LF . '' . self::LF, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyBodyContentReturnsBodyContentFromHtml()
{
$this->subject->setHtml($this->html5DocumentType . '
');
self::assertSame('
' . self::LF, $this->subject->emogrifyBodyContent());
}
/**
* @test
*/
public function emogrifyBodyContentReturnsBodyContentFromContent()
{
$this->subject->setHtml('
');
self::assertSame('
' . self::LF, $this->subject->emogrifyBodyContent());
}
/**
* @test
*/
public function importantInExternalCssOverwritesInlineCss()
{
$css = 'p { margin: 1px !important; }';
$html = $this->html5DocumentType . '
some content
';
$expected = '
';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function importantInExternalCssKeepsInlineCssForOtherAttributes()
{
$css = 'p { margin: 1px !important; }';
$html = $this->html5DocumentType . '
some content
';
$expected = '
';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function emogrifyHandlesImportantStyleTagCaseInsensitive()
{
$css = 'p { margin: 1px !ImPorTant; }';
$html = $this->html5DocumentType . '
some content
';
$expected = '
';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function irrelevantMediaQueriesAreRemoved()
{
$uselessQuery = '@media all and (max-width: 500px) { em { color:red; } }';
$this->subject->setCss($uselessQuery);
$this->subject->setHtml($this->html5DocumentType . '
');
$result = $this->subject->emogrify();
self::assertNotContains($uselessQuery, $result);
}
/**
* @test
*/
public function relevantMediaQueriesAreRetained()
{
$usefulQuery = '@media all and (max-width: 500px) { p { color:red; } }';
$this->subject->setCss($usefulQuery);
$this->subject->setHtml($this->html5DocumentType . '
');
$result = $this->subject->emogrify();
self::assertContains($usefulQuery, $result);
}
/**
* @test
*/
public function importantStyleRuleFromInlineCssOverwritesImportantStyleRuleFromExternalCss()
{
$css = 'p { margin: 1px !important; padding: 1px;}';
$html = $this->html5DocumentType . '
some content
' . '';
$expected = '
';
$this->subject->setHtml($html);
$this->subject->setCss($css);
self::assertContains($expected, $this->subject->emogrify());
}
/**
* @test
*/
public function addExcludedSelectorRemovesMatchingElementsFromEmogrification()
{
$css = 'p { margin: 0; }';
$this->subject->setHtml($this->html5DocumentType . '
');
$this->subject->setCss($css);
$this->subject->addExcludedSelector('p.x');
$html = $this->subject->emogrify();
self::assertContains('
', $html);
}
/**
* @test
*/
public function addExcludedSelectorExcludesMatchingElementEventWithWhitespaceAroundSelector()
{
$css = 'p { margin: 0; }';
$this->subject->setHtml($this->html5DocumentType . '
');
$this->subject->setCss($css);
$this->subject->addExcludedSelector(' p.x ');
$html = $this->subject->emogrify();
self::assertContains('
', $html);
}
/**
* @test
*/
public function addExcludedSelectorKeepsNonMatchingElementsInEmogrification()
{
$css = 'p { margin: 0; }';
$this->subject->setHtml($this->html5DocumentType . '
');
$this->subject->setCss($css);
$this->subject->addExcludedSelector('p.x');
$html = $this->subject->emogrify();
self::assertContains('
', $html);
}
/**
* @test
*/
public function removeExcludedSelectorGetsMatchingElementsToBeEmogrifiedAgain()
{
$css = 'p { margin: 0; }';
$this->subject->setHtml($this->html5DocumentType . '
');
$this->subject->setCss($css);
$this->subject->addExcludedSelector('p.x');
$this->subject->removeExcludedSelector('p.x');
$html = $this->subject->emogrify();
self::assertContains('
', $html);
}
/**
* @test
*/
public function emptyMediaQueriesAreRemoved()
{
$emptyQuery = '@media all and (max-width: 500px) { }';
$this->subject->setCss($emptyQuery);
$this->subject->setHtml($this->html5DocumentType . '
');
$result = $this->subject->emogrify();
self::assertNotContains($emptyQuery, $result);
}
/**
* @test
*/
public function multiLineMediaQueryWithWindowsLineEndingsIsAppliedOnlyOnce()
{
$css = '@media all {
' . '.medium {font-size:18px;}
' . '.small {font-size:14px;}
' . '}';
$this->subject->setCss($css);
$this->subject->setHtml($this->html5DocumentType . '' . '
medium
' . '
small
' . '');
$result = $this->subject->emogrify();
self::assertSame(1, substr_count($result, ''));
}
/**
* @test
*/
public function multiLineMediaQueryWithUnixLineEndingsIsAppliedOnlyOnce()
{
$css = '@media all {
' . '.medium {font-size:18px;}
' . '.small {font-size:14px;}
' . '}';
$this->subject->setCss($css);
$this->subject->setHtml($this->html5DocumentType . '' . '
medium
' . '
small
' . '');
$result = $this->subject->emogrify();
self::assertSame(1, substr_count($result, ''));
}
/**
* @test
*/
public function multipleMediaQueriesAreAppliedOnlyOnce()
{
$css = '@media all {
' . '.medium {font-size:18px;
' . '.small {font-size:14px;}
' . '}' . '@media screen {
' . '.medium {font-size:24px;}
' . '.small {font-size:18px;}
' . '}';
$this->subject->setCss($css);
$this->subject->setHtml($this->html5DocumentType . '' . '
medium
' . '
small
' . '');
$result = $this->subject->emogrify();
self::assertSame(1, substr_count($result, ''));
}
/**
* @return string[][]
*/
public function dataUriMediaTypeDataProvider()
{
return array('nothing' => array(''), ';charset=utf-8' => array(';charset=utf-8'), ';base64' => array(';base64'), ';charset=utf-8;base64' => array(';charset=utf-8;base64'));
}
/**
* @test
* @param string $dataUriMediaType
* @dataProvider dataUriMediaTypeDataProvider
*/
public function dataUrisAreConserved($dataUriMediaType)
{
$html = $this->html5DocumentType . '';
$this->subject->setHtml($html);
$styleRule = 'background-image: url(data:image/png' . $dataUriMediaType . ',iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAABUk' . 'lEQVQ4y81UsY6CQBCdWXBjYWFMjEgAE0piY8c38B9+iX+ksaHCgs5YWEhIrJCQYGJBomiC7lzhVcfqEa+5KXfey3s783bRdd00TR' . 'VFAQAAICJEhN/q8Xjoug7D4RA+qsFgwDjn9QYiTiaT+Xx+OByOx+NqtapjWq0WjEajekPTtCAIiIiIyrKMoqiOMQxDlVqyLMt1XQ' . 'A4nU6z2Wy9XkthEnK/3zdN8znC/X7v+36WZfJ7120vFos4joUQRHS5XDabzXK5bGrbtu1er/dtTFU1TWu3202VHceZTqe3242Itt' . 'ut53nj8bip8m6345wLIQCgKIowDIuikAoz6Wm3233mjHPe6XRe5UROJqImIWPwh/pvZMbYM2GKorx5oUw6m+v1miTJ+XzO8/x+v7' . '+UtizrM8+GYahVVSFik9/jxy6rqlJN02SM1cmI+GbbQghd178AAO2FXws6LwMAAAAASUVORK5CYII=);';
$this->subject->setCss('html {' . $styleRule . '}');
$result = $this->subject->emogrify();
self::assertContains('', $result);
}
}