PageRenderTime 32ms CodeModel.GetById 8ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/pelago/emogrifier/Tests/Unit/EmogrifierTest.php

https://gitlab.com/yousafsyed/easternglamor
PHP | 1093 lines | 702 code | 123 blank | 268 comment | 0 complexity | 3e7813f894a71153b152f217a7c4576b MD5 | raw file
  1. <?php
  2. namespace Pelago\Tests\Unit;
  3. use Pelago\Emogrifier;
  4. /**
  5. * Test case.
  6. *
  7. * @author Oliver Klee <typo3-coding@oliverklee.de>
  8. */
  9. class EmogrifierTest extends \PHPUnit_Framework_TestCase
  10. {
  11. /**
  12. * @var string
  13. */
  14. const LF = "\n";
  15. /**
  16. * @var string
  17. */
  18. private $html4TransitionalDocumentType = '';
  19. /**
  20. * @var string
  21. */
  22. private $xhtml1StrictDocumentType = '';
  23. /**
  24. * @var string
  25. */
  26. private $html5DocumentType = '<!DOCTYPE html>';
  27. /**
  28. * @var Emogrifier
  29. */
  30. private $subject = null;
  31. /**
  32. * Sets up the test case.
  33. *
  34. * @return void
  35. */
  36. protected function setUp()
  37. {
  38. $this->html4TransitionalDocumentType = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" '
  39. . '"http://www.w3.org/TR/REC-html40/loose.dtd">';
  40. $this->xhtml1StrictDocumentType = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '
  41. . '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
  42. $this->subject = new Emogrifier();
  43. }
  44. /**
  45. * @test
  46. *
  47. * @expectedException \BadMethodCallException
  48. */
  49. public function emogrifyForNoDataSetReturnsThrowsException()
  50. {
  51. $this->subject->emogrify();
  52. }
  53. /**
  54. * @test
  55. *
  56. * @expectedException \BadMethodCallException
  57. */
  58. public function emogrifyForEmptyHtmlAndEmptyCssThrowsException()
  59. {
  60. $this->subject->setHtml('');
  61. $this->subject->setCss('');
  62. $this->subject->emogrify();
  63. }
  64. /**
  65. * @test
  66. */
  67. public function emogrifyByDefaultEncodesUmlautsAsHtmlEntities()
  68. {
  69. $html = $this->html5DocumentType . '<html><p>Einen schönen Gruß!</p></html>';
  70. $this->subject->setHtml($html);
  71. $this->assertContains(
  72. 'Einen sch&ouml;nen Gru&szlig;!',
  73. $this->subject->emogrify()
  74. );
  75. }
  76. /**
  77. * @test
  78. */
  79. public function emogrifyCanKeepEncodedUmlauts()
  80. {
  81. $this->subject->preserveEncoding = true;
  82. $encodedString = 'Küss die Hand, schöne Frau.';
  83. $html = $this->html5DocumentType . '<html><p>' . $encodedString . '</p></html>';
  84. $this->subject->setHtml($html);
  85. $this->assertContains(
  86. $encodedString,
  87. $this->subject->emogrify()
  88. );
  89. }
  90. /**
  91. * @test
  92. */
  93. public function emogrifyForHtmlTagOnlyAndEmptyCssReturnsHtmlTagWithHtml4DocumentType()
  94. {
  95. $html = '<html></html>';
  96. $this->subject->setHtml($html);
  97. $this->subject->setCss('');
  98. $this->assertSame(
  99. $this->html4TransitionalDocumentType . self::LF . $html . self::LF,
  100. $this->subject->emogrify()
  101. );
  102. }
  103. /**
  104. * @test
  105. */
  106. public function emogrifyForHtmlTagWithXhtml1StrictDocumentTypeKeepsDocumentType()
  107. {
  108. $html = $this->xhtml1StrictDocumentType . self::LF . '<html></html>' . self::LF;
  109. $this->subject->setHtml($html);
  110. $this->subject->setCss('');
  111. $this->assertSame(
  112. $html,
  113. $this->subject->emogrify()
  114. );
  115. }
  116. /**
  117. * @test
  118. */
  119. public function emogrifyForHtmlTagWithXhtml5DocumentTypeKeepsDocumentType()
  120. {
  121. $html = $this->html5DocumentType . self::LF . '<html></html>' . self::LF;
  122. $this->subject->setHtml($html);
  123. $this->subject->setCss('');
  124. $this->assertSame(
  125. $html,
  126. $this->subject->emogrify()
  127. );
  128. }
  129. /**
  130. * @test
  131. */
  132. public function emogrifyByDefaultRemovesWbrTag()
  133. {
  134. $html = $this->html5DocumentType . self::LF . '<html>foo<wbr/>bar</html>' . self::LF;
  135. $this->subject->setHtml($html);
  136. $this->assertContains(
  137. 'foobar',
  138. $this->subject->emogrify()
  139. );
  140. }
  141. /**
  142. * @test
  143. */
  144. public function addUnprocessableTagCausesTagToBeRemoved()
  145. {
  146. $this->subject->addUnprocessableHtmlTag('p');
  147. $html = $this->html5DocumentType . self::LF . '<html><p></p></html>' . self::LF;
  148. $this->subject->setHtml($html);
  149. $this->assertNotContains(
  150. '<p>',
  151. $this->subject->emogrify()
  152. );
  153. }
  154. /**
  155. * @test
  156. */
  157. public function addUnprocessableTagNotRemovesTagWithContent()
  158. {
  159. $this->subject->addUnprocessableHtmlTag('p');
  160. $html = $this->html5DocumentType . self::LF . '<html><p>foobar</p></html>' . self::LF;
  161. $this->subject->setHtml($html);
  162. $this->assertContains(
  163. '<p>',
  164. $this->subject->emogrify()
  165. );
  166. }
  167. /**
  168. * @test
  169. */
  170. public function removeUnprocessableHtmlTagCausesTagToStayAgain()
  171. {
  172. $this->subject->addUnprocessableHtmlTag('p');
  173. $this->subject->removeUnprocessableHtmlTag('p');
  174. $html = $this->html5DocumentType . self::LF . '<html><p>foo<br/><span>bar</span></p></html>' . self::LF;
  175. $this->subject->setHtml($html);
  176. $this->assertContains(
  177. '<p>',
  178. $this->subject->emogrify()
  179. );
  180. }
  181. /**
  182. * @test
  183. */
  184. public function emogrifyCanAddMatchingElementRuleOnHtmlElementFromCss()
  185. {
  186. $html = $this->html5DocumentType . self::LF . '<html></html>' . self::LF;
  187. $this->subject->setHtml($html);
  188. $styleRule = 'color: #000;';
  189. $this->subject->setCss('html {' . $styleRule . '}');
  190. $this->assertContains(
  191. '<html style="' . $styleRule . '">',
  192. $this->subject->emogrify()
  193. );
  194. }
  195. /**
  196. * @test
  197. */
  198. public function emogrifyNotAddsNotMatchingElementRuleOnHtmlElementFromCss()
  199. {
  200. $html = $this->html5DocumentType . self::LF . '<html></html>' . self::LF;
  201. $this->subject->setHtml($html);
  202. $this->subject->setCss('p {color:#000;}');
  203. $this->assertContains(
  204. '<html>',
  205. $this->subject->emogrify()
  206. );
  207. }
  208. /**
  209. * @test
  210. */
  211. public function emogrifyCanMatchTwoElements()
  212. {
  213. $html = $this->html5DocumentType . self::LF . '<html><p></p><p></p></html>' . self::LF;
  214. $this->subject->setHtml($html);
  215. $styleRule = 'color: #000;';
  216. $this->subject->setCss('p {' . $styleRule . '}');
  217. $this->assertSame(
  218. 2,
  219. substr_count($this->subject->emogrify(), '<p style="' . $styleRule . '">')
  220. );
  221. }
  222. /**
  223. * @test
  224. */
  225. public function emogrifyCanAssignTwoStyleRulesFromSameMatcherToElement()
  226. {
  227. $html = $this->html5DocumentType . self::LF . '<html><p></p></html>' . self::LF;
  228. $this->subject->setHtml($html);
  229. $styleRulesIn = 'color:#000; text-align:left;';
  230. $styleRulesOut = 'color: #000; text-align: left;';
  231. $this->subject->setCss('p {' . $styleRulesIn . '}');
  232. $this->assertContains(
  233. '<p style="' . $styleRulesOut . '">',
  234. $this->subject->emogrify()
  235. );
  236. }
  237. /**
  238. * @test
  239. */
  240. public function emogrifyCanMatchAttributeOnlySelector()
  241. {
  242. $html = $this->html5DocumentType . self::LF . '<html><p hidden="hidden"></p></html>' . self::LF;
  243. $this->subject->setHtml($html);
  244. $this->subject->setCss('[hidden] { color:red; }');
  245. $this->assertContains(
  246. '<p hidden="hidden" style="color: red;">',
  247. $this->subject->emogrify()
  248. );
  249. }
  250. /**
  251. * @test
  252. */
  253. public function emogrifyCanAssignStyleRulesFromTwoIdenticalMatchersToElement()
  254. {
  255. $html = $this->html5DocumentType . self::LF . '<html><p></p></html>' . self::LF;
  256. $this->subject->setHtml($html);
  257. $styleRule1 = 'color: #000;';
  258. $styleRule2 = 'text-align: left;';
  259. $this->subject->setCss('p {' . $styleRule1 . '} p {' . $styleRule2 . '}');
  260. $this->assertContains(
  261. '<p style="' . $styleRule1 . ' ' . $styleRule2 . '">',
  262. $this->subject->emogrify()
  263. );
  264. }
  265. /**
  266. * @test
  267. */
  268. public function emogrifyCanAssignStyleRulesFromTwoDifferentMatchersToElement()
  269. {
  270. $html = $this->html5DocumentType . self::LF . '<html><p class="x"></p></html>' . self::LF;
  271. $this->subject->setHtml($html);
  272. $styleRule1 = 'color: #000;';
  273. $styleRule2 = 'text-align: left;';
  274. $this->subject->setCss('p {' . $styleRule1 . '} .x {' . $styleRule2 . '}');
  275. $this->assertContains(
  276. '<p class="x" style="' . $styleRule1 . ' ' . $styleRule2 . '">',
  277. $this->subject->emogrify()
  278. );
  279. }
  280. /**
  281. * Data provide for selectors.
  282. *
  283. * @return array[]
  284. */
  285. public function selectorDataProvider()
  286. {
  287. $styleRule = 'color: red;';
  288. $styleAttribute = 'style="' . $styleRule . '"';
  289. return array(
  290. 'universal selector HTML'
  291. => array('* {' . $styleRule . '} ', '#<html id="html" ' . $styleAttribute . '>#'),
  292. 'universal selector BODY'
  293. => array('* {' . $styleRule . '} ', '#<body ' . $styleAttribute . '>#'),
  294. 'universal selector P'
  295. => array('* {' . $styleRule . '} ', '#<p[^>]*' . $styleAttribute . '>#'),
  296. 'type selector matches first P'
  297. => array('p {' . $styleRule . '} ', '#<p class="p-1" ' . $styleAttribute . '>#'),
  298. 'type selector matches second P'
  299. => array('p {' . $styleRule . '} ', '#<p class="p-2" ' . $styleAttribute . '>#'),
  300. 'descendant selector P SPAN'
  301. => array('p span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'),
  302. 'descendant selector BODY SPAN'
  303. => array('body span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'),
  304. 'child selector P > SPAN matches direct child'
  305. => array('p > span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'),
  306. 'child selector BODY > SPAN not matches grandchild'
  307. => array('body > span {' . $styleRule . '} ', '#<span>#'),
  308. 'adjacent selector P + P not matches first P' => array('p + p {' . $styleRule . '} ', '#<p class="p-1">#'),
  309. 'adjacent selector P + P matches second P'
  310. => array('p + p {' . $styleRule . '} ', '#<p class="p-2" style="' . $styleRule . '">#'),
  311. 'adjacent selector P + P matches third P'
  312. => array('p + p {' . $styleRule . '} ', '#<p class="p-3" style="' . $styleRule . '">#'),
  313. 'ID selector #HTML' => array('#html {' . $styleRule . '} ', '#<html id="html" ' . $styleAttribute . '>#'),
  314. 'type and ID selector HTML#HTML'
  315. => array('html#html {' . $styleRule . '} ', '#<html id="html" ' . $styleAttribute . '>#'),
  316. 'class selector .P-1' => array('.p-1 {' . $styleRule . '} ', '#<p class="p-1" ' . $styleAttribute . '>#'),
  317. 'type and class selector P.P-1'
  318. => array('p.p-1 {' . $styleRule . '} ', '#<p class="p-1" ' . $styleAttribute . '>#'),
  319. 'attribute presence selector SPAN[title] matches element with matching attribute'
  320. => array('span[title] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'),
  321. 'attribute presence selector SPAN[title] not matches element without any attributes'
  322. => array('span[title] {' . $styleRule . '} ', '#<span>#'),
  323. 'attribute value selector SPAN[title] matches element with matching attribute value' => array(
  324. 'span[title="bonjour"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'
  325. ),
  326. 'attribute value selector SPAN[title] not matches element with other attribute value'
  327. => array('span[title="bonjour"] {' . $styleRule . '} ', '#<span title="buenas dias">#'),
  328. 'attribute value selector SPAN[title] not matches element without any attributes'
  329. => array('span[title="bonjour"] {' . $styleRule . '} ', '#<span>#'),
  330. );
  331. }
  332. /**
  333. * @test
  334. *
  335. * @param string $css the complete CSS
  336. * @param string $containedHtml regular expression for the the HTML that needs to be contained in the merged HTML
  337. *
  338. * @dataProvider selectorDataProvider
  339. */
  340. public function emogrifierMatchesSelectors($css, $containedHtml)
  341. {
  342. $html = $this->html5DocumentType . self::LF .
  343. '<html id="html">' .
  344. ' <body>' .
  345. ' <p class="p-1"><span>some text</span></p>' .
  346. ' <p class="p-2"><span title="bonjour">some</span> text</p>' .
  347. ' <p class="p-3"><span title="buenas dias">some</span> more text</p>' .
  348. ' </body>' .
  349. '</html>';
  350. $this->subject->setHtml($html);
  351. $this->subject->setCss($css);
  352. $this->assertRegExp(
  353. $containedHtml,
  354. $this->subject->emogrify()
  355. );
  356. }
  357. /**
  358. * Data provider for emogrifyDropsWhitespaceFromCssDeclarations.
  359. *
  360. * @return array[]
  361. */
  362. public function cssDeclarationWhitespaceDroppingDataProvider()
  363. {
  364. return array(
  365. 'no whitespace, trailing semicolon' => array('color:#000;', 'color: #000;'),
  366. 'no whitespace, no trailing semicolon' => array('color:#000', 'color: #000;'),
  367. 'space after colon, no trailing semicolon' => array('color: #000', 'color: #000;'),
  368. 'space before colon, no trailing semicolon' => array('color :#000', 'color: #000;'),
  369. 'space before property name, no trailing semicolon' => array(' color:#000', 'color: #000;'),
  370. 'space before trailing semicolon' => array(' color:#000 ;', 'color: #000;'),
  371. 'space after trailing semicolon' => array(' color:#000; ', 'color: #000;'),
  372. 'space after property value, no trailing semicolon' => array(' color:#000; ', 'color: #000;'),
  373. );
  374. }
  375. /**
  376. * @test
  377. *
  378. * @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
  379. * @param string $expectedStyleAttributeContent the expected value of the style attribute
  380. *
  381. * @dataProvider cssDeclarationWhitespaceDroppingDataProvider
  382. */
  383. public function emogrifyDropsLeadingAndTrailingWhitespaceFromCssDeclarations(
  384. $cssDeclarationBlock,
  385. $expectedStyleAttributeContent
  386. ) {
  387. $html = $this->html5DocumentType . self::LF . '<html></html>';
  388. $css = 'html {' . $cssDeclarationBlock . '}';
  389. $this->subject->setHtml($html);
  390. $this->subject->setCss($css);
  391. $this->assertContains(
  392. 'html style="' . $expectedStyleAttributeContent . '">',
  393. $this->subject->emogrify()
  394. );
  395. }
  396. /**
  397. * Data provider for emogrifyFormatsCssDeclarations.
  398. *
  399. * @return array[]
  400. */
  401. public function formattedCssDeclarationDataProvider()
  402. {
  403. return array(
  404. 'one declaration' => array('color: #000;', 'color: #000;'),
  405. 'one declaration with dash in property name' => array('font-weight: bold;', 'font-weight: bold;'),
  406. 'one declaration with space in property value' => array('margin: 0 4px;', 'margin: 0 4px;'),
  407. 'two declarations separated by semicolon' => array('color: #000;width: 3px;', 'color: #000; width: 3px;'),
  408. 'two declarations separated by semicolon and space'
  409. => array('color: #000; width: 3px;', 'color: #000; width: 3px;'),
  410. 'two declaration separated by semicolon and Linefeed' => array(
  411. 'color: #000;' . self::LF . 'width: 3px;', 'color: #000; width: 3px;'
  412. ),
  413. );
  414. }
  415. /**
  416. * @test
  417. *
  418. * @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
  419. * @param string $expectedStyleAttributeContent the expected value of the style attribute
  420. *
  421. * @dataProvider formattedCssDeclarationDataProvider
  422. */
  423. public function emogrifyFormatsCssDeclarations($cssDeclarationBlock, $expectedStyleAttributeContent)
  424. {
  425. $html = $this->html5DocumentType . self::LF . '<html></html>';
  426. $css = 'html {' . $cssDeclarationBlock . '}';
  427. $this->subject->setHtml($html);
  428. $this->subject->setCss($css);
  429. $this->assertContains(
  430. 'html style="' . $expectedStyleAttributeContent . '">',
  431. $this->subject->emogrify()
  432. );
  433. }
  434. /**
  435. * @test
  436. */
  437. public function emogrifyKeepsExistingStyleAttributes()
  438. {
  439. $styleAttribute = 'style="color: #ccc;"';
  440. $html = $this->html5DocumentType . self::LF . '<html ' . $styleAttribute . '></html>';
  441. $this->subject->setHtml($html);
  442. $this->assertContains(
  443. $styleAttribute,
  444. $this->subject->emogrify()
  445. );
  446. }
  447. /**
  448. * @test
  449. */
  450. public function emogrifyAddsCssAfterExistingStyle()
  451. {
  452. $styleAttributeValue = 'color: #ccc;';
  453. $html = $this->html5DocumentType . self::LF . '<html style="' . $styleAttributeValue . '"></html>';
  454. $this->subject->setHtml($html);
  455. $cssDeclarations = 'margin: 0 2px;';
  456. $css = 'html {' . $cssDeclarations . '}';
  457. $this->subject->setCss($css);
  458. $this->assertContains(
  459. 'style="' . $styleAttributeValue . ' ' . $cssDeclarations . '"',
  460. $this->subject->emogrify()
  461. );
  462. }
  463. /**
  464. * @test
  465. */
  466. public function emogrifyCanMatchMinifiedCss()
  467. {
  468. $html = $this->html5DocumentType . self::LF . '<html><p></p></html>' . self::LF;
  469. $this->subject->setHtml($html);
  470. $this->subject->setCss('p{color:blue;}html{color:red;}');
  471. $this->assertContains(
  472. '<html style="color: red;">',
  473. $this->subject->emogrify()
  474. );
  475. }
  476. /**
  477. * @test
  478. */
  479. public function emogrifyLowercasesAttributeNamesFromStyleAttributes()
  480. {
  481. $html = $this->html5DocumentType . self::LF . '<html style="COLOR:#ccc;"></html>';
  482. $this->subject->setHtml($html);
  483. $this->assertContains(
  484. 'style="color: #ccc;"',
  485. $this->subject->emogrify()
  486. );
  487. }
  488. /**
  489. * @test
  490. */
  491. public function emogrifyLowerCasesAttributeNames()
  492. {
  493. $html = $this->html5DocumentType . self::LF . '<html></html>';
  494. $this->subject->setHtml($html);
  495. $cssIn = 'html {mArGiN:0 2pX;}';
  496. $cssOut = 'margin: 0 2pX;';
  497. $this->subject->setCss($cssIn);
  498. $this->assertContains(
  499. 'style="' . $cssOut . '"',
  500. $this->subject->emogrify()
  501. );
  502. }
  503. /**
  504. * @test
  505. */
  506. public function emogrifyPreservesCaseForAttributeValuesFromPassedInCss()
  507. {
  508. $css = "content: 'Hello World';";
  509. $html = $this->html5DocumentType . self::LF . '<html><body><p>target</p></body></html>';
  510. $this->subject->setHtml($html);
  511. $this->subject->setCss('p {' . $css . '}');
  512. $this->assertContains(
  513. '<p style="' . $css . '">target</p>',
  514. $this->subject->emogrify()
  515. );
  516. }
  517. /**
  518. * @test
  519. */
  520. public function emogrifyPreservesCaseForAttributeValuesFromParsedStyleBlock()
  521. {
  522. $css = "content: 'Hello World';";
  523. $html = $this->html5DocumentType . self::LF . '<html><head><style>p {'
  524. . $css . '}</style></head><body><p>target</p></body></html>';
  525. $this->subject->setHtml($html);
  526. $this->assertContains(
  527. '<p style="' . $css . '">target</p>',
  528. $this->subject->emogrify()
  529. );
  530. }
  531. /**
  532. * @test
  533. */
  534. public function emogrifyRemovesStyleNodes()
  535. {
  536. $html = $this->html5DocumentType . self::LF . '<html><style type="text/css"></style></html>';
  537. $this->subject->setHtml($html);
  538. $this->assertNotContains(
  539. '<style>',
  540. $this->subject->emogrify()
  541. );
  542. }
  543. /**
  544. * Data provider for things that should be left out when applying the CSS.
  545. *
  546. * @return array[]
  547. */
  548. public function unneededCssThingsDataProvider()
  549. {
  550. return array(
  551. 'CSS comments with one asterisk' => array('p {color: #000;/* black */}', 'black'),
  552. 'CSS comments with two asterisks' => array('p {color: #000;/** black */}', 'black'),
  553. '@import directive' => array('@import "foo.css";', '@import'),
  554. 'style in "aural" media type rule' => array('@media aural {p {color: #000;}}', '#000'),
  555. 'style in "braille" media type rule' => array('@media braille {p {color: #000;}}', '#000'),
  556. 'style in "embossed" media type rule' => array('@media embossed {p {color: #000;}}', '#000'),
  557. 'style in "handheld" media type rule' => array('@media handheld {p {color: #000;}}', '#000'),
  558. 'style in "print" media type rule' => array('@media print {p {color: #000;}}', '#000'),
  559. 'style in "projection" media type rule' => array('@media projection {p {color: #000;}}', '#000'),
  560. 'style in "speech" media type rule' => array('@media speech {p {color: #000;}}', '#000'),
  561. 'style in "tty" media type rule' => array('@media tty {p {color: #000;}}', '#000'),
  562. 'style in "tv" media type rule' => array('@media tv {p {color: #000;}}', '#000'),
  563. );
  564. }
  565. /**
  566. * @test
  567. *
  568. * @param string $css
  569. * @param string $markerNotExpectedInHtml
  570. *
  571. * @dataProvider unneededCssThingsDataProvider
  572. */
  573. public function emogrifyFiltersUnneededCssThings($css, $markerNotExpectedInHtml)
  574. {
  575. $html = $this->html5DocumentType . self::LF . '<html><p>foo</p></html>';
  576. $this->subject->setHtml($html);
  577. $this->subject->setCss($css);
  578. $this->assertNotContains(
  579. $markerNotExpectedInHtml,
  580. $this->subject->emogrify()
  581. );
  582. }
  583. /**
  584. * Data provider for media rules.
  585. *
  586. * @return array[]
  587. */
  588. public function mediaRulesDataProvider()
  589. {
  590. return array(
  591. 'style in "only all" media type rule' => array('@media only all {p {color: #000;}}'),
  592. 'style in "only screen" media type rule' => array('@media only screen {p {color: #000;}}'),
  593. 'style in media type rule' => array('@media {p {color: #000;}}'),
  594. 'style in "screen" media type rule' => array('@media screen {p {color: #000;}}'),
  595. 'style in "all" media type rule' => array('@media all {p {color: #000;}}'),
  596. );
  597. }
  598. /**
  599. * @test
  600. *
  601. * @param string $css
  602. *
  603. * @dataProvider mediaRulesDataProvider
  604. */
  605. public function emogrifyKeepsMediaRules($css)
  606. {
  607. $html = $this->html5DocumentType . self::LF . '<html><p>foo</p></html>';
  608. $this->subject->setHtml($html);
  609. $this->subject->setCss($css);
  610. $this->assertContains(
  611. $css,
  612. $this->subject->emogrify()
  613. );
  614. }
  615. /**
  616. * @test
  617. */
  618. public function emogrifyAddsMissingHeadElement()
  619. {
  620. $html = $this->html5DocumentType . self::LF . '<html></html>';
  621. $this->subject->setHtml($html);
  622. $this->subject->setCss('@media all { html {} }');
  623. $this->assertContains(
  624. '<head>',
  625. $this->subject->emogrify()
  626. );
  627. }
  628. /**
  629. * @test
  630. */
  631. public function emogrifyKeepExistingHeadElementContent()
  632. {
  633. $html = $this->html5DocumentType . self::LF . '<html><head><!-- original content --></head></html>';
  634. $this->subject->setHtml($html);
  635. $this->subject->setCss('@media all { html {} }');
  636. $this->assertContains(
  637. '<!-- original content -->',
  638. $this->subject->emogrify()
  639. );
  640. }
  641. /**
  642. * @test
  643. */
  644. public function emogrifyKeepExistingHeadElementAddStyleElement()
  645. {
  646. $html = $this->html5DocumentType . self::LF . '<html><head><!-- original content --></head></html>';
  647. $this->subject->setHtml($html);
  648. $this->subject->setCss('@media all { html {} }');
  649. $this->assertContains(
  650. '<style type="text/css">',
  651. $this->subject->emogrify()
  652. );
  653. }
  654. /**
  655. * Valid media query which need to be preserved
  656. *
  657. * @return array[]
  658. */
  659. public function validMediaPreserveDataProvider()
  660. {
  661. return array(
  662. 'style in "only screen and size" media type rule' => array(
  663. '@media only screen and (min-device-width: 320px) and (max-device-width: 480px) '
  664. . '{ h1 { color:red; } }'
  665. ),
  666. 'style in "screen size" media type rule' => array(
  667. '@media screen and (min-device-width: 320px) and (max-device-width: 480px) '
  668. . '{ h1 { color:red; } }'
  669. ),
  670. 'style in "only screen and screen size" media type rule' => array(
  671. '@media only screen and (min-device-width: 320px) and (max-device-width: 480px) '
  672. . '{ h1 { color:red; } }'
  673. ),
  674. 'style in "all and screen size" media type rule' => array(
  675. '@media all and (min-device-width: 320px) and (max-device-width: 480px) '
  676. . '{ h1 { color:red; } }'
  677. ),
  678. 'style in "only all and" media type rule' => array(
  679. '@media only all and (min-device-width: 320px) and (max-device-width: 480px) '
  680. . '{ h1 { color:red; } }'
  681. ),
  682. 'style in "all" media type rule' => array('@media all {p {color: #000;}}'),
  683. 'style in "only screen" media type rule' => array('@media only screen { h1 { color:red; } }'),
  684. 'style in "only all" media type rule' => array('@media only all { h1 { color:red; } }'),
  685. 'style in "screen" media type rule' => array('@media screen { h1 { color:red; } }'),
  686. 'style in media type rule without specification' => array('@media { h1 { color:red; } }'),
  687. );
  688. }
  689. /**
  690. * @test
  691. *
  692. * @param string $css
  693. *
  694. * @dataProvider validMediaPreserveDataProvider
  695. */
  696. public function emogrifyWithValidMediaQueryContainsInnerCss($css)
  697. {
  698. $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
  699. $this->subject->setHtml($html);
  700. $this->subject->setCss($css);
  701. $this->assertContains(
  702. $css,
  703. $this->subject->emogrify()
  704. );
  705. }
  706. /**
  707. * @test
  708. *
  709. * @param string $css
  710. *
  711. * @dataProvider validMediaPreserveDataProvider
  712. */
  713. public function emogrifyForHtmlWithValidMediaQueryContainsInnerCss($css)
  714. {
  715. $html = $this->html5DocumentType . PHP_EOL . '<html><style type="text/css">' . $css
  716. . '</style><h1></h1></html>';
  717. $this->subject->setHtml($html);
  718. $this->assertContains(
  719. $css,
  720. $this->subject->emogrify()
  721. );
  722. }
  723. /**
  724. * @test
  725. *
  726. * @param string $css
  727. *
  728. * @dataProvider validMediaPreserveDataProvider
  729. */
  730. public function emogrifyWithValidMediaQueryNotContainsInlineCss($css)
  731. {
  732. $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
  733. $this->subject->setHtml($html);
  734. $this->subject->setCss($css);
  735. $this->assertNotContains(
  736. 'style="color:red"',
  737. $this->subject->emogrify()
  738. );
  739. }
  740. /**
  741. * Invalid media query which need to be strip
  742. *
  743. * @return array[]
  744. */
  745. public function invalidMediaPreserveDataProvider()
  746. {
  747. return array(
  748. 'style in "braille" type rule' => array('@media braille { h1 { color:red; } }'),
  749. 'style in "embossed" type rule' => array('@media embossed { h1 { color:red; } }'),
  750. 'style in "handheld" type rule' => array('@media handheld { h1 { color:red; } }'),
  751. 'style in "print" type rule' => array('@media print { h1 { color:red; } }'),
  752. 'style in "projection" type rule' => array('@media projection { h1 { color:red; } }'),
  753. 'style in "speech" type rule' => array('@media speech { h1 { color:red; } }'),
  754. 'style in "tty" type rule' => array('@media tty { h1 { color:red; } }'),
  755. 'style in "tv" type rule' => array('@media tv { h1 { color:red; } }'),
  756. );
  757. }
  758. /**
  759. * @test
  760. *
  761. * @param string $css
  762. *
  763. * @dataProvider invalidMediaPreserveDataProvider
  764. */
  765. public function emogrifyWithInvalidMediaQueryaNotContainsInnerCss($css)
  766. {
  767. $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
  768. $this->subject->setHtml($html);
  769. $this->subject->setCss($css);
  770. $this->assertNotContains(
  771. $css,
  772. $this->subject->emogrify()
  773. );
  774. }
  775. /**
  776. * @test
  777. *
  778. * @param string $css
  779. *
  780. * @dataProvider invalidMediaPreserveDataProvider
  781. */
  782. public function emogrifyWithInValidMediaQueryNotContainsInlineCss($css)
  783. {
  784. $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
  785. $this->subject->setHtml($html);
  786. $this->subject->setCss($css);
  787. $this->assertNotContains(
  788. 'style="color: red"',
  789. $this->subject->emogrify()
  790. );
  791. }
  792. /**
  793. * @test
  794. *
  795. * @param string $css
  796. *
  797. * @dataProvider invalidMediaPreserveDataProvider
  798. */
  799. public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInnerCss($css)
  800. {
  801. $html = $this->html5DocumentType . PHP_EOL . '<html><style type="text/css">' . $css
  802. . '</style><h1></h1></html>';
  803. $this->subject->setHtml($html);
  804. $this->assertNotContains(
  805. $css,
  806. $this->subject->emogrify()
  807. );
  808. }
  809. /**
  810. * @test
  811. *
  812. * @param string $css
  813. *
  814. * @dataProvider invalidMediaPreserveDataProvider
  815. */
  816. public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInlineCss($css)
  817. {
  818. $html = $this->html5DocumentType . PHP_EOL . '<html><style type="text/css">' . $css
  819. . '</style><h1></h1></html>';
  820. $this->subject->setHtml($html);
  821. $this->assertNotContains(
  822. 'style="color: red"',
  823. $this->subject->emogrify()
  824. );
  825. }
  826. /**
  827. * @test
  828. */
  829. public function emogrifyAppliesCssFromStyleNodes()
  830. {
  831. $styleAttributeValue = 'color: #ccc;';
  832. $html = $this->html5DocumentType . self::LF .
  833. '<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>';
  834. $this->subject->setHtml($html);
  835. $this->assertContains(
  836. '<html style="' . $styleAttributeValue . '">',
  837. $this->subject->emogrify()
  838. );
  839. }
  840. /**
  841. * @test
  842. */
  843. public function emogrifyWhenDisabledNotAppliesCssFromStyleBlocks()
  844. {
  845. $styleAttributeValue = 'color: #ccc;';
  846. $html = $this->html5DocumentType . self::LF .
  847. '<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>';
  848. $this->subject->setHtml($html);
  849. $this->subject->disableStyleBlocksParsing();
  850. $this->assertNotContains(
  851. '<html style="' . $styleAttributeValue . '">',
  852. $this->subject->emogrify()
  853. );
  854. }
  855. /**
  856. * @test
  857. */
  858. public function emogrifyWhenStyleBlocksParsingDisabledKeepInlineStyles()
  859. {
  860. $styleAttributeValue = 'text-align: center;';
  861. $html = $this->html5DocumentType . self::LF .
  862. '<html><head><style type="text/css">p { color: #ccc; }</style></head>'
  863. . '<body><p style="' . $styleAttributeValue . '">paragraph</p></body></html>';
  864. $expected = '<p style="' . $styleAttributeValue . '">';
  865. $this->subject->setHtml($html);
  866. $this->subject->disableStyleBlocksParsing();
  867. $this->assertContains(
  868. $expected,
  869. $this->subject->emogrify()
  870. );
  871. }
  872. /**
  873. * @test
  874. */
  875. public function emogrifyWhenDisabledNotAppliesCssFromInlineStyles()
  876. {
  877. $styleAttributeValue = 'color: #ccc;';
  878. $html = $this->html5DocumentType . self::LF .
  879. '<html style="' . $styleAttributeValue . '"></html>';
  880. $expected = '<html></html>';
  881. $this->subject->setHtml($html);
  882. $this->subject->disableInlineStyleAttributesParsing();
  883. $this->assertContains(
  884. $expected,
  885. $this->subject->emogrify()
  886. );
  887. }
  888. /**
  889. * @test
  890. */
  891. public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles()
  892. {
  893. $styleAttributeValue = 'color: #ccc;';
  894. $html = $this->html5DocumentType . self::LF .
  895. '<html><head><style type="text/css">p { ' . $styleAttributeValue . ' }</style></head>'
  896. . '<body><p style="text-align: center;">paragraph</p></body></html>';
  897. $expected = '<p style="' . $styleAttributeValue . '">';
  898. $this->subject->setHtml($html);
  899. $this->subject->disableInlineStyleAttributesParsing();
  900. $this->assertContains(
  901. $expected,
  902. $this->subject->emogrify()
  903. );
  904. }
  905. /**
  906. * @test
  907. */
  908. public function emogrifyAppliesCssWithUpperCaseSelector()
  909. {
  910. $html = $this->html5DocumentType . self::LF .
  911. '<html><style type="text/css">P { color:#ccc; }</style><body><p>paragraph</p></body></html>';
  912. $expected = '<p style="color: #ccc;">';
  913. $this->subject->setHtml($html);
  914. $this->assertContains(
  915. $expected,
  916. $this->subject->emogrify()
  917. );
  918. }
  919. /**
  920. * Emogrify was handling case differently for passed in CSS vs CSS parsed from style blocks.
  921. * @test
  922. */
  923. public function emogrifyAppliesCssWithMixedCaseAttributesInStyleBlock()
  924. {
  925. $html = $this->html5DocumentType . self::LF .
  926. '<html><head><style>#topWrap p {padding-bottom: 1px;PADDING-TOP: 0;}</style></head>'
  927. . '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>';
  928. $expected = '<p style="text-align: center; padding-bottom: 1px; padding-top: 0;">';
  929. $this->subject->setHtml($html);
  930. $this->assertContains(
  931. $expected,
  932. $this->subject->emogrify()
  933. );
  934. }
  935. /**
  936. * Passed in CSS sets the order, but style block CSS overrides values.
  937. * @test
  938. */
  939. public function emogrifyMergesCssWithMixedCaseAttribute()
  940. {
  941. $css = 'p { margin: 0; padding-TOP: 0; PADDING-bottom: 1PX;}';
  942. $html = $this->html5DocumentType . self::LF .
  943. '<html><head><style>#topWrap p {padding-bottom: 3px;PADDING-TOP: 1px;}</style></head>'
  944. . '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>';
  945. $expected = '<p style="text-align: center; margin: 0; padding-top: 1px; padding-bottom: 3px;">';
  946. $this->subject->setHtml($html);
  947. $this->subject->setCss($css);
  948. $this->assertContains(
  949. $expected,
  950. $this->subject->emogrify()
  951. );
  952. }
  953. /**
  954. * @test
  955. */
  956. public function emogrifyMergesCssWithMixedUnits()
  957. {
  958. $css = 'p { margin: 1px; padding-bottom:0;}';
  959. $html = $this->html5DocumentType . self::LF .
  960. '<html><head><style>#topWrap p {margin:0;padding-bottom: 1px;}</style></head>'
  961. . '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>';
  962. $expected = '<p style="text-align: center; margin: 0; padding-bottom: 1px;">';
  963. $this->subject->setHtml($html);
  964. $this->subject->setCss($css);
  965. $this->assertContains(
  966. $expected,
  967. $this->subject->emogrify()
  968. );
  969. }
  970. }