diff options
-rw-r--r-- | src/HTML5/Serializer/OutputRules.php | 87 | ||||
-rw-r--r-- | test/HTML5/Serializer/OutputRulesTest.php | 38 |
2 files changed, 119 insertions, 6 deletions
diff --git a/src/HTML5/Serializer/OutputRules.php b/src/HTML5/Serializer/OutputRules.php index 537a4bf..7ea7c6a 100644 --- a/src/HTML5/Serializer/OutputRules.php +++ b/src/HTML5/Serializer/OutputRules.php @@ -43,25 +43,49 @@ class OutputRules implements \Masterminds\HTML5\Serializer\RulesInterface self::NAMESPACE_XMLNS, ); - const IM_IN_HTML = 1; const IM_IN_SVG = 2; const IM_IN_MATHML = 3; + /** + * Used as cache to detect if is available ENT_HTML5 + * @var boolean + */ private $hasHTML5 = false; protected $traverser; protected $encode = false; - protected $xpath; - protected $out; protected $outputMode; + private $xpath; + + protected $nonBooleanAttributes = array( + /* + array( + 'nodeNamespace'=>'http://www.w3.org/1999/xhtml', + 'attrNamespace'=>'http://www.w3.org/1999/xhtml', + + 'nodeName'=>'img', 'nodeName'=>array('img', 'a'), + 'attrName'=>'alt', 'attrName'=>array('title', 'alt'), + + + 'prefixes'=>['xh'=>'http://www.w3.org/1999/xhtml'), + 'xpath' => "@checked[../../xh:input[@type='radio' or @type='checkbox']]", + ), + */ + array( + 'nodeNamespace'=>'http://www.w3.org/1999/xhtml', + 'attrName'=>array('alt', 'title'), + ), + + ); + const DOCTYPE = '<!DOCTYPE html>'; public function __construct($output, $options = array()) @@ -76,6 +100,10 @@ class OutputRules implements \Masterminds\HTML5\Serializer\RulesInterface // If HHVM, see https://github.com/facebook/hhvm/issues/2727 $this->hasHTML5 = defined('ENT_HTML5') && !defined('HHVM_VERSION'); } + public function addRule(array $rule) + { + $this->nonBooleanAttributes[] = $rule; + } public function setTraverser(\Masterminds\HTML5\Serializer\Traverser $traverser) { @@ -259,12 +287,63 @@ class OutputRules implements \Masterminds\HTML5\Serializer\RulesInterface } $this->wr(' ')->wr($name); - if (isset($val) && $val !== '') { + + if ((isset($val) && $val !== '') || $this->nonBooleanAttribute($node)) { $this->wr('="')->wr($val)->wr('"'); } } } + + protected function nonBooleanAttribute(\DOMAttr $attr) + { + $ele = $attr->ownerElement; + foreach($this->nonBooleanAttributes as $rule){ + + if(isset($rule['nodeNamespace']) && $rule['nodeNamespace']!==$ele->namespaceURI){ + continue; + } + if(isset($rule['attNamespace']) && $rule['attNamespace']!==$attr->namespaceURI){ + continue; + } + if(isset($rule['nodeName']) && !is_array($rule['nodeName']) && $rule['nodeName']!==$ele->localName){ + continue; + } + if(isset($rule['nodeName']) && is_array($rule['nodeName']) && !in_array($ele->localName, $rule['nodeName'], true)){ + continue; + } + if(isset($rule['attrName']) && !is_array($rule['attrName']) && $rule['attrName']!==$attr->localName){ + continue; + } + if(isset($rule['attrName']) && is_array($rule['attrName']) && !in_array($attr->localName, $rule['attrName'], true)){ + continue; + } + if(isset($rule['xpath'])){ + + $xp = $this->getXPath($attr); + if(isset($rule['prefixes'])){ + foreach($rule['prefixes'] as $nsPrefix => $ns){ + $xp->registerNamespace($nsPrefix, $ns); + } + } + if(!$xp->query($rule['xpath'], $attr->ownerElement)->length){ + continue; + } + } + + return true; + } + + return false; + } + + private function getXPath(\DOMNode $node){ + if(!$this->xpath){ + $this->xpath = new \DOMXPath($node->ownerDocument); + } + return $this->xpath; + } + /** * Write the closing tag. * diff --git a/test/HTML5/Serializer/OutputRulesTest.php b/test/HTML5/Serializer/OutputRulesTest.php index 558e6b0..e89d723 100644 --- a/test/HTML5/Serializer/OutputRulesTest.php +++ b/test/HTML5/Serializer/OutputRulesTest.php @@ -439,12 +439,46 @@ class OutputRulesTest extends \Masterminds\HTML5\Tests\TestCase $this->assertEquals($expected, $m->invoke($o, $test, $isAttribute)); } + public function booleanAttributes() + { + return array( + array('<img alt="" ismap>'), + array('<img alt="">'), + array('<input type="radio" readonly>'), + array('<input type="radio" checked disabled>'), + array('<input type="checkbox" checked disabled>'), + array('<select disabled></select>'), + array('<div ng-app>foo</div>'), + array('<script defer></script>'), + ); + } + /** + * @dataProvider booleanAttributes + */ + public function testBooleanAttrs($html) + { + $dom = $this->html5->loadHTML('<!doctype html><html lang="en"><body>'.$html.'</body></html>'); + + $stream = fopen('php://temp', 'w'); + $r = new OutputRules($stream, $this->html5->getOptions()); + $t = new Traverser($dom, $stream, $r, $this->html5->getOptions()); + + $node = $dom->getElementsByTagName('body')->item(0)->firstChild; + + $m = $this->getProtectedMethod('attrs'); + $m->invoke($r, $node); + + $content = stream_get_contents($stream, - 1, 0); + $this->assertContains($content, $html); + + } + public function testAttrs() { $dom = $this->html5->loadHTML('<!doctype html> <html lang="en"> <body> - <div id="foo" class="bar baz" disabled>foo bar baz</div> + <div id="foo" class="bar baz">foo bar baz</div> </body> </html>'); @@ -458,7 +492,7 @@ class OutputRulesTest extends \Masterminds\HTML5\Tests\TestCase $m->invoke($r, $list->item(0)); $content = stream_get_contents($stream, - 1, 0); - $this->assertEquals(' id="foo" class="bar baz" disabled', $content); + $this->assertEquals(' id="foo" class="bar baz"', $content); } public function testSvg() |