EmogrifierTest.php 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221
  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 = '
  15. ';
  16. /**
  17. * @var string
  18. */
  19. private $html4TransitionalDocumentType = '';
  20. /**
  21. * @var string
  22. */
  23. private $xhtml1StrictDocumentType = '';
  24. /**
  25. * @var string
  26. */
  27. private $html5DocumentType = '<!DOCTYPE html>';
  28. /**
  29. * @var Emogrifier
  30. */
  31. private $subject = null;
  32. /**
  33. * Sets up the test case.
  34. *
  35. * @return void
  36. */
  37. protected function setUp()
  38. {
  39. $this->html4TransitionalDocumentType = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' . '"http://www.w3.org/TR/REC-html40/loose.dtd">';
  40. $this->xhtml1StrictDocumentType = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' . '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
  41. $this->subject = new Emogrifier();
  42. }
  43. /**
  44. * @test
  45. *
  46. * @expectedException \BadMethodCallException
  47. */
  48. public function emogrifyForNoDataSetReturnsThrowsException()
  49. {
  50. $this->subject->emogrify();
  51. }
  52. /**
  53. * @test
  54. *
  55. * @expectedException \BadMethodCallException
  56. */
  57. public function emogrifyForEmptyHtmlAndEmptyCssThrowsException()
  58. {
  59. $this->subject->setHtml('');
  60. $this->subject->setCss('');
  61. $this->subject->emogrify();
  62. }
  63. /**
  64. * @test
  65. *
  66. * @expectedException \BadMethodCallException
  67. */
  68. public function emogrifyBodyContentForNoDataSetReturnsThrowsException()
  69. {
  70. $this->subject->emogrifyBodyContent();
  71. }
  72. /**
  73. * @test
  74. *
  75. * @expectedException \BadMethodCallException
  76. */
  77. public function emogrifyBodyContentForEmptyHtmlAndEmptyCssThrowsException()
  78. {
  79. $this->subject->setHtml('');
  80. $this->subject->setCss('');
  81. $this->subject->emogrifyBodyContent();
  82. }
  83. /**
  84. * @test
  85. */
  86. public function emogrifyAddsHtmlTagIfNoHtmlTagAndNoHeadTagAreProvided()
  87. {
  88. $this->subject->setHtml('<p>Hello</p>');
  89. $emogrifiedHtml = $this->subject->emogrify();
  90. self::assertContains('<html>', $emogrifiedHtml);
  91. }
  92. /**
  93. * @test
  94. */
  95. public function emogrifyAddsHtmlTagIfHeadTagIsProvidedButNoHtmlTaqg()
  96. {
  97. $this->subject->setHtml('<head><title>Hello</title></head><p>World</p>');
  98. $emogrifiedHtml = $this->subject->emogrify();
  99. self::assertContains('<html>', $emogrifiedHtml);
  100. }
  101. /**
  102. * @test
  103. */
  104. public function emogrifyAddsHeadTagIfNoHtmlTagAndNoHeadTagAreProvided()
  105. {
  106. $this->subject->setHtml('<p>Hello</p>');
  107. $emogrifiedHtml = $this->subject->emogrify();
  108. self::assertContains('<head>', $emogrifiedHtml);
  109. }
  110. /**
  111. * @test
  112. */
  113. public function emogrifyAddsHtmlTagIfHtmlTagIsProvidedButNoHeadTaqg()
  114. {
  115. $this->subject->setHtml('<html></head><p>World</p></html>');
  116. $emogrifiedHtml = $this->subject->emogrify();
  117. self::assertContains('<head>', $emogrifiedHtml);
  118. }
  119. /**
  120. * @test
  121. */
  122. public function emogrifyKeepsDollarSignsAndSquareBrackets()
  123. {
  124. $templateMarker = '$[USER:NAME]$';
  125. $html = $this->html5DocumentType . '<html><p>' . $templateMarker . '</p></html>';
  126. $this->subject->setHtml($html);
  127. self::assertContains($templateMarker, $this->subject->emogrify());
  128. }
  129. /**
  130. * @test
  131. */
  132. public function emogrifyKeepsUtf8UmlautsInHtml5()
  133. {
  134. $umlautString = 'Küss die Hand, schöne Frau.';
  135. $html = $this->html5DocumentType . '<html><p>' . $umlautString . '</p></html>';
  136. $this->subject->setHtml($html);
  137. self::assertContains($umlautString, $this->subject->emogrify());
  138. }
  139. /**
  140. * @test
  141. */
  142. public function emogrifyKeepsUtf8UmlautsInXhtml()
  143. {
  144. $umlautString = 'Öösel läks õunu täis ämber uhkelt ümber.';
  145. $html = $this->xhtml1StrictDocumentType . '<html<p>' . $umlautString . '</p></html>';
  146. $this->subject->setHtml($html);
  147. self::assertContains($umlautString, $this->subject->emogrify());
  148. }
  149. /**
  150. * @test
  151. */
  152. public function emogrifyKeepsUtf8UmlautsInHtml4()
  153. {
  154. $umlautString = 'Öösel läks õunu täis ämber uhkelt ümber.';
  155. $html = $this->html4TransitionalDocumentType . '<html><p>' . $umlautString . '</p></html>';
  156. $this->subject->setHtml($html);
  157. self::assertContains($umlautString, $umlautString, $this->subject->emogrify());
  158. }
  159. /**
  160. * @test
  161. */
  162. public function emogrifyKeepsHtmlEntities()
  163. {
  164. $entityString = 'a &amp; b &gt; c';
  165. $html = $this->html5DocumentType . '<html><p>' . $entityString . '</p></html>';
  166. $this->subject->setHtml($html);
  167. self::assertContains($entityString, $this->subject->emogrify());
  168. }
  169. /**
  170. * @test
  171. */
  172. public function emogrifyKeepsHtmlEntitiesInXhtml()
  173. {
  174. $entityString = 'a &amp; b &gt; c';
  175. $html = $this->xhtml1StrictDocumentType . '<html<p>' . $entityString . '</p></html>';
  176. $this->subject->setHtml($html);
  177. self::assertContains($entityString, $this->subject->emogrify());
  178. }
  179. /**
  180. * @test
  181. */
  182. public function emogrifyKeepsHtmlEntitiesInHtml4()
  183. {
  184. $entityString = 'a &amp; b &gt; c';
  185. $html = $this->html4TransitionalDocumentType . '<html><p>' . $entityString . '</p></html>';
  186. $this->subject->setHtml($html);
  187. self::assertContains($entityString, $entityString, $this->subject->emogrify());
  188. }
  189. /**
  190. * @test
  191. */
  192. public function emogrifyKeepsUtf8UmlautsWithoutDocumentType()
  193. {
  194. $umlautString = 'Küss die Hand, schöne Frau.';
  195. $html = '<html><head></head><p>' . $umlautString . '</p></html>';
  196. $this->subject->setHtml($html);
  197. self::assertContains($umlautString, $this->subject->emogrify());
  198. }
  199. /**
  200. * @test
  201. */
  202. public function emogrifyKeepsUtf8UmlautsWithoutDocumentTypeAndWithoutHtmlAndWithoutHead()
  203. {
  204. $umlautString = 'Küss die Hand, schöne Frau.';
  205. $html = '<p>' . $umlautString . '</p>';
  206. $this->subject->setHtml($html);
  207. self::assertContains($umlautString, $this->subject->emogrify());
  208. }
  209. /**
  210. * @test
  211. */
  212. public function emogrifyKeepsUtf8UmlautsWithoutDocumentTypeAndWithHtmlAndWithoutHead()
  213. {
  214. $umlautString = 'Küss die Hand, schöne Frau.';
  215. $html = '<html><p>' . $umlautString . '</p></html>';
  216. $this->subject->setHtml($html);
  217. self::assertContains($umlautString, $this->subject->emogrify());
  218. }
  219. /**
  220. * @test
  221. */
  222. public function emogrifyKeepsUtf8UmlautsWithoutDocumentTypeAndWithoutHtmlAndWithHead()
  223. {
  224. $umlautString = 'Küss die Hand, schöne Frau.';
  225. $html = '<head></head><p>' . $umlautString . '</p>';
  226. $this->subject->setHtml($html);
  227. self::assertContains($umlautString, $this->subject->emogrify());
  228. }
  229. /**
  230. * @test
  231. */
  232. public function emogrifyForHtmlTagOnlyAndEmptyCssByDefaultAddsHtml5DocumentType()
  233. {
  234. $html = '<html></html>';
  235. $this->subject->setHtml($html);
  236. $this->subject->setCss('');
  237. self::assertContains($this->html5DocumentType, $this->subject->emogrify());
  238. }
  239. /**
  240. * @test
  241. */
  242. public function emogrifyForHtmlTagWithXhtml1StrictDocumentTypeKeepsDocumentType()
  243. {
  244. $html = $this->xhtml1StrictDocumentType . '<html></html>';
  245. $this->subject->setHtml($html);
  246. self::assertContains($this->xhtml1StrictDocumentType, $this->subject->emogrify());
  247. }
  248. /**
  249. * @test
  250. */
  251. public function emogrifyForHtmlTagWithXhtml5DocumentTypeKeepsDocumentType()
  252. {
  253. $html = $this->html5DocumentType . '<html></html>';
  254. $this->subject->setHtml($html);
  255. self::assertContains($this->html5DocumentType, $this->subject->emogrify());
  256. }
  257. /**
  258. * @test
  259. */
  260. public function emogrifyAddsContentTypeMetaTag()
  261. {
  262. $html = $this->html5DocumentType . '<p>Hello</p>';
  263. $this->subject->setHtml($html);
  264. self::assertContains('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">', $this->subject->emogrify());
  265. }
  266. /**
  267. * @test
  268. */
  269. public function emogrifyForExistingContentTypeMetaTagNotAddsSecondContentTypeMetaTag()
  270. {
  271. $html = $this->html5DocumentType . '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' . '<body><p>Hello</p></body></html>';
  272. $this->subject->setHtml($html);
  273. $numberOfContentTypeMetaTags = substr_count($this->subject->emogrify(), 'Content-Type');
  274. self::assertSame(1, $numberOfContentTypeMetaTags);
  275. }
  276. /**
  277. * @test
  278. */
  279. public function emogrifyByDefaultRemovesWbrTag()
  280. {
  281. $html = $this->html5DocumentType . '<html>foo<wbr/>bar</html>';
  282. $this->subject->setHtml($html);
  283. self::assertContains('foobar', $this->subject->emogrify());
  284. }
  285. /**
  286. * @test
  287. */
  288. public function addUnprocessableTagCausesGivenEmptyTagToBeRemoved()
  289. {
  290. $this->subject->addUnprocessableHtmlTag('p');
  291. $html = $this->html5DocumentType . '<html><p></p></html>';
  292. $this->subject->setHtml($html);
  293. self::assertNotContains('<p>', $this->subject->emogrify());
  294. }
  295. /**
  296. * @test
  297. */
  298. public function addUnprocessableTagNotRemovesGivenTagWithContent()
  299. {
  300. $this->subject->addUnprocessableHtmlTag('p');
  301. $html = $this->html5DocumentType . '<html><p>foobar</p></html>';
  302. $this->subject->setHtml($html);
  303. self::assertContains('<p>', $this->subject->emogrify());
  304. }
  305. /**
  306. * @test
  307. */
  308. public function removeUnprocessableHtmlTagCausesTagToStayAgain()
  309. {
  310. $this->subject->addUnprocessableHtmlTag('p');
  311. $this->subject->removeUnprocessableHtmlTag('p');
  312. $html = $this->html5DocumentType . '<html><p>foo<br/><span>bar</span></p></html>';
  313. $this->subject->setHtml($html);
  314. self::assertContains('<p>', $this->subject->emogrify());
  315. }
  316. /**
  317. * @test
  318. */
  319. public function emogrifyCanAddMatchingElementRuleOnHtmlElementFromCss()
  320. {
  321. $html = $this->html5DocumentType . '<html></html>';
  322. $this->subject->setHtml($html);
  323. $styleRule = 'color: #000;';
  324. $this->subject->setCss('html {' . $styleRule . '}');
  325. self::assertContains('<html style="' . $styleRule . '">', $this->subject->emogrify());
  326. }
  327. /**
  328. * @test
  329. */
  330. public function emogrifyNotAddsNotMatchingElementRuleOnHtmlElementFromCss()
  331. {
  332. $html = $this->html5DocumentType . '<html></html>';
  333. $this->subject->setHtml($html);
  334. $this->subject->setCss('p {color:#000;}');
  335. self::assertContains('<html>', $this->subject->emogrify());
  336. }
  337. /**
  338. * @test
  339. */
  340. public function emogrifyCanMatchTwoElements()
  341. {
  342. $html = $this->html5DocumentType . '<html><p></p><p></p></html>';
  343. $this->subject->setHtml($html);
  344. $styleRule = 'color: #000;';
  345. $this->subject->setCss('p {' . $styleRule . '}');
  346. self::assertSame(2, substr_count($this->subject->emogrify(), '<p style="' . $styleRule . '">'));
  347. }
  348. /**
  349. * @test
  350. */
  351. public function emogrifyCanAssignTwoStyleRulesFromSameMatcherToElement()
  352. {
  353. $html = $this->html5DocumentType . '<html><p></p></html>';
  354. $this->subject->setHtml($html);
  355. $styleRulesIn = 'color:#000; text-align:left;';
  356. $styleRulesOut = 'color: #000; text-align: left;';
  357. $this->subject->setCss('p {' . $styleRulesIn . '}');
  358. self::assertContains('<p style="' . $styleRulesOut . '">', $this->subject->emogrify());
  359. }
  360. /**
  361. * @test
  362. */
  363. public function emogrifyCanMatchAttributeOnlySelector()
  364. {
  365. $html = $this->html5DocumentType . '<html><p hidden="hidden"></p></html>';
  366. $this->subject->setHtml($html);
  367. $this->subject->setCss('[hidden] { color:red; }');
  368. self::assertContains('<p hidden="hidden" style="color: red;">', $this->subject->emogrify());
  369. }
  370. /**
  371. * @test
  372. */
  373. public function emogrifyCanAssignStyleRulesFromTwoIdenticalMatchersToElement()
  374. {
  375. $html = $this->html5DocumentType . '<html><p></p></html>';
  376. $this->subject->setHtml($html);
  377. $styleRule1 = 'color: #000;';
  378. $styleRule2 = 'text-align: left;';
  379. $this->subject->setCss('p {' . $styleRule1 . '} p {' . $styleRule2 . '}');
  380. self::assertContains('<p style="' . $styleRule1 . ' ' . $styleRule2 . '">', $this->subject->emogrify());
  381. }
  382. /**
  383. * @test
  384. */
  385. public function emogrifyCanAssignStyleRulesFromTwoDifferentMatchersToElement()
  386. {
  387. $html = $this->html5DocumentType . '<html><p class="x"></p></html>';
  388. $this->subject->setHtml($html);
  389. $styleRule1 = 'color: #000;';
  390. $styleRule2 = 'text-align: left;';
  391. $this->subject->setCss('p {' . $styleRule1 . '} .x {' . $styleRule2 . '}');
  392. self::assertContains('<p class="x" style="' . $styleRule1 . ' ' . $styleRule2 . '">', $this->subject->emogrify());
  393. }
  394. /**
  395. * Data provide for selectors.
  396. *
  397. * @return string[][]
  398. */
  399. public function selectorDataProvider()
  400. {
  401. $styleRule = 'color: red;';
  402. $styleAttribute = 'style="' . $styleRule . '"';
  403. return array('universal selector HTML' => array('* {' . $styleRule . '} ', '#<html id="html" ' . $styleAttribute . '>#'), 'universal selector BODY' => array('* {' . $styleRule . '} ', '#<body ' . $styleAttribute . '>#'), 'universal selector P' => array('* {' . $styleRule . '} ', '#<p[^>]*' . $styleAttribute . '>#'), 'type selector matches first P' => array('p {' . $styleRule . '} ', '#<p class="p-1" ' . $styleAttribute . '>#'), 'type selector matches second P' => array('p {' . $styleRule . '} ', '#<p class="p-2" ' . $styleAttribute . '>#'), 'descendant selector P SPAN' => array('p span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'), 'descendant selector BODY SPAN' => array('body span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'), 'child selector P > SPAN matches direct child' => array('p > span {' . $styleRule . '} ', '#<span ' . $styleAttribute . '>#'), 'child selector BODY > SPAN not matches grandchild' => array('body > span {' . $styleRule . '} ', '#<span>#'), 'adjacent selector P + P not matches first P' => array('p + p {' . $styleRule . '} ', '#<p class="p-1">#'), 'adjacent selector P + P matches second P' => array('p + p {' . $styleRule . '} ', '#<p class="p-2" style="' . $styleRule . '">#'), 'adjacent selector P + P matches third P' => array('p + p {' . $styleRule . '} ', '#<p class="p-3" style="' . $styleRule . '">#'), 'ID selector #HTML' => array('#html {' . $styleRule . '} ', '#<html id="html" ' . $styleAttribute . '>#'), 'type and ID selector HTML#HTML' => array('html#html {' . $styleRule . '} ', '#<html id="html" ' . $styleAttribute . '>#'), 'class selector .P-1' => array('.p-1 {' . $styleRule . '} ', '#<p class="p-1" ' . $styleAttribute . '>#'), 'type and class selector P.P-1' => array('p.p-1 {' . $styleRule . '} ', '#<p class="p-1" ' . $styleAttribute . '>#'), 'attribute presence selector SPAN[title] matches element with matching attribute' => array('span[title] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'), 'attribute presence selector SPAN[title] not matches element without any attributes' => array('span[title] {' . $styleRule . '} ', '#<span>#'), 'attribute value selector [id="html"] matches element with matching attribute value' => array('[id="html"] {' . $styleRule . '} ', '#<html id="html" ' . $styleAttribute . '>#'), 'attribute value selector SPAN[title] matches element with matching attribute value' => array('span[title="bonjour"] {' . $styleRule . '} ', '#<span title="bonjour" ' . $styleAttribute . '>#'), 'attribute value selector SPAN[title] not matches element with other attribute value' => array('span[title="bonjour"] {' . $styleRule . '} ', '#<span title="buenas dias">#'), 'attribute value selector SPAN[title] not matches element without any attributes' => array('span[title="bonjour"] {' . $styleRule . '} ', '#<span>#'), 'BODY:first-child matches first child' => array('body:first-child {' . $styleRule . '} ', '#<p class="p-1" style="' . $styleRule . '">#'), 'BODY:first-child not matches middle child' => array('body:first-child {' . $styleRule . '} ', '#<p class="p-2">#'), 'BODY:first-child not matches last child' => array('body:first-child {' . $styleRule . '} ', '#<p class="p-3">#'), 'BODY:last-child not matches first child' => array('body:last-child {' . $styleRule . '} ', '#<p class="p-1">#'), 'BODY:last-child not matches middle child' => array('body:last-child {' . $styleRule . '} ', '#<p class="p-2">#'), 'BODY:last-child matches last child' => array('body:last-child {' . $styleRule . '} ', '#<p class="p-3" style="' . $styleRule . '">#'));
  404. }
  405. /**
  406. * @test
  407. *
  408. * @param string $css the complete CSS
  409. * @param string $htmlRegularExpression regular expression for the the HTML that needs to be contained in the HTML
  410. *
  411. * @dataProvider selectorDataProvider
  412. */
  413. public function emogrifierMatchesSelectors($css, $htmlRegularExpression)
  414. {
  415. $html = $this->html5DocumentType . '<html id="html">' . ' <body>' . ' <p class="p-1"><span>some text</span></p>' . ' <p class="p-2"><span title="bonjour">some</span> text</p>' . ' <p class="p-3"><span title="buenas dias">some</span> more text</p>' . ' </body>' . '</html>';
  416. $this->subject->setHtml($html);
  417. $this->subject->setCss($css);
  418. $result = $this->subject->emogrify();
  419. self::assertRegExp($htmlRegularExpression, $result);
  420. }
  421. /**
  422. * Data provider for emogrifyDropsWhitespaceFromCssDeclarations.
  423. *
  424. * @return string[][]
  425. */
  426. public function cssDeclarationWhitespaceDroppingDataProvider()
  427. {
  428. 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('
  429. color:#222;', 'color: #222;'), 'newline after property semicolon' => array('color:#222;
  430. ', 'color: #222;'), 'newline before colon, trailing semicolon' => array('color
  431. :#333;', 'color: #333;'), 'newline after colon, trailing semicolon' => array('color:
  432. #333;', 'color: #333;'), 'newline after semicolon' => array('color:#333
  433. ;', 'color: #333;'));
  434. }
  435. /**
  436. * @test
  437. *
  438. * @param string $cssDeclaration the CSS declaration block (without the curly braces)
  439. * @param string $expectedStyleAttributeContent the expected value of the style attribute
  440. *
  441. * @dataProvider cssDeclarationWhitespaceDroppingDataProvider
  442. */
  443. public function emogrifyDropsLeadingAndTrailingWhitespaceFromCssDeclarations($cssDeclaration, $expectedStyleAttributeContent)
  444. {
  445. $html = $this->html5DocumentType . '<html></html>';
  446. $css = 'html {' . $cssDeclaration . '}';
  447. $this->subject->setHtml($html);
  448. $this->subject->setCss($css);
  449. $result = $this->subject->emogrify();
  450. self::assertContains('html style="' . $expectedStyleAttributeContent . '">', $result);
  451. }
  452. /**
  453. * Data provider for emogrifyFormatsCssDeclarations.
  454. *
  455. * @return string[][]
  456. */
  457. public function formattedCssDeclarationDataProvider()
  458. {
  459. 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;
  460. 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;'));
  461. }
  462. /**
  463. * @test
  464. *
  465. * @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
  466. * @param string $expectedStyleAttributeContent the expected value of the style attribute
  467. *
  468. * @dataProvider formattedCssDeclarationDataProvider
  469. */
  470. public function emogrifyFormatsCssDeclarations($cssDeclarationBlock, $expectedStyleAttributeContent)
  471. {
  472. $html = $this->html5DocumentType . '<html></html>';
  473. $css = 'html {' . $cssDeclarationBlock . '}';
  474. $this->subject->setHtml($html);
  475. $this->subject->setCss($css);
  476. self::assertContains('html style="' . $expectedStyleAttributeContent . '">', $this->subject->emogrify());
  477. }
  478. /**
  479. * Data provider for emogrifyInvalidDeclaration.
  480. *
  481. * @return string[][]
  482. */
  483. public function invalidDeclarationDataProvider()
  484. {
  485. 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 :'));
  486. }
  487. /**
  488. * @test
  489. *
  490. * @param string $cssDeclarationBlock the CSS declaration block (without the curly braces)
  491. *
  492. * @dataProvider invalidDeclarationDataProvider
  493. */
  494. public function emogrifyDropsInvalidDeclaration($cssDeclarationBlock)
  495. {
  496. $html = $this->html5DocumentType . '<html></html>';
  497. $css = 'html {' . $cssDeclarationBlock . '}';
  498. $this->subject->setHtml($html);
  499. $this->subject->setCss($css);
  500. self::assertContains('<html style="">', $this->subject->emogrify());
  501. }
  502. /**
  503. * @test
  504. */
  505. public function emogrifyKeepsExistingStyleAttributes()
  506. {
  507. $styleAttribute = 'style="color: #ccc;"';
  508. $html = $this->html5DocumentType . '<html ' . $styleAttribute . '></html>';
  509. $this->subject->setHtml($html);
  510. self::assertContains($styleAttribute, $this->subject->emogrify());
  511. }
  512. /**
  513. * @test
  514. */
  515. public function emogrifyAddsCssAfterExistingStyle()
  516. {
  517. $styleAttributeValue = 'color: #ccc;';
  518. $html = $this->html5DocumentType . '<html style="' . $styleAttributeValue . '"></html>';
  519. $this->subject->setHtml($html);
  520. $cssDeclarations = 'margin: 0 2px;';
  521. $css = 'html {' . $cssDeclarations . '}';
  522. $this->subject->setCss($css);
  523. self::assertContains('style="' . $styleAttributeValue . ' ' . $cssDeclarations . '"', $this->subject->emogrify());
  524. }
  525. /**
  526. * @test
  527. */
  528. public function emogrifyCanMatchMinifiedCss()
  529. {
  530. $html = $this->html5DocumentType . '<html><p></p></html>';
  531. $this->subject->setHtml($html);
  532. $this->subject->setCss('p{color:blue;}html{color:red;}');
  533. self::assertContains('<html style="color: red;">', $this->subject->emogrify());
  534. }
  535. /**
  536. * @test
  537. */
  538. public function emogrifyLowercasesAttributeNamesFromStyleAttributes()
  539. {
  540. $html = $this->html5DocumentType . '<html style="COLOR:#ccc;"></html>';
  541. $this->subject->setHtml($html);
  542. self::assertContains('style="color: #ccc;"', $this->subject->emogrify());
  543. }
  544. /**
  545. * @test
  546. */
  547. public function emogrifyLowerCasesAttributeNames()
  548. {
  549. $html = $this->html5DocumentType . '<html></html>';
  550. $this->subject->setHtml($html);
  551. $cssIn = 'html {mArGiN:0 2pX;}';
  552. $cssOut = 'margin: 0 2pX;';
  553. $this->subject->setCss($cssIn);
  554. self::assertContains('style="' . $cssOut . '"', $this->subject->emogrify());
  555. }
  556. /**
  557. * @test
  558. */
  559. public function emogrifyPreservesCaseForAttributeValuesFromPassedInCss()
  560. {
  561. $css = 'content: \'Hello World\';';
  562. $html = $this->html5DocumentType . '<html><body><p>target</p></body></html>';
  563. $this->subject->setHtml($html);
  564. $this->subject->setCss('p {' . $css . '}');
  565. self::assertContains('<p style="' . $css . '">target</p>', $this->subject->emogrify());
  566. }
  567. /**
  568. * @test
  569. */
  570. public function emogrifyPreservesCaseForAttributeValuesFromParsedStyleBlock()
  571. {
  572. $css = 'content: \'Hello World\';';
  573. $html = $this->html5DocumentType . '<html><head><style>p {' . $css . '}</style></head><body><p>target</p></body></html>';
  574. $this->subject->setHtml($html);
  575. self::assertContains('<p style="' . $css . '">target</p>', $this->subject->emogrify());
  576. }
  577. /**
  578. * @test
  579. */
  580. public function emogrifyRemovesStyleNodes()
  581. {
  582. $html = $this->html5DocumentType . '<html><style type="text/css"></style></html>';
  583. $this->subject->setHtml($html);
  584. self::assertNotContains('<style>', $this->subject->emogrify());
  585. }
  586. /**
  587. * @test
  588. */
  589. public function emogrifyIgnoresInvalidCssSelector()
  590. {
  591. $html = $this->html5DocumentType . '<html><style type="text/css">p{color:red;} <style data-x="1">html{cursor:text;}</style></html>';
  592. $this->subject->setHtml($html);
  593. $hasError = false;
  594. set_error_handler(function ($errorNumber, $errorMessage) use(&$hasError) {
  595. if ($errorMessage === 'DOMXPath::query(): Invalid expression') {
  596. return true;
  597. }
  598. $hasError = true;
  599. return true;
  600. });
  601. $this->subject->emogrify();
  602. restore_error_handler();
  603. self::assertFalse($hasError);
  604. }
  605. /**
  606. * Data provider for things that should be left out when applying the CSS.
  607. *
  608. * @return array[]
  609. */
  610. public function unneededCssThingsDataProvider()
  611. {
  612. 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'));
  613. }
  614. /**
  615. * @test
  616. *
  617. * @param string $css
  618. * @param string $markerNotExpectedInHtml
  619. *
  620. * @dataProvider unneededCssThingsDataProvider
  621. */
  622. public function emogrifyFiltersUnneededCssThings($css, $markerNotExpectedInHtml)
  623. {
  624. $html = $this->html5DocumentType . '<html><p>foo</p></html>';
  625. $this->subject->setHtml($html);
  626. $this->subject->setCss($css);
  627. self::assertNotContains($markerNotExpectedInHtml, $this->subject->emogrify());
  628. }
  629. /**
  630. * Data provider for media rules.
  631. *
  632. * @return array[]
  633. */
  634. public function mediaRulesDataProvider()
  635. {
  636. 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;}}'));
  637. }
  638. /**
  639. * @test
  640. *
  641. * @param string $css
  642. *
  643. * @dataProvider mediaRulesDataProvider
  644. */
  645. public function emogrifyKeepsMediaRules($css)
  646. {
  647. $html = $this->html5DocumentType . '<html><p>foo</p></html>';
  648. $this->subject->setHtml($html);
  649. $this->subject->setCss($css);
  650. self::assertContains($css, $this->subject->emogrify());
  651. }
  652. /**
  653. * @test
  654. */
  655. public function removeAllowedMediaTypeRemovesStylesForTheGivenMediaType()
  656. {
  657. $css = '@media screen { html {} }';
  658. $html = $this->html5DocumentType . '<html></html>';
  659. $this->subject->setHtml($html);
  660. $this->subject->setCss($css);
  661. $this->subject->removeAllowedMediaType('screen');
  662. self::assertNotContains($css, $this->subject->emogrify());
  663. }
  664. /**
  665. * @test
  666. */
  667. public function addAllowedMediaTypeKeepsStylesForTheGivenMediaType()
  668. {
  669. $css = '@media braille { html { some-property: value; } }';
  670. $html = $this->html5DocumentType . '<html></html>';
  671. $this->subject->setHtml($html);
  672. $this->subject->setCss($css);
  673. $this->subject->addAllowedMediaType('braille');
  674. self::assertContains($css, $this->subject->emogrify());
  675. }
  676. /**
  677. * @test
  678. */
  679. public function emogrifyAddsMissingHeadElement()
  680. {
  681. $html = $this->html5DocumentType . '<html></html>';
  682. $this->subject->setHtml($html);
  683. $this->subject->setCss('@media all { html {} }');
  684. self::assertContains('<head>', $this->subject->emogrify());
  685. }
  686. /**
  687. * @test
  688. */
  689. public function emogrifyKeepExistingHeadElementContent()
  690. {
  691. $html = $this->html5DocumentType . '<html><head><!-- original content --></head></html>';
  692. $this->subject->setHtml($html);
  693. $this->subject->setCss('@media all { html {} }');
  694. self::assertContains('<!-- original content -->', $this->subject->emogrify());
  695. }
  696. /**
  697. * @test
  698. */
  699. public function emogrifyKeepExistingHeadElementAddStyleElement()
  700. {
  701. $html = $this->html5DocumentType . '<html><head><!-- original content --></head></html>';
  702. $this->subject->setHtml($html);
  703. $this->subject->setCss('@media all { html {} }');
  704. self::assertContains('<style type="text/css">', $this->subject->emogrify());
  705. }
  706. /**
  707. * Valid media query which need to be preserved
  708. *
  709. * @return array[]
  710. */
  711. public function validMediaPreserveDataProvider()
  712. {
  713. return array('style in "only screen and size" media type rule' => array('@media only screen and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }'), 'style in "screen size" media type rule' => array('@media screen and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }'), 'style in "only screen and screen size" media type rule' => array('@media only screen and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }'), 'style in "all and screen size" media type rule' => array('@media all and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }'), 'style in "only all and" media type rule' => array('@media only all and (min-device-width: 320px) and (max-device-width: 480px) { h1 { color:red; } }'), 'style in "all" media type rule' => array('@media all {p {color: #000;}}'), 'style in "only screen" media type rule' => array('@media only screen { h1 { color:red; } }'), 'style in "only all" media type rule' => array('@media only all { h1 { color:red; } }'), 'style in "screen" media type rule' => array('@media screen { h1 { color:red; } }'), 'style in media type rule without specification' => array('@media { h1 { color:red; } }'));
  714. }
  715. /**
  716. * @test
  717. *
  718. * @param string $css
  719. *
  720. * @dataProvider validMediaPreserveDataProvider
  721. */
  722. public function emogrifyWithValidMediaQueryContainsInnerCss($css)
  723. {
  724. $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1><p></p></html>';
  725. $this->subject->setHtml($html);
  726. $this->subject->setCss($css);
  727. self::assertContains($css, $this->subject->emogrify());
  728. }
  729. /**
  730. * @test
  731. *
  732. * @param string $css
  733. *
  734. * @dataProvider validMediaPreserveDataProvider
  735. */
  736. public function emogrifyForHtmlWithValidMediaQueryContainsInnerCss($css)
  737. {
  738. $html = $this->html5DocumentType . PHP_EOL . '<html><style type="text/css">' . $css . '</style><h1></h1><p></p></html>';
  739. $this->subject->setHtml($html);
  740. self::assertContains($css, $this->subject->emogrify());
  741. }
  742. /**
  743. * @test
  744. *
  745. * @param string $css
  746. *
  747. * @dataProvider validMediaPreserveDataProvider
  748. */
  749. public function emogrifyWithValidMediaQueryNotContainsInlineCss($css)
  750. {
  751. $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
  752. $this->subject->setHtml($html);
  753. $this->subject->setCss($css);
  754. self::assertNotContains('style="color:red"', $this->subject->emogrify());
  755. }
  756. /**
  757. * Invalid media query which need to be strip
  758. *
  759. * @return array[]
  760. */
  761. public function invalidMediaPreserveDataProvider()
  762. {
  763. 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; } }'));
  764. }
  765. /**
  766. * @test
  767. *
  768. * @param string $css
  769. *
  770. * @dataProvider invalidMediaPreserveDataProvider
  771. */
  772. public function emogrifyWithInvalidMediaQueryaNotContainsInnerCss($css)
  773. {
  774. $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
  775. $this->subject->setHtml($html);
  776. $this->subject->setCss($css);
  777. self::assertNotContains($css, $this->subject->emogrify());
  778. }
  779. /**
  780. * @test
  781. *
  782. * @param string $css
  783. *
  784. * @dataProvider invalidMediaPreserveDataProvider
  785. */
  786. public function emogrifyWithInValidMediaQueryNotContainsInlineCss($css)
  787. {
  788. $html = $this->html5DocumentType . PHP_EOL . '<html><h1></h1></html>';
  789. $this->subject->setHtml($html);
  790. $this->subject->setCss($css);
  791. self::assertNotContains('style="color: red"', $this->subject->emogrify());
  792. }
  793. /**
  794. * @test
  795. *
  796. * @param string $css
  797. *
  798. * @dataProvider invalidMediaPreserveDataProvider
  799. */
  800. public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInnerCss($css)
  801. {
  802. $html = $this->html5DocumentType . PHP_EOL . '<html><style type="text/css">' . $css . '</style><h1></h1></html>';
  803. $this->subject->setHtml($html);
  804. self::assertNotContains($css, $this->subject->emogrify());
  805. }
  806. /**
  807. * @test
  808. *
  809. * @param string $css
  810. *
  811. * @dataProvider invalidMediaPreserveDataProvider
  812. */
  813. public function emogrifyFromHtmlWithInValidMediaQueryNotContainsInlineCss($css)
  814. {
  815. $html = $this->html5DocumentType . PHP_EOL . '<html><style type="text/css">' . $css . '</style><h1></h1></html>';
  816. $this->subject->setHtml($html);
  817. self::assertNotContains('style="color: red"', $this->subject->emogrify());
  818. }
  819. /**
  820. * @test
  821. */
  822. public function emogrifyAppliesCssFromStyleNodes()
  823. {
  824. $styleAttributeValue = 'color: #ccc;';
  825. $html = $this->html5DocumentType . '<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>';
  826. $this->subject->setHtml($html);
  827. self::assertContains('<html style="' . $styleAttributeValue . '">', $this->subject->emogrify());
  828. }
  829. /**
  830. * @test
  831. */
  832. public function emogrifyWhenDisabledNotAppliesCssFromStyleBlocks()
  833. {
  834. $styleAttributeValue = 'color: #ccc;';
  835. $html = $this->html5DocumentType . '<html><style type="text/css">html {' . $styleAttributeValue . '}</style></html>';
  836. $this->subject->setHtml($html);
  837. $this->subject->disableStyleBlocksParsing();
  838. self::assertNotContains('<html style="' . $styleAttributeValue . '">', $this->subject->emogrify());
  839. }
  840. /**
  841. * @test
  842. */
  843. public function emogrifyWhenStyleBlocksParsingDisabledKeepInlineStyles()
  844. {
  845. $styleAttributeValue = 'text-align: center;';
  846. $html = $this->html5DocumentType . '<html><head><style type="text/css">p { color: #ccc; }</style></head>' . '<body><p style="' . $styleAttributeValue . '">paragraph</p></body></html>';
  847. $expected = '<p style="' . $styleAttributeValue . '">';
  848. $this->subject->setHtml($html);
  849. $this->subject->disableStyleBlocksParsing();
  850. self::assertContains($expected, $this->subject->emogrify());
  851. }
  852. /**
  853. * @test
  854. */
  855. public function emogrifyWhenDisabledNotAppliesCssFromInlineStyles()
  856. {
  857. $styleAttributeValue = 'color: #ccc;';
  858. $html = $this->html5DocumentType . '<html style="' . $styleAttributeValue . '"></html>';
  859. $this->subject->setHtml($html);
  860. $this->subject->disableInlineStyleAttributesParsing();
  861. self::assertNotContains('<html style', $this->subject->emogrify());
  862. }
  863. /**
  864. * @test
  865. */
  866. public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles()
  867. {
  868. $styleAttributeValue = 'color: #ccc;';
  869. $html = $this->html5DocumentType . '<html><head><style type="text/css">p { ' . $styleAttributeValue . ' }</style></head>' . '<body><p style="text-align: center;">paragraph</p></body></html>';
  870. $expected = '<p style="' . $styleAttributeValue . '">';
  871. $this->subject->setHtml($html);
  872. $this->subject->disableInlineStyleAttributesParsing();
  873. self::assertContains($expected, $this->subject->emogrify());
  874. }
  875. /**
  876. * @test
  877. */
  878. public function emogrifyAppliesCssWithUpperCaseSelector()
  879. {
  880. $html = $this->html5DocumentType . '<html><style type="text/css">P { color:#ccc; }</style><body><p>paragraph</p></body></html>';
  881. $expected = '<p style="color: #ccc;">';
  882. $this->subject->setHtml($html);
  883. self::assertContains($expected, $this->subject->emogrify());
  884. }
  885. /**
  886. * Emogrify was handling case differently for passed in CSS vs CSS parsed from style blocks.
  887. * @test
  888. */
  889. public function emogrifyAppliesCssWithMixedCaseAttributesInStyleBlock()
  890. {
  891. $html = $this->html5DocumentType . '<html><head><style>#topWrap p {padding-bottom: 1px;PADDING-TOP: 0;}</style></head>' . '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>';
  892. $expected = '<p style="text-align: center; padding-bottom: 1px; padding-top: 0;">';
  893. $this->subject->setHtml($html);
  894. self::assertContains($expected, $this->subject->emogrify());
  895. }
  896. /**
  897. * Passed in CSS sets the order, but style block CSS overrides values.
  898. * @test
  899. */
  900. public function emogrifyMergesCssWithMixedCaseAttribute()
  901. {
  902. $css = 'p { margin: 0; padding-TOP: 0; PADDING-bottom: 1PX;}';
  903. $html = $this->html5DocumentType . '<html><head><style>#topWrap p {padding-bottom: 3px;PADDING-TOP: 1px;}</style></head>' . '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>';
  904. $expected = '<p style="text-align: center; margin: 0; padding-top: 1px; padding-bottom: 3px;">';
  905. $this->subject->setHtml($html);
  906. $this->subject->setCss($css);
  907. self::assertContains($expected, $this->subject->emogrify());
  908. }
  909. /**
  910. * @test
  911. */
  912. public function emogrifyMergesCssWithMixedUnits()
  913. {
  914. $css = 'p { margin: 1px; padding-bottom:0;}';
  915. $html = $this->html5DocumentType . '<html><head><style>#topWrap p {margin:0;padding-bottom: 1px;}</style></head>' . '<body><div id="topWrap"><p style="text-align: center;">some content</p></div></body></html>';
  916. $expected = '<p style="text-align: center; margin: 0; padding-bottom: 1px;">';
  917. $this->subject->setHtml($html);
  918. $this->subject->setCss($css);
  919. self::assertContains($expected, $this->subject->emogrify());
  920. }
  921. /**
  922. * @test
  923. */
  924. public function emogrifyByDefaultRemovesElementsWithDisplayNoneFromExternalCss()
  925. {
  926. $css = 'div.foo { display: none; }';
  927. $html = $this->html5DocumentType . '<html><body><div class="bar"></div><div class="foo"></div></body></html>';
  928. $expected = '<div class="bar"></div>';
  929. $this->subject->setHtml($html);
  930. $this->subject->setCss($css);
  931. self::assertContains($expected, $this->subject->emogrify());
  932. }
  933. /**
  934. * @test
  935. */
  936. public function emogrifyByDefaultRemovesElementsWithDisplayNoneInStyleAttribute()
  937. {
  938. $html = $this->html5DocumentType . '<html><body><div class="bar"></div><div class="foobar" style="display: none;"></div>' . '</body></html>';
  939. $expected = '<div class="bar"></div>';
  940. $this->subject->setHtml($html);
  941. self::assertContains($expected, $this->subject->emogrify());
  942. }
  943. /**
  944. * @test
  945. */
  946. public function emogrifyAfterDisableInvisibleNodeRemovalPreservesInvisibleElements()
  947. {
  948. $css = 'div.foo { display: none; }';
  949. $html = $this->html5DocumentType . '<html><body><div class="bar"></div><div class="foo"></div></body></html>';
  950. $expected = '<div class="foo" style="display: none;">';
  951. $this->subject->setHtml($html);
  952. $this->subject->setCss($css);
  953. $this->subject->disableInvisibleNodeRemoval();
  954. self::assertContains($expected, $this->subject->emogrify());
  955. }
  956. /**
  957. * @test
  958. */
  959. public function emogrifyKeepsCssMediaQueriesWithCssCommentAfterMediaQuery()
  960. {
  961. $css = '@media only screen and (max-width: 480px) { body { color: #ffffff } /* some comment */ }';
  962. $html = $this->html5DocumentType . '<html><body></body></html>';
  963. $expected = '@media only screen and (max-width: 480px)';
  964. $this->subject->setHtml($html);
  965. $this->subject->setCss($css);
  966. self::assertContains($expected, $this->subject->emogrify());
  967. }
  968. /**
  969. * @test
  970. */
  971. public function emogrifyForXhtmlDocumentTypeConvertsXmlSelfClosingTagsToNonXmlSelfClosingTag()
  972. {
  973. $this->subject->setHtml($this->xhtml1StrictDocumentType . '<html><body><br/></body></html>');
  974. self::assertContains('<body><br></body>', $this->subject->emogrify());
  975. }
  976. /**
  977. * @test
  978. */
  979. public function emogrifyForHtml5DocumentTypeKeepsNonXmlSelfClosingTagsAsNonXmlSelfClosing()
  980. {
  981. $this->subject->setHtml($this->html5DocumentType . '<html><body><br></body></html>');
  982. self::assertContains('<body><br></body>', $this->subject->emogrify());
  983. }
  984. /**
  985. * @test
  986. */
  987. public function emogrifyForHtml5DocumentTypeConvertXmlSelfClosingTagsToNonXmlSelfClosingTag()
  988. {
  989. $this->subject->setHtml($this->html5DocumentType . '<html><body><br/></body></html>');
  990. self::assertContains('<body><br></body>', $this->subject->emogrify());
  991. }
  992. /**
  993. * @test
  994. */
  995. public function emogrifyAutomaticallyClosesUnclosedTag()
  996. {
  997. $this->subject->setHtml($this->html5DocumentType . '<html><body><p></body></html>');
  998. self::assertContains('<body><p></p></body>', $this->subject->emogrify());
  999. }
  1000. /**
  1001. * @test
  1002. */
  1003. public function emogrifyReturnsCompleteHtmlDocument()
  1004. {
  1005. $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
  1006. self::assertSame($this->html5DocumentType . self::LF . '<html>' . self::LF . '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head>' . self::LF . '<body><p></p></body>' . self::LF . '</html>' . self::LF, $this->subject->emogrify());
  1007. }
  1008. /**
  1009. * @test
  1010. */
  1011. public function emogrifyBodyContentReturnsBodyContentFromHtml()
  1012. {
  1013. $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
  1014. self::assertSame('<p></p>' . self::LF, $this->subject->emogrifyBodyContent());
  1015. }
  1016. /**
  1017. * @test
  1018. */
  1019. public function emogrifyBodyContentReturnsBodyContentFromContent()
  1020. {
  1021. $this->subject->setHtml('<p></p>');
  1022. self::assertSame('<p></p>' . self::LF, $this->subject->emogrifyBodyContent());
  1023. }
  1024. /**
  1025. * @test
  1026. */
  1027. public function importantInExternalCssOverwritesInlineCss()
  1028. {
  1029. $css = 'p { margin: 1px !important; }';
  1030. $html = $this->html5DocumentType . '<html><head</head><body><p style="margin: 2px;">some content</p></body></html>';
  1031. $expected = '<p style="margin: 1px !important;">';
  1032. $this->subject->setHtml($html);
  1033. $this->subject->setCss($css);
  1034. self::assertContains($expected, $this->subject->emogrify());
  1035. }
  1036. /**
  1037. * @test
  1038. */
  1039. public function importantInExternalCssKeepsInlineCssForOtherAttributes()
  1040. {
  1041. $css = 'p { margin: 1px !important; }';
  1042. $html = $this->html5DocumentType . '<html><head</head><body><p style="margin: 2px; text-align: center;">some content</p></body></html>';
  1043. $expected = '<p style="margin: 1px !important; text-align: center;">';
  1044. $this->subject->setHtml($html);
  1045. $this->subject->setCss($css);
  1046. self::assertContains($expected, $this->subject->emogrify());
  1047. }
  1048. /**
  1049. * @test
  1050. */
  1051. public function emogrifyHandlesImportantStyleTagCaseInsensitive()
  1052. {
  1053. $css = 'p { margin: 1px !ImPorTant; }';
  1054. $html = $this->html5DocumentType . '<html><head</head><body><p style="margin: 2px;">some content</p></body></html>';
  1055. $expected = '<p style="margin: 1px !ImPorTant;">';
  1056. $this->subject->setHtml($html);
  1057. $this->subject->setCss($css);
  1058. self::assertContains($expected, $this->subject->emogrify());
  1059. }
  1060. /**
  1061. * @test
  1062. */
  1063. public function irrelevantMediaQueriesAreRemoved()
  1064. {
  1065. $uselessQuery = '@media all and (max-width: 500px) { em { color:red; } }';
  1066. $this->subject->setCss($uselessQuery);
  1067. $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
  1068. $result = $this->subject->emogrify();
  1069. self::assertNotContains($uselessQuery, $result);
  1070. }
  1071. /**
  1072. * @test
  1073. */
  1074. public function relevantMediaQueriesAreRetained()
  1075. {
  1076. $usefulQuery = '@media all and (max-width: 500px) { p { color:red; } }';
  1077. $this->subject->setCss($usefulQuery);
  1078. $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
  1079. $result = $this->subject->emogrify();
  1080. self::assertContains($usefulQuery, $result);
  1081. }
  1082. /**
  1083. * @test
  1084. */
  1085. public function importantStyleRuleFromInlineCssOverwritesImportantStyleRuleFromExternalCss()
  1086. {
  1087. $css = 'p { margin: 1px !important; padding: 1px;}';
  1088. $html = $this->html5DocumentType . '<html><head</head><body><p style="margin: 2px !important; text-align: center;">some content</p>' . '</body></html>';
  1089. $expected = '<p style="margin: 2px !important; text-align: center; padding: 1px;">';
  1090. $this->subject->setHtml($html);
  1091. $this->subject->setCss($css);
  1092. self::assertContains($expected, $this->subject->emogrify());
  1093. }
  1094. /**
  1095. * @test
  1096. */
  1097. public function addExcludedSelectorRemovesMatchingElementsFromEmogrification()
  1098. {
  1099. $css = 'p { margin: 0; }';
  1100. $this->subject->setHtml($this->html5DocumentType . '<html><body><p class="x"></p></body></html>');
  1101. $this->subject->setCss($css);
  1102. $this->subject->addExcludedSelector('p.x');
  1103. $html = $this->subject->emogrify();
  1104. self::assertContains('<p class="x"></p>', $html);
  1105. }
  1106. /**
  1107. * @test
  1108. */
  1109. public function addExcludedSelectorExcludesMatchingElementEventWithWhitespaceAroundSelector()
  1110. {
  1111. $css = 'p { margin: 0; }';
  1112. $this->subject->setHtml($this->html5DocumentType . '<html><body><p class="x"></p></body></html>');
  1113. $this->subject->setCss($css);
  1114. $this->subject->addExcludedSelector(' p.x ');
  1115. $html = $this->subject->emogrify();
  1116. self::assertContains('<p class="x"></p>', $html);
  1117. }
  1118. /**
  1119. * @test
  1120. */
  1121. public function addExcludedSelectorKeepsNonMatchingElementsInEmogrification()
  1122. {
  1123. $css = 'p { margin: 0; }';
  1124. $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
  1125. $this->subject->setCss($css);
  1126. $this->subject->addExcludedSelector('p.x');
  1127. $html = $this->subject->emogrify();
  1128. self::assertContains('<p style="margin: 0;"></p>', $html);
  1129. }
  1130. /**
  1131. * @test
  1132. */
  1133. public function removeExcludedSelectorGetsMatchingElementsToBeEmogrifiedAgain()
  1134. {
  1135. $css = 'p { margin: 0; }';
  1136. $this->subject->setHtml($this->html5DocumentType . '<html><body><p class="x"></p></body></html>');
  1137. $this->subject->setCss($css);
  1138. $this->subject->addExcludedSelector('p.x');
  1139. $this->subject->removeExcludedSelector('p.x');
  1140. $html = $this->subject->emogrify();
  1141. self::assertContains('<p class="x" style="margin: 0;"></p>', $html);
  1142. }
  1143. /**
  1144. * @test
  1145. */
  1146. public function emptyMediaQueriesAreRemoved()
  1147. {
  1148. $emptyQuery = '@media all and (max-width: 500px) { }';
  1149. $this->subject->setCss($emptyQuery);
  1150. $this->subject->setHtml($this->html5DocumentType . '<html><body><p></p></body></html>');
  1151. $result = $this->subject->emogrify();
  1152. self::assertNotContains($emptyQuery, $result);
  1153. }
  1154. /**
  1155. * @test
  1156. */
  1157. public function multiLineMediaQueryWithWindowsLineEndingsIsAppliedOnlyOnce()
  1158. {
  1159. $css = '@media all {
  1160. ' . '.medium {font-size:18px;}
  1161. ' . '.small {font-size:14px;}
  1162. ' . '}';
  1163. $this->subject->setCss($css);
  1164. $this->subject->setHtml($this->html5DocumentType . '<html><body>' . '<p class="medium">medium</p>' . '<p class="small">small</p>' . '</body></html>');
  1165. $result = $this->subject->emogrify();
  1166. self::assertSame(1, substr_count($result, '<style type="text/css">' . $css . '</style>'));
  1167. }
  1168. /**
  1169. * @test
  1170. */
  1171. public function multiLineMediaQueryWithUnixLineEndingsIsAppliedOnlyOnce()
  1172. {
  1173. $css = '@media all {
  1174. ' . '.medium {font-size:18px;}
  1175. ' . '.small {font-size:14px;}
  1176. ' . '}';
  1177. $this->subject->setCss($css);
  1178. $this->subject->setHtml($this->html5DocumentType . '<html><body>' . '<p class="medium">medium</p>' . '<p class="small">small</p>' . '</body></html>');
  1179. $result = $this->subject->emogrify();
  1180. self::assertSame(1, substr_count($result, '<style type="text/css">' . $css . '</style>'));
  1181. }
  1182. /**
  1183. * @test
  1184. */
  1185. public function multipleMediaQueriesAreAppliedOnlyOnce()
  1186. {
  1187. $css = '@media all {
  1188. ' . '.medium {font-size:18px;
  1189. ' . '.small {font-size:14px;}
  1190. ' . '}' . '@media screen {
  1191. ' . '.medium {font-size:24px;}
  1192. ' . '.small {font-size:18px;}
  1193. ' . '}';
  1194. $this->subject->setCss($css);
  1195. $this->subject->setHtml($this->html5DocumentType . '<html><body>' . '<p class="medium">medium</p>' . '<p class="small">small</p>' . '</body></html>');
  1196. $result = $this->subject->emogrify();
  1197. self::assertSame(1, substr_count($result, '<style type="text/css">' . $css . '</style>'));
  1198. }
  1199. /**
  1200. * @return string[][]
  1201. */
  1202. public function dataUriMediaTypeDataProvider()
  1203. {
  1204. return array('nothing' => array(''), ';charset=utf-8' => array(';charset=utf-8'), ';base64' => array(';base64'), ';charset=utf-8;base64' => array(';charset=utf-8;base64'));
  1205. }
  1206. /**
  1207. * @test
  1208. * @param string $dataUriMediaType
  1209. * @dataProvider dataUriMediaTypeDataProvider
  1210. */
  1211. public function dataUrisAreConserved($dataUriMediaType)
  1212. {
  1213. $html = $this->html5DocumentType . '<html></html>';
  1214. $this->subject->setHtml($html);
  1215. $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=);';
  1216. $this->subject->setCss('html {' . $styleRule . '}');
  1217. $result = $this->subject->emogrify();
  1218. self::assertContains('<html style="' . $styleRule . '">', $result);
  1219. }
  1220. }