Browse Source

Added Element::classList

ns
Dustin Wilson 3 years ago
parent
commit
c023e35f9c
  1. 4
      lib/DOM/DOMException.php
  2. 24
      lib/DOM/Document.php
  3. 50
      lib/DOM/Element.php
  4. 13
      lib/DOM/TemplateElement.php
  5. 307
      lib/DOM/TokenList.php
  6. 2
      lib/TreeBuilder.php

4
lib/DOM/DOMException.php

@ -9,7 +9,9 @@ namespace MensBeam\HTML;
class DOMException extends \Exception {
// From PHP's DOMException; keeping error codes consistent
const WRONG_DOCUMENT = 4;
const INVALID_CHARACTER = 5;
const NO_MODIFICATION_ALLOWED = 7;
const SYNTAX_ERROR = 12;
const DOCUMENT_ELEMENT_DOCUMENTFRAG_EXPECTED = 100;
const STRING_EXPECTED = 101;
@ -17,7 +19,9 @@ class DOMException extends \Exception {
protected static $messages = [
4 => 'Supplied node does not belong to this document',
5 => 'Invalid character',
7 => 'Modification not allowed here',
12 => 'Syntax error',
100 => 'Document, Element, or DocumentFragment expected; found %s',
101 => 'The "%s" argument should be a string; found %s',
102 => 'Failed to set the "outerHTML" property; the element does not have a parent node'

24
lib/DOM/Document.php

@ -66,16 +66,7 @@ class Document extends \DOMDocument {
if ($name !== 'template') {
$e = parent::createElement($name, $value);
} else {
$e = new TemplateElement($name, $value);
// Elements that are created by their constructor in PHP aren't owned by any
// document and are readonly until owned by one. Temporarily append to a
// document fragment so the element will be owned by the document.
$frag = $this->createDocumentFragment();
$frag->appendChild($e);
$frag->removeChild($e);
unset($frag);
$e = new TemplateElement($this, $name, $value);
$this->templateElements[] = $e;
$e->content = $this->createDocumentFragment();
}
@ -93,19 +84,10 @@ class Document extends \DOMDocument {
public function createElementNS($namespaceURI, $qualifiedName, $value = "") {
try {
if ($qualifiedName !== 'template' && $namespaceURI !== null) {
if ($qualifiedName !== 'template' || $namespaceURI !== null) {
$e = parent::createElementNS($namespaceURI, $qualifiedName, $value);
} else {
$e = new TemplateElement($qualifiedName, $value);
// Elements that are created by their constructor in PHP aren't owned by any
// document and are readonly until owned by one. Temporarily append to a
// document fragment so the element will be owned by the document.
$frag = $this->createDocumentFragment();
$frag->appendChild($e);
$frag->removeChild($e);
unset($frag);
$e = new TemplateElement($this, $qualifiedName, $value);
$this->templateElements[] = $e;
$e->content = $this->createDocumentFragment();
}

50
lib/DOM/Element.php

@ -9,9 +9,37 @@ namespace MensBeam\HTML;
class Element extends \DOMElement {
use EscapeString, Moonwalk, Serialize, Walk;
protected $_classList;
public function getAttribute($name) {
// Newer versions of the DOM spec have getAttribute return an empty string only
// when the attribute exists and is empty, otherwise null. This fixes that.
$value = parent::getAttribute($name);
if ($value === '' && !$this->hasAttribute($name)) {
return null;
}
return $value;
}
public function getAttributeNS($namespaceURI, $qualifiedName) {
// Newer versions of the DOM spec have getAttributeNS return an empty string
// only when the attribute exists and is empty, otherwise null. This fixes that.
$value = parent::getAttributeNS($namespaceURI, $qualifiedName);
if ($value === '' && !$this->hasAttribute($qualifiedName)) {
return null;
}
return $value;
}
public function setAttribute($name, $value) {
try {
parent::setAttribute($name, $value);
if ($this->_classList !== null && $name === 'class') {
$this->_classList->value = $value;
} else {
parent::setAttribute($name, $value);
}
} catch (\DOMException $e) {
// The attribute name is invalid for XML
// Replace any offending characters with "UHHHHHH" where H are the
@ -27,7 +55,11 @@ class Element extends \DOMElement {
public function setAttributeNS($namespaceURI, $qualifiedName, $value) {
try {
parent::setAttributeNS($namespaceURI, $qualifiedName, $value);
if ($namespaceURI === null && $this->_classList !== null && $qualifiedName === 'class') {
$this->_classList->value = $value;
} else {
parent::setAttributeNS($namespaceURI, $qualifiedName, $value);
}
} catch (\DOMException $e) {
// The attribute name is invalid for XML
// Replace any offending characters with "UHHHHHH" where H are the
@ -59,6 +91,20 @@ class Element extends \DOMElement {
public function __get(string $prop) {
switch ($prop) {
case 'classList':
// MensBeam\HTML\TokenList uses WeakReference to prevent a circular reference,
// so it requires PHP 7.4 to work.
if (version_compare(\PHP_VERSION, '7.4.0', '>=')) {
// Only create the class list if it is actually used.
if ($this->_classList === null) {
$this->_classList = new TokenList($this, 'class');
}
return $this->_classList;
}
return null;
break;
### DOM Parsing Specification ###
# 2.3 The InnerHTML mixin
#

13
lib/DOM/TemplateElement.php

@ -9,4 +9,17 @@ namespace MensBeam\HTML;
/** Class specifically for template elements to handle its content property. */
class TemplateElement extends Element {
public $content = null;
public function __construct(Document $ownerDocument, string $qualifiedName, ?string $value = null, string $namespace = '') {
parent::__construct($qualifiedName, $value, $namespace);
// Elements that are created by their constructor in PHP aren't owned by any
// document and are readonly until owned by one. Temporarily append to a
// document fragment so the element will be owned by the supplied owner
// document.
$frag = $ownerDocument->createDocumentFragment();
$frag->appendChild($this);
$frag->removeChild($this);
unset($frag);
}
}

307
lib/DOM/TokenList.php

@ -0,0 +1,307 @@
<?php
/** @license MIT
* Copyright 2017 , Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace MensBeam\HTML;
class TokenList implements \ArrayAccess, \Countable, \Iterator {
protected $localName;
protected $element;
protected $_length = 0;
protected $position = 0;
# A DOMTokenList object has an associated token set (a set), which is initially
# empty.
protected $tokenSet = [];
private const ASCII_WHITESPACE_REGEX = '/[\t\n\x0c\r ]+/';
public function __construct(\DOMElement $element, string $attributeLocalName) {
# A DOMTokenList object also has an associated element and an attribute’s local
# name.
# When a DOMTokenList object is created, then:
#
# 1. Let element be associated element.
// Using a weak reference here to prevent a circular reference.
$this->element = \WeakReference::create($element);
# 2. Let localName be associated attribute’s local name.
$this->localName = $attributeLocalName;
# 3. Let value be the result of getting an attribute value given element and
# localName.
$value = $element->getAttribute($attributeLocalName);
# 4. Run the attribute change steps for element, localName, value, value, and
# null.
$this->attributeChange($attributeLocalName, $value, $value);
}
public function add(...$tokens) {
# 1. For each token in tokens:
foreach ($tokens as $token) {
# 1. If token is the empty string, then throw a "SyntaxError" DOMException.
if ($token === '') {
throw new DOMException(DOMException::SYNTAX_ERROR);
}
# 2. If token contains any ASCII whitespace, then throw an
# "InvalidCharacterError" DOMException.
if (preg_match(self::ASCII_WHITESPACE_REGEX, $token)) {
throw new DOMException(DOMException::INVALID_CHARACTER);
}
}
# 2. For each token in tokens, append token to this’s token set.
foreach ($tokens as $token) {
if (!in_array($token, $this->tokenSet)) {
// The spec does not say to trim, but browsers do.
$this->tokenSet[] = trim($token);
$this->_length++;
}
}
# 3. Run the update steps.
$this->update();
}
public function contains(string $token): bool {
return (in_array($token, $this->tokenSet));
}
public function count(): int {
return $this->_length;
}
public function current() {
return $this->item($this->position);
}
public function item(int $index): string {
return $this->tokenSet[$index];
}
public function key() {
return $this->position;
}
public function next() {
++$this->position;
}
public function rewind() {
$this->position = 0;
}
public function offsetExists($offset) {
return $this->contains($offset);
}
public function offsetGet($offset): string {
return $this->item($offset);
}
public function offsetSet($offset, $value) {
$this->add($offset);
}
public function offsetUnset($offset) {
$this->remove($offset);
}
public function remove(...$tokens) {
# 1. For each token in tokens:
foreach ($tokens as $token) {
# 1. If token is the empty string, then throw a "SyntaxError" DOMException.
if ($token === '') {
throw new DOMException(DOMException::SYNTAX_ERROR);
}
# 2. If token contains any ASCII whitespace, then throw an
# "InvalidCharacterError" DOMException.
if (preg_match(self::ASCII_WHITESPACE_REGEX, $token)) {
throw new DOMException(DOMException::INVALID_CHARACTER);
}
}
# For each token in tokens, remove token from this’s token set.
$changed = false;
foreach ($tokens as $token) {
if (in_array($token, $this->tokenSet)) {
unset($this->tokenSet[$token]);
$this->_length--;
$changed = true;
}
}
if ($changed) {
$this->tokenSet = array_values($this->tokenSet);
}
# 3. Run the update steps.
$this->update();
}
public function replace(string $token, string $newToken): bool {
# 1. If either token or newToken is the empty string, then throw a "SyntaxError"
# DOMException.
if ($token === '' || $newToken === '') {
throw new DOMException(DOMException::SYNTAX_ERROR);
}
# 2. If either token or newToken contains any ASCII whitespace, then throw an
# "InvalidCharacterError" DOMException.
if (preg_match(self::ASCII_WHITESPACE_REGEX, $token) || preg_match(self::ASCII_WHITESPACE_REGEX, $newToken)) {
throw new DOMException(DOMException::INVALID_CHARACTER);
}
// The spec does not say to trim, but browsers do.
$token = trim($token);
$newToken = trim($token);
# 3. If this’s token set does not contain token, then return false.
if (!isset($this->tokenSet[$token])) {
return false;
}
# 4. Replace token in this’s token set with newToken.
$index = array_search($token, $this->tokenSet);
$this->tokenSet[$index] = $newToken;
# 5. Run the update steps.
$this->update();
# 6. Return true.
return true;
}
public function supports(string $token): bool {
# 1. Let result be the return value of validation steps called with token.
# 2. Return result.
#
# A DOMTokenList object’s validation steps for a given token are:
#
# 1. If the associated attribute’s local name does not define supported tokens,
# throw a TypeError.
# 2. Let lowercase token be a copy of token, in ASCII lowercase.
# 3. If lowercase token is present in supported tokens, return true.
# 4. Return false.
// This class is presently only used for Element::classList, and it supports any
// valid class name as a token. So, there's nothing to do here at the moment.
// Just return true.
return true;
}
public function toggle(string $token, ?bool $force = false): bool {
# 1. If token is the empty string, then throw a "SyntaxError" DOMException.
if ($token === '') {
throw new DOMException(DOMException::SYNTAX_ERROR);
}
# 2. If token contains any ASCII whitespace, then throw an
# "InvalidCharacterError" DOMException.
if (preg_match(self::ASCII_WHITESPACE_REGEX, $token)) {
throw new DOMException(DOMException::INVALID_CHARACTER);
}
# 3. If this’s token set[token] exists, then:
if (isset($this->tokenSet[$token])) {
# 1. If force is either not given or is false, then remove token from this’s
# token set, run the update steps and return false.
if (!$force) {
$this->remove($token);
return false;
}
# 2. Return true.
return true;
}
# 4. Otherwise, if force not given or is true, append token to this’s token set,
# run the update steps, and return true.
else {
$this->add($token);
return true;
}
# 5. Return false.
return false;
}
public function valid() {
return array_key_exists($this->position, $this->tokenSet);
}
protected function attributeChange(string $localName, ?string $oldValue = null, ?string $value = null, ?string $namespace = null) {
# A DOMTokenList object has these attribute change steps for its associated
# element:
#
# 1. If localName is associated attribute’s local name, namespace is null, and
# value is null, then empty token set.
if ($localName !== $this->localName || $namespace !== null) {
return;
}
if ($value === null) {
$this->tokenSet = [];
$this->tokenKeys = [];
$this->_length = 0;
}
# 2. Otherwise, if localName is associated attribute’s local name, namespace is
# null, then set token set to value, parsed.
else {
$this->tokenSet = $this->parseOrderedSet($value);
$this->_length = count($this->tokenSet);
}
}
protected function parseOrderedSet(string $input) {
if ($input === '') {
return [];
}
# The ordered set parser takes a string input and then runs these steps:
#
# 1. Let inputTokens be the result of splitting input on ASCII whitespace.
// There isn't a Set in php, so make sure all the tokens are unique.
$inputTokens = array_unique(preg_split(self::ASCII_WHITESPACE_REGEX, $input));
# 2. Let tokens be a new ordered set.
# 3. For each token in inputTokens, append token to tokens.
# 4. Return tokens.
// There isn't a Set in php, so just return the uniqued input tokens.
return $inputTokens;
}
protected function update() {
// Create the attribute using createAttribute because setAttribute has been
// extended to use TokenList when necessary.
$element = $this->element->get();
$doc = $element->ownerDocument;
$class = $doc->createAttribute($this->localName);
$class->value = $this->__toString();
$element->appendChild($class);
}
public function __get(string $prop) {
switch ($prop) {
case 'length': return $this->_length;
break;
case 'value': return $this->__toString();
break;
}
}
public function __set(string $prop, $value) {
if ($prop === 'value') {
$this->tokenSet = $this->parseOrderedSet($value);
$this->_length = count($this->tokenSet);
}
}
public function __toString(): string {
# The ordered set serializer takes a set and returns the concatenation of set
# using U+0020 SPACE.
return implode(' ', $this->tokenSet);
}
}

2
lib/TreeBuilder.php

@ -4177,7 +4177,7 @@ class TreeBuilder {
}
public function isHTMLIntegrationPoint(Element $e): bool {
$encoding = strtolower($e->getAttribute('encoding'));
$encoding = strtolower((string)$e->getAttribute('encoding'));
return ((
$e->namespaceURI === Parser::MATHML_NAMESPACE &&
$e->nodeName === 'annotation-xml' && (

Loading…
Cancel
Save