2 namespace Masterminds\HTML5\Tests\Serializer;
4 use Masterminds\HTML5\Serializer\OutputRules;
5 use Masterminds\HTML5\Serializer\Traverser;
8 class OutputRulesTest extends \Masterminds\HTML5\Tests\TestCase
11 protected $markup = '<!doctype html>
14 <meta charset="utf-8">
18 <p>This is a test.</p>
27 public function setUp()
29 $this->html5 = $this->getInstance();
33 * Using reflection we make a protected method accessible for testing.
36 * The name of the method on the Traverser class to test.
38 * @return \ReflectionMethod for the specified method
40 public function getProtectedMethod($name)
42 $class = new \ReflectionClass('\Masterminds\HTML5\Serializer\OutputRules');
43 $method = $class->getMethod($name);
44 $method->setAccessible(true);
49 public function getTraverserProtectedProperty($name)
51 $class = new \ReflectionClass('\Masterminds\HTML5\Serializer\Traverser');
52 $property = $class->getProperty($name);
53 $property->setAccessible(true);
58 public function getOutputRules($options = array())
60 $options = $options + $this->html5->getOptions();
61 $stream = fopen('php://temp', 'w');
62 $dom = $this->html5->loadHTML($this->markup);
63 $r = new OutputRules($stream, $options);
64 $t = new Traverser($dom, $stream, $r, $options);
72 public function testDocument()
74 $dom = $this->html5->loadHTML('<!doctype html><html lang="en"><body>foo</body></html>');
76 $stream = fopen('php://temp', 'w');
77 $r = new OutputRules($stream, $this->html5->getOptions());
78 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
81 $expected = '<!DOCTYPE html>' . PHP_EOL . '<html lang="en"><body>foo</body></html>' . PHP_EOL;
82 $this->assertEquals($expected, stream_get_contents($stream, - 1, 0));
85 public function testEmptyDocument()
87 $dom = $this->html5->loadHTML('');
89 $stream = fopen('php://temp', 'w');
90 $r = new OutputRules($stream, $this->html5->getOptions());
91 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
94 $expected = '<!DOCTYPE html>' . PHP_EOL;
95 $this->assertEquals($expected, stream_get_contents($stream, - 1, 0));
98 public function testDoctype()
100 $dom = $this->html5->loadHTML('<!doctype html><html lang="en"><body>foo</body></html>');
102 $stream = fopen('php://temp', 'w');
103 $r = new OutputRules($stream, $this->html5->getOptions());
104 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
106 $m = $this->getProtectedMethod('doctype');
107 $m->invoke($r, 'foo');
108 $this->assertEquals("<!DOCTYPE html>" . PHP_EOL, stream_get_contents($stream, - 1, 0));
111 public function testElement()
113 $dom = $this->html5->loadHTML(
117 <div id="foo" class="bar baz">foo bar baz</div>
118 <svg width="150" height="100" viewBox="0 0 3 2">
119 <rect width="1" height="2" x="0" fill="#008d46" />
120 <rect width="1" height="2" x="1" fill="#ffffff" />
121 <rect width="1" height="2" x="2" fill="#d2232c" />
126 $stream = fopen('php://temp', 'w');
127 $r = new OutputRules($stream, $this->html5->getOptions());
128 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
130 $list = $dom->getElementsByTagName('div');
131 $r->element($list->item(0));
132 $this->assertEquals('<div id="foo" class="bar baz">foo bar baz</div>', stream_get_contents($stream, - 1, 0));
135 function testSerializeWithNamespaces()
137 $this->html5 = $this->getInstance(array(
138 'xmlNamespaces' => true
143 <html><body id="body" xmlns:x="http://www.prefixed.com">
144 <a id="bar1" xmlns="http://www.prefixed.com/bar1">
145 <b id="bar4" xmlns="http://www.prefixed.com/bar4"><x:prefixed id="prefixed">xy</x:prefixed></b>
147 <svg id="svg">svg</svg>
148 <c id="bar2" xmlns="http://www.prefixed.com/bar2"></c>
151 <xn:d id="bar5" xmlns:xn="http://www.prefixed.com/xn" xmlns="http://www.prefixed.com/bar5_x"><x id="bar5_x">y</x></xn:d>
155 $dom = $this->html5->loadHTML($source, array(
156 'xmlNamespaces' => true
158 $this->assertFalse($this->html5->hasErrors(), print_r($this->html5->getErrors(), 1));
160 $stream = fopen('php://temp', 'w');
161 $r = new OutputRules($stream, $this->html5->getOptions());
162 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
165 $rendered = stream_get_contents($stream, - 1, 0);
167 $clear = function($s){
168 return trim(preg_replace('/[\s]+/', " ", $s));
171 $this->assertEquals($clear($source), $clear($rendered));
174 public function testElementWithScript()
176 $dom = $this->html5->loadHTML(
181 var $jQ = jQuery.noConflict();
182 // Use jQuery via $jQ(...)
183 $jQ(document).ready(function () {
184 $jQ("#mktFrmSubmit").wrap("<div class=\'buttonSubmit\'></div>");
185 $jQ(".buttonSubmit").prepend("<span></span>");
190 <div id="foo" class="bar baz">foo bar baz</div>
194 $stream = fopen('php://temp', 'w');
195 $r = new OutputRules($stream, $this->html5->getOptions());
196 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
198 $script = $dom->getElementsByTagName('script');
199 $r->element($script->item(0));
202 var $jQ = jQuery.noConflict();
203 // Use jQuery via $jQ(...)
204 $jQ(document).ready(function () {
205 $jQ("#mktFrmSubmit").wrap("<div class=\'buttonSubmit\'></div>");
206 $jQ(".buttonSubmit").prepend("<span></span>");
208 </script>', stream_get_contents($stream, - 1, 0));
211 public function testElementWithStyle()
213 $dom = $this->html5->loadHTML(
224 <div id="foo" class="bar baz">foo bar baz</div>
228 $stream = fopen('php://temp', 'w');
229 $r = new OutputRules($stream, $this->html5->getOptions());
230 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
232 $style = $dom->getElementsByTagName('style');
233 $r->element($style->item(0));
234 $this->assertEquals('<style>
238 </style>', stream_get_contents($stream, - 1, 0));
241 public function testOpenTag()
243 $dom = $this->html5->loadHTML('<!doctype html>
246 <div id="foo" class="bar baz">foo bar baz</div>
250 $stream = fopen('php://temp', 'w');
251 $r = new OutputRules($stream, $this->html5->getOptions());
252 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
254 $list = $dom->getElementsByTagName('div');
255 $m = $this->getProtectedMethod('openTag');
256 $m->invoke($r, $list->item(0));
257 $this->assertEquals('<div id="foo" class="bar baz">', stream_get_contents($stream, - 1, 0));
260 public function testCData()
262 $dom = $this->html5->loadHTML('<!doctype html>
265 <div><![CDATA[bar]]></div>
269 $stream = fopen('php://temp', 'w');
270 $r = new OutputRules($stream, $this->html5->getOptions());
271 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
273 $list = $dom->getElementsByTagName('div');
274 $r->cdata($list->item(0)->childNodes->item(0));
275 $this->assertEquals('<![CDATA[bar]]>', stream_get_contents($stream, - 1, 0));
277 $dom = $this->html5->loadHTML('<!doctype html>
284 $dom->getElementById('foo')->appendChild(new \DOMCdataSection("]]>Foo<[![CDATA test ]]>"));
286 $stream = fopen('php://temp', 'w');
287 $r = new OutputRules($stream, $this->html5->getOptions());
288 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
289 $list = $dom->getElementsByTagName('div');
290 $r->cdata($list->item(0)->childNodes->item(0));
292 $this->assertEquals('<![CDATA[]]]]><![CDATA[>Foo<[![CDATA test ]]]]><![CDATA[>]]>', stream_get_contents($stream, - 1, 0));
295 public function testComment()
297 $dom = $this->html5->loadHTML('<!doctype html>
300 <div><!-- foo --></div>
304 $stream = fopen('php://temp', 'w');
305 $r = new OutputRules($stream, $this->html5->getOptions());
306 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
308 $list = $dom->getElementsByTagName('div');
309 $r->comment($list->item(0)->childNodes->item(0));
310 $this->assertEquals('<!-- foo -->', stream_get_contents($stream, - 1, 0));
312 $dom = $this->html5->loadHTML('<!doctype html>
318 $dom->getElementById('foo')->appendChild(new \DOMComment('<!-- --> --> Foo -->'));
320 $stream = fopen('php://temp', 'w');
321 $r = new OutputRules($stream, $this->html5->getOptions());
322 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
324 $list = $dom->getElementsByTagName('div');
325 $r->comment($list->item(0)->childNodes->item(0));
327 // Could not find more definitive guidelines on what this should be. Went with
328 // what the HTML5 spec says and what \DOMDocument::saveXML() produces.
329 $this->assertEquals('<!--<!-- --> --> Foo -->-->', stream_get_contents($stream, - 1, 0));
332 public function testText()
334 $dom = $this->html5->loadHTML('<!doctype html>
337 <script>baz();</script>
341 $stream = fopen('php://temp', 'w');
342 $r = new OutputRules($stream, $this->html5->getOptions());
343 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
345 $list = $dom->getElementsByTagName('script');
346 $r->text($list->item(0)->childNodes->item(0));
347 $this->assertEquals('baz();', stream_get_contents($stream, - 1, 0));
349 $dom = $this->html5->loadHTML('<!doctype html>
351 <head id="foo"></head>
353 $foo = $dom->getElementById('foo');
354 $foo->appendChild(new \DOMText('<script>alert("hi");</script>'));
356 $stream = fopen('php://temp', 'w');
357 $r = new OutputRules($stream, $this->html5->getOptions());
358 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
360 $r->text($foo->firstChild);
361 $this->assertEquals('<script>alert("hi");</script>', stream_get_contents($stream, - 1, 0));
364 public function testNl()
366 list ($o, $s) = $this->getOutputRules();
368 $m = $this->getProtectedMethod('nl');
370 $this->assertEquals(PHP_EOL, stream_get_contents($s, - 1, 0));
373 public function testWr()
375 list ($o, $s) = $this->getOutputRules();
377 $m = $this->getProtectedMethod('wr');
378 $m->invoke($o, 'foo');
379 $this->assertEquals('foo', stream_get_contents($s, - 1, 0));
382 public function getEncData()
389 '&'<>"'
393 'This + is. a < test',
394 'This + is. a < test',
395 'This + is. a < test'
401 '.+#'
408 '.+#''
414 '&".<'
420 '&'<>"'
432 * Test basic encoding of text.
433 * @dataProvider getEncData
435 public function testEnc($isAttribute, $test, $expected, $expectedEncoded)
437 list ($o, $s) = $this->getOutputRules();
438 $m = $this->getProtectedMethod('enc');
440 $this->assertEquals($expected, $m->invoke($o, $test, $isAttribute));
442 list ($o, $s) = $this->getOutputRules(array(
443 'encode_entities' => true
445 $m = $this->getProtectedMethod('enc');
446 $this->assertEquals($expectedEncoded, $m->invoke($o, $test, $isAttribute));
450 * Test basic encoding of text.
451 * @dataProvider getEncData
453 public function testEscape($isAttribute, $test, $expected, $expectedEncoded)
455 list ($o, $s) = $this->getOutputRules();
456 $m = $this->getProtectedMethod('escape');
458 $this->assertEquals($expected, $m->invoke($o, $test, $isAttribute));
461 public function booleanAttributes()
464 array('<img alt="" ismap>'),
465 array('<img alt="">'),
466 array('<input type="radio" readonly>'),
467 array('<input type="radio" checked disabled>'),
468 array('<input type="checkbox" checked disabled>'),
469 array('<input type="radio" value="" checked disabled>'),
470 array('<div data-value=""></div>'),
471 array('<select disabled></select>'),
472 array('<div ng-app></div>'),
473 array('<script defer></script>'),
477 * @dataProvider booleanAttributes
479 public function testBooleanAttrs($html)
481 $dom = $this->html5->loadHTML('<!doctype html><html lang="en"><body>'.$html.'</body></html>');
483 $stream = fopen('php://temp', 'w');
484 $r = new OutputRules($stream, $this->html5->getOptions());
485 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
487 $node = $dom->getElementsByTagName('body')->item(0)->firstChild;
489 $m = $this->getProtectedMethod('attrs');
490 $m->invoke($r, $node);
492 $content = stream_get_contents($stream, - 1, 0);
494 $html = preg_replace('~<[a-z]+(.*)></[a-z]+>~', '\1', $html);
495 $html = preg_replace('~<[a-z]+(.*)/?>~', '\1', $html);
497 $this->assertEquals($content, $html);
501 public function testAttrs()
503 $dom = $this->html5->loadHTML('<!doctype html>
506 <div id="foo" class="bar baz">foo bar baz</div>
510 $stream = fopen('php://temp', 'w');
511 $r = new OutputRules($stream, $this->html5->getOptions());
512 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
514 $list = $dom->getElementsByTagName('div');
516 $m = $this->getProtectedMethod('attrs');
517 $m->invoke($r, $list->item(0));
519 $content = stream_get_contents($stream, - 1, 0);
520 $this->assertEquals(' id="foo" class="bar baz"', $content);
523 public function testSvg()
525 $dom = $this->html5->loadHTML(
529 <div id="foo" class="bar baz">foo bar baz</div>
530 <svg width="150" height="100" viewBox="0 0 3 2">
531 <rect width="1" height="2" x="0" fill="#008d46" />
532 <rect width="1" height="2" x="1" fill="#ffffff" />
533 <rect width="1" height="2" x="2" fill="#d2232c" />
534 <rect id="Bar" x="300" y="100" width="300" height="100" fill="rgb(255,255,0)">
535 <animate attributeName="x" attributeType="XML" begin="0s" dur="9s" fill="freeze" from="300" to="0" />
541 $stream = fopen('php://temp', 'w');
542 $r = new OutputRules($stream, $this->html5->getOptions());
543 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
545 $list = $dom->getElementsByTagName('svg');
546 $r->element($list->item(0));
547 $contents = stream_get_contents($stream, - 1, 0);
548 $this->assertRegExp('|<svg width="150" height="100" viewBox="0 0 3 2">|', $contents);
549 $this->assertRegExp('|<rect width="1" height="2" x="0" fill="#008d46" />|', $contents);
550 $this->assertRegExp('|<rect id="Bar" x="300" y="100" width="300" height="100" fill="rgb\(255,255,0\)">|', $contents);
553 public function testMath()
555 $dom = $this->html5->loadHTML(
559 <div id="foo" class="bar baz">foo bar baz</div>
562 <csymbol definitionURL="http://www.example.com/mathops/multiops.html#plusminus">
570 $stream = fopen('php://temp', 'w');
571 $r = new OutputRules($stream, $this->html5->getOptions());
572 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
574 $list = $dom->getElementsByTagName('math');
575 $r->element($list->item(0));
576 $content = stream_get_contents($stream, - 1, 0);
577 $this->assertRegExp('|<math>|', $content);
578 $this->assertRegExp('|<csymbol definitionURL="http://www.example.com/mathops/multiops.html#plusminus">|', $content);
581 public function testProcessorInstruction()
583 $dom = $this->html5->loadHTMLFragment('<?foo bar ?>');
585 $stream = fopen('php://temp', 'w');
586 $r = new OutputRules($stream, $this->html5->getOptions());
587 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
589 $r->processorInstruction($dom->firstChild);
590 $content = stream_get_contents($stream, - 1, 0);
591 $this->assertRegExp('|<\?foo bar \?>|', $content);
594 public function testAddressTag()
596 $dom = $this->html5->loadHTML(
601 <a href="../People/Raggett/">Dave Raggett</a>,
602 <a href="../People/Arnaud/">Arnaud Le Hors</a>,
603 contact persons for the <a href="Activity">W3C HTML Activity</a>
608 $stream = fopen('php://temp', 'w');
609 $r = new OutputRules($stream, $this->html5->getOptions());
610 $t = new Traverser($dom, $stream, $r, $this->html5->getOptions());
612 $list = $dom->getElementsByTagName('address');
613 $r->element($list->item(0));
614 $contents = stream_get_contents($stream, - 1, 0);
616 $this->assertRegExp('|<address>|', $contents);
617 $this->assertRegExp('|<a href="../People/Raggett/">Dave Raggett</a>,|', $contents);
618 $this->assertRegExp('|<a href="../People/Arnaud/">Arnaud Le Hors</a>,|', $contents);
619 $this->assertRegExp('|contact persons for the <a href="Activity">W3C HTML Activity</a>|', $contents);
620 $this->assertRegExp('|</address>|', $contents);
624 * Ensure direct DOM manipulation doesn't break TEXT_RAW elements (iframe, script, etc...)
626 public function testHandlingInvalidRawContent()
628 $dom = $this->html5->loadHTML(
630 <html lang="en" id="base">
632 <script id="template" type="x-tmpl-mustache">
638 $badNode = $dom->createElement("p", "Bar");
640 // modify the content of the TEXT_RAW element: <script id="template"> appending dom nodes
641 $styleElement = $dom->getElementById("template");
642 $styleElement->appendChild($badNode);
644 $contents = $this->html5->saveHTML($dom);
646 $this->assertTrue(strpos($contents, '<script id="template" type="x-tmpl-mustache">
648 <p>Bar</p></script>')!==false);