Sort out namespaced attributes

This commit is contained in:
J. King 2021-03-18 12:40:54 -04:00
parent 02add5633a
commit 82621a11e3
5 changed files with 100 additions and 61 deletions

View file

@ -68,7 +68,7 @@ class ParseError {
const UNEXPECTED_CHAR = 211; const UNEXPECTED_CHAR = 211;
const UNEXPECTED_EOF = 212; const UNEXPECTED_EOF = 212;
const UNEXPECTED_PARENT = 213; const UNEXPECTED_PARENT = 213;
const UNEXPECTED_ATTRIBUTE_VALUE = 214; const INVALID_NAMESPACE_ATTRIBUTE_VALUE = 214;
const FOSTERED_START_TAG = 215; const FOSTERED_START_TAG = 215;
const FOSTERED_END_TAG = 216; const FOSTERED_END_TAG = 216;
const FOSTERED_CHAR = 217; const FOSTERED_CHAR = 217;
@ -87,7 +87,7 @@ class ParseError {
self::UNEXPECTED_CHAR => 'Unexpected character data', self::UNEXPECTED_CHAR => 'Unexpected character data',
self::UNEXPECTED_EOF => 'Unexpected end of file', self::UNEXPECTED_EOF => 'Unexpected end of file',
self::UNEXPECTED_PARENT => 'Start tag <%s> not valid in parent <%s>', self::UNEXPECTED_PARENT => 'Start tag <%s> not valid in parent <%s>',
self::UNEXPECTED_ATTRIBUTE_VALUE => 'Unexpected value in attribute "%s"', self::INVALID_NAMESPACE_ATTRIBUTE_VALUE => 'Invalid value for attribute "%s"; it must have value "%s" or be omitted',
self::FOSTERED_START_TAG => 'Start tag <%s> moved to before table', self::FOSTERED_START_TAG => 'Start tag <%s> moved to before table',
self::FOSTERED_END_TAG => 'End tag </%s> moved to before table', self::FOSTERED_END_TAG => 'End tag </%s> moved to before table',
self::FOSTERED_CHAR => 'Character moved to before table', self::FOSTERED_CHAR => 'Character moved to before table',

View file

@ -70,41 +70,34 @@ abstract class TagToken extends Token {
$this->name = $name; $this->name = $name;
} }
public function getAttribute(string $name) { public function hasAttribute(string $name): bool {
$key = $this->_getAttributeKey($name); return (!is_null($this->_getAttributeKey($name)));
}
return (isset($this->attributes[$key])) ? $this->attributes[$key] : null; public function getAttribute(string $name) {
} $key = $this->_getAttributeKey($name);
return (isset($this->attributes[$key])) ? $this->attributes[$key] : null;
}
public function hasAttribute(string $name): bool { public function setAttribute(string $name, string $value) {
return (!is_null($this->_getAttributeKey($name))); $key = $this->_getAttributeKey($name);
} if (is_null($key)) {
$this->attributes[] = new TokenAttr($name, $value);
} else {
$attribute = &$this->attributes[$key];
$attribute->name = $name;
$attribute->value = $value;
}
}
public function removeAttribute(string $name) { private function _getAttributeKey(string $name) {
unset($this->attributes[$this->_getAttributeKey($name)]); foreach ($this->attributes as $key => $a) {
} if ($a->name === $name) {
return $key;
public function setAttribute(string $name, string $value) { }
$key = $this->_getAttributeKey($name); }
return null;
if (is_null($key)) { }
$this->attributes[] = new TokenAttr($name, $value);
} else {
$attribute = &$this->attributes[$key];
$attribute->name = $name;
$attribute->value = $value;
}
}
private function _getAttributeKey(string $name) {
foreach ($this->attributes as $key => $a) {
if ($a->name === $name) {
return $key;
}
}
return null;
}
} }
class StartTagToken extends TagToken { class StartTagToken extends TagToken {
@ -120,8 +113,12 @@ class EOFToken extends Token {
} }
class TokenAttr { class TokenAttr {
/** @var string The name of the attribute */
public $name; public $name;
/** @var string The attribute's value */
public $value; public $value;
/** @var string|null The attribute's namespace. This is normally null but may be set during tree construction */
public $namespace = null;
public function __construct(string $name, string $value) { public function __construct(string $name, string $value) {
$this->name = $name; $this->name = $name;

View file

@ -192,11 +192,11 @@ class TreeBuilder {
'xlink:show' => Parser::XLINK_NAMESPACE, 'xlink:show' => Parser::XLINK_NAMESPACE,
'xlink:title' => Parser::XLINK_NAMESPACE, 'xlink:title' => Parser::XLINK_NAMESPACE,
'xlink:type' => Parser::XLINK_NAMESPACE, 'xlink:type' => Parser::XLINK_NAMESPACE,
'xml:base' => Parser::XML_NAMESPACE, 'xml:id' => Parser::XML_NAMESPACE, // DEVIATION
'xml:lang' => Parser::XML_NAMESPACE, 'xml:lang' => Parser::XML_NAMESPACE,
'xml:space' => Parser::XML_NAMESPACE, 'xml:space' => Parser::XML_NAMESPACE,
'xmlns' => Parser::XMLNS_NAMESPACE, 'xmlns' => Parser::XMLNS_NAMESPACE,
'xmlns:xlink' => Parser::XLINK_NAMESPACE, 'xmlns:xlink' => Parser::XMLNS_NAMESPACE,
]; ];
# The following elements have varying levels of special parsing rules: HTMLs # The following elements have varying levels of special parsing rules: HTMLs
# address, applet, area, article, aside, base, basefont, bgsound, blockquote, # address, applet, area, article, aside, base, basefont, bgsound, blockquote,
@ -4277,29 +4277,21 @@ class TreeBuilder {
$element = $document->createElementNS($namespace, $localName); $element = $document->createElementNS($namespace, $localName);
# Append each attribute in the given token to element. # Append each attribute in the given token to element.
foreach ($token->attributes as $attr) { foreach ($token->attributes as $attr) {
$ns = null; # If element has an xmlns attribute in the XMLNS namespace whose value
if ($namespace) { # is not exactly the same as the element's namespace, that is a
// Determine the namespace URI for the prefix, if any # parse error. Similarly, if element has an xmlns:xlink attribute in
if (strpos($attr->name, "xml:") === 0) { # the XMLNS namespace whose value is not the XLink Namespace, that
$ns = Parser::XML_NAMESPACE; # is a parse error.
} elseif (strpos($attr->name, "xmlns:") === 0) { // NOTE: The specification is silent as to how to handle these
$ns = Parser::XMLNS_NAMESPACE; // attributes. We assume these bad attributes should be dropped,
} elseif (strpos($attr->name, "xlink:") === 0) { // since they break the DOM when added
$ns = Parser::XLINK_NAMESPACE; if ($attr->name === "xmlns" && $namespace !== null && $attr->value !== $namespace) {
} $this->error(ParseError::INVALID_NAMESPACE_ATTRIBUTE_VALUE, "xmlns", $namespace);
} elseif ($attr->name === "xmlns:xlink" && $namespace !== null && $attr->value !== Parser::XLINK_NAMESPACE) {
$this->error(ParseError::INVALID_NAMESPACE_ATTRIBUTE_VALUE, "xmlns:xlink", Parser::XLINK_NAMESPACE);
} else {
$element->setAttributeNS($attr->namespace, $attr->name, $attr->value);
} }
$element->setAttributeNS($ns, $attr->name, $attr->value);
}
# If element has an xmlns attribute in the XMLNS namespace whose value
# is not exactly the same as the element's namespace, that is a
# parse error. Similarly, if element has an xmlns:xlink attribute in
# the XMLNS namespace whose value is not the XLink Namespace, that
# is a parse error.
if ($element->hasAttributeNS(Parser::XMLNS_NAMESPACE, "xmlns") && $element->getAttributeNS(Parser::XMLNS_NAMESPACE, "xmlns") !== $element->namespaceURI) {
$this->error(ParseError::UNEXPECTED_ATTRIBUTE_VALUE, "xmlns");
}
if ($element->hasAttributeNS(Parser::XMLNS_NAMESPACE, "xmlns:link") && $element->getAttributeNS(Parser::XMLNS_NAMESPACE, "xmlns:xlink") !== Parser::XLINK_NAMESPACE) {
$this->error(ParseError::UNEXPECTED_ATTRIBUTE_VALUE, "xmlns:xlink");
} }
# Return element. # Return element.
return $element; return $element;

View file

@ -76,9 +76,6 @@ class TestTreeConstructor extends \PHPUnit\Framework\TestCase {
// run the tree builder // run the tree builder
try { try {
$treeBuilder->constructTree(); $treeBuilder->constructTree();
} catch (\DOMException $e) {
$this->markTestIncomplete('Requires implementation of the "Coercing an HTML DOM into an infoset" specification section');
return;
} catch (LoopException $e) { } catch (LoopException $e) {
$act = $this->balanceTree($this->serializeTree($doc, (bool) $fragmentContext), $exp); $act = $this->balanceTree($this->serializeTree($doc, (bool) $fragmentContext), $exp);
$this->assertEquals($exp, $act, $e->getMessage()."\n".$treeBuilder->debugLog); $this->assertEquals($exp, $act, $e->getMessage()."\n".$treeBuilder->debugLog);

View file

@ -0,0 +1,53 @@
#data
<!DOCTYPE html><svg xmlns="http://www.w3.org/2000/svg"/>
#errors
#document
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <svg svg>
#data
<!DOCTYPE html><svg xmlns="http://www.w3.org/1999/xlink"/>
#errors
(1,58): invalid-namespace-attribute-value
#document
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <svg svg>
#data
<!DOCTYPE html><svg xmlns:xlink="http://www.w3.org/1999/xlink"/>
#errors
#document
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <svg svg>
#data
<!DOCTYPE html><svg xlink:href="http://example.com/"/>
#errors
#document
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <svg svg>
| xlink href="http://example.com/"
#data
<!DOCTYPE html><svg xmlns:xlink="http://www.w3.org/1999/xhtml" xlink:href="http://example.com/"/>
#errors
(1,97): invalid-namespace-attribute-value
#document
| <!DOCTYPE html>
| <html>
| <head>
| <body>
| <svg svg>
| xlink href="http://example.com/"