*/ 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('Hello

World

'); $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->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 . '' . '

some content

'; $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 . '' . '

some content

'; $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 . '' . '

some content

'; $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 = '