'method',
'getAttributeNS' => 'method',
'getElementsByTagName' => 'method',
'getElementsByTagNameNS' => 'method',
'hasAttribute' => 'method',
'hasAttributeNS' => 'method',
'removeAttribute' => 'method',
'removeAttributeNS' => 'method',
'setAttribute' => 'method',
'setAttributeNS' => 'method',
// From DOMNode
'appendChild' => 'insert',
'insertBefore' => 'insert',
'replaceChild' => 'insert',
'cloneNode' => 'method',
'getLineNo' => 'method',
'hasAttributes' => 'method',
'hasChildNodes' => 'method',
'isSameNode' => 'method',
'lookupNamespaceURI'=> 'method',
'lookupPrefix' => 'method',
'normalize' => 'method',
'removeChild' => 'method',
'nodeName' => 'property',
'nodeValue' => 'property',
'nodeType' => 'property',
'parentNode' => 'property',
'childNodes' => 'property',
'firstChild' => 'property',
'lastChild' => 'property',
'previousSibling' => 'property',
'nextSibling' => 'property',
'namespaceURI' => 'property',
'prefix' => 'property',
'localName' => 'property',
'textContent' => 'property'
);
$dom = dom_import_simplexml($this);
if (!isset($passthrough[$name]))
{
if (method_exists($dom, $name))
{
throw new BadMethodCallException('DOM method ' . $name . '() is not supported');
}
if (property_exists($dom, $name))
{
throw new BadMethodCallException('DOM property ' . $name . ' is not supported');
}
throw new BadMethodCallException('Undefined method ' . get_class($this) . '::' . $name . '()');
}
switch ($passthrough[$name])
{
case 'insert':
if (isset($args[0])
&& $args[0] instanceof SimpleXMLElement)
{
$args[0] = $dom->ownerDocument->importNode(dom_import_simplexml($args[0]), true);
}
// no break; here
case 'method':
foreach ($args as &$arg)
{
if ($arg instanceof SimpleXMLElement)
{
$arg = dom_import_simplexml($arg);
}
}
unset($arg);
$ret = call_user_func_array(array($dom, $name), $args);
break;
case 'property':
$ret = $dom->$name;
break;
}
if ($ret instanceof DOMText)
{
return $ret->textContent;
}
if ($ret instanceof DOMNode)
{
if ($ret instanceof DOMAttr)
{
/**
* Methods that affect attributes can't return the attributes themselves. Instead,
* we make them chainable
*/
return $this;
}
return simplexml_import_dom($ret, get_class($this));
}
if ($ret instanceof DOMNodeList)
{
$class = get_class($this);
$list = array();
$i = -1;
while (++$i < $ret->length)
{
$node = $ret->item($i);
$list[$i] = ($node instanceof DOMText) ? $node->textContent : simplexml_import_dom($node, $class);
}
return $list;
}
return $ret;
}
//=================================
// DOM convenience methods
//=================================
/**
* Add a new sibling before this node
*
* This is a convenience method. The same result can be achieved with
*
* $node->parentNode()->insertBefore($new, $node);
*
*
* @param SimpleXMLElement $new New node
* @return SimpleDOM The inserted node
*/
public function insertBeforeSelf(SimpleXMLElement $new)
{
$tmp = dom_import_simplexml($this);
$node = $tmp->ownerDocument->importNode(dom_import_simplexml($new), true);
return simplexml_import_dom($this->insertNode($tmp, $node, 'before'), get_class($this));
}
/**
* Add a new sibling after this node
*
* This is a convenience method. The same result can be achieved with
*
* $node->parentNode()->insertBefore($new, $node->nextSibling());
*
*
* @param SimpleXMLElement $new New node
* @return SimpleDOM The inserted node
*/
public function insertAfterSelf(SimpleXMLElement $new)
{
$tmp = dom_import_simplexml($this);
$node = $tmp->ownerDocument->importNode(dom_import_simplexml($new), true);
return simplexml_import_dom($this->insertNode($tmp, $node, 'after'), get_class($this));
}
/**
* Delete this node from document
*
* This is a convenience method. The same result can be achieved with
*
* $node->parentNode()->removeChild($node);
*
*
* @return void
*/
public function deleteSelf()
{
$tmp = dom_import_simplexml($this);
if ($tmp->isSameNode($tmp->ownerDocument->documentElement))
{
throw new BadMethodCallException('deleteSelf() cannot be used to delete the root node');
}
$tmp->parentNode->removeChild($tmp);
}
/**
* Remove this node from document
*
* This is a convenience method. The same result can be achieved with
*
* $node->parentNode()->removeChild($node);
*
*
* @return SimpleDOM The removed node
*/
public function removeSelf()
{
$tmp = dom_import_simplexml($this);
if ($tmp->isSameNode($tmp->ownerDocument->documentElement))
{
throw new BadMethodCallException('removeSelf() cannot be used to remove the root node');
}
$node = $tmp->parentNode->removeChild($tmp);
return simplexml_import_dom($node, get_class($this));
}
/**
* Replace this node
*
* This is a convenience method. The same result can be achieved with
*
* $node->parentNode()->replaceChild($new, $node);
*
*
* @param SimpleXMLElement $new New node
* @return SimpleDOM Replaced node on success
*/
public function replaceSelf(SimpleXMLElement $new)
{
$old = dom_import_simplexml($this);
$new = $old->ownerDocument->importNode(dom_import_simplexml($new), true);
$node = $old->parentNode->replaceChild($new, $old);
return simplexml_import_dom($node, get_class($this));
}
/**
* Delete all elements matching a XPath expression
*
* @param string $xpath XPath expression
* @return integer Number of nodes removed
*/
public function deleteNodes($xpath)
{
if (!is_string($xpath))
{
throw new InvalidArgumentException('Argument 1 passed to deleteNodes() must be a string, ' . gettype($xpath) . ' given');
}
$nodes = $this->_xpath($xpath);
if (isset($nodes[0]))
{
$tmp = dom_import_simplexml($nodes[0]);
if ($tmp->isSameNode($tmp->ownerDocument->documentElement))
{
unset($nodes[0]);
}
}
foreach ($nodes as $node)
{
$node->deleteSelf();
}
return count($nodes);
}
/**
* Remove all elements matching a XPath expression
*
* @param string $xpath XPath expression
* @return array Array of removed nodes on success or FALSE on failure
*/
public function removeNodes($xpath)
{
if (!is_string($xpath))
{
throw new InvalidArgumentException('Argument 1 passed to removeNodes() must be a string, ' . gettype($xpath) . ' given');
}
$nodes = $this->_xpath($xpath);
if (isset($nodes[0]))
{
$tmp = dom_import_simplexml($nodes[0]);
if ($tmp->isSameNode($tmp->ownerDocument->documentElement))
{
unset($nodes[0]);
}
}
$return = array();
foreach ($nodes as $node)
{
$return[] = $node->removeSelf();
}
return $return;
}
/**
* Remove all elements matching a XPath expression
*
* @param string $xpath XPath expression
* @param SimpleXMLElement $new Replacement node
* @return array Array of replaced nodes on success or FALSE on failure
*/
public function replaceNodes($xpath, SimpleXMLElement $new)
{
if (!is_string($xpath))
{
throw new InvalidArgumentException('Argument 1 passed to replaceNodes() must be a string, ' . gettype($xpath) . ' given');
}
$nodes = array();
foreach ($this->_xpath($xpath) as $node)
{
$nodes[] = $node->replaceSelf($new);
}
return $nodes;
}
/**
* Copy all attributes from a node to current node
*
* @param SimpleXMLElement $src Source node
* @param bool $overwrite If TRUE, overwrite existing attributes.
* Otherwise, ignore duplicate attributes
* @return SimpleDOM Current node
*/
public function copyAttributesFrom(SimpleXMLElement $src, $overwrite = true)
{
$dom = dom_import_simplexml($this);
foreach (dom_import_simplexml($src)->attributes as $attr)
{
if ($overwrite
|| !$dom->hasAttributeNS($attr->namespaceURI, $attr->nodeName))
{
$dom->setAttributeNS($attr->namespaceURI, $attr->nodeName, $attr->nodeValue);
}
}
return $this;
}
/**
* Clone all children from a node and add them to current node
*
* This method takes a snapshot of the children nodes then append them in order to avoid infinite
* recursion if the destination node is a descendant of or the source node itself
*
* @param SimpleXMLElement $src Source node
* @param bool $deep If TRUE, clone descendant nodes as well
* @return SimpleDOM Current node
*/
public function cloneChildrenFrom(SimpleXMLElement $src, $deep = true)
{
$src = dom_import_simplexml($src);
$dst = dom_import_simplexml($this);
$doc = $dst->ownerDocument;
$fragment = $doc->createDocumentFragment();
foreach ($src->childNodes as $child)
{
$fragment->appendChild($doc->importNode($child->cloneNode($deep), $deep));
}
$dst->appendChild($fragment);
return $this;
}
/**
* Move current node to a new parent
*
* ATTENTION! using references to the old node will screw up the original document
*
* @param SimpleXMLElement $dst Target parent
* @return SimpleDOM Current node
*/
public function moveTo(SimpleXMLElement $dst)
{
return simplexml_import_dom(dom_import_simplexml($dst), get_class($this))->appendChild($this->removeSelf());
}
/**
* Return the first node of the result of an XPath expression
*
* @param string $xpath XPath expression
* @return mixed SimpleDOM object if any node was returned, NULL otherwise
*/
public function firstOf($xpath)
{
$nodes = $this->xpath($xpath);
return (isset($nodes[0])) ? $nodes[0] : null;
}
//=================================
// DOM extra
//=================================
/**
* Insert a CDATA section
*
* @param string $content CDATA content
* @param string $mode Where to add this node: 'append' to current node,
* 'before' current node or 'after' current node
* @return SimpleDOM Current node
*/
public function insertCDATA($content, $mode = 'append')
{
$this->insert('CDATASection', $content, $mode);
return $this;
}
/**
* Insert a comment node
*
* @param string $content Comment content
* @param string $mode Where to add this node: 'append' to current node,
* 'before' current node or 'after' current node
* @return SimpleDOM Current node
*/
public function insertComment($content, $mode = 'append')
{
$this->insert('Comment', $content, $mode);
return $this;
}
/**
* Insert a text node
*
* @param string $content CDATA content
* @param string $mode Where to add this node: 'append' to current node,
* 'before' current node or 'after' current node
* @return SimpleDOM Current node
*/
public function insertText($content, $mode = 'append')
{
$this->insert('TextNode', $content, $mode);
return $this;
}
/**
* Insert raw XML data
*
* @param string $xml XML to insert
* @param string $mode Where to add this tag: 'append' to current node,
* 'before' current node or 'after' current node
* @return SimpleDOM Current node
*/
public function insertXML($xml, $mode = 'append')
{
$tmp = dom_import_simplexml($this);
$fragment = $tmp->ownerDocument->createDocumentFragment();
/**
* Disable error reporting
*/
$use_errors = libxml_use_internal_errors(true);
if (!$fragment->appendXML($xml))
{
libxml_use_internal_errors($use_errors);
throw new InvalidArgumentException(libxml_get_last_error()->message);
}
libxml_use_internal_errors($use_errors);
$this->insertNode($tmp, $fragment, $mode);
return $this;
}
/**
* Insert a Processing Instruction
*
* The content of the PI can be passed either as string or as an associative array.
*
* @param string $target Target of the processing instruction
* @param string|array $data Content of the processing instruction
* @return bool TRUE on success, FALSE on failure
*/
public function insertPI($target, $data = null, $mode = 'before')
{
$tmp = dom_import_simplexml($this);
$doc = $tmp->ownerDocument;
if (isset($data))
{
if (is_array($data))
{
$str = '';
foreach ($data as $k => $v)
{
$str .= $k . '="' . htmlspecialchars($v) . '" ';
}
$data = substr($str, 0, -1);
}
else
{
$data = (string) $data;
}
$pi = $doc->createProcessingInstruction($target, $data);
}
else
{
$pi = $doc->createProcessingInstruction($target);
}
if ($pi !== false)
{
$this->insertNode($tmp, $pi, $mode);
}
return $this;
}
/**
* Set several attributes at once
*
* @param array $attr Attributes as name => value pairs
* @param string $ns Namespace for the attributes
* @return SimpleDOM Current node
*/
public function setAttributes(array $attr, $ns = null)
{
$dom = dom_import_simplexml($this);
foreach ($attr as $k => $v)
{
$dom->setAttributeNS($ns, $k, $v);
}
return $this;
}
/**
* Return the content of current node as a string
*
* Roughly emulates the innerHTML property found in browsers, although it is not meant to
* perfectly match any specific implementation.
*
* @todo Write a test for HTML entities that can't be represented in the document's encoding
*
* @return string Content of current node
*/
public function innerHTML()
{
$dom = dom_import_simplexml($this);
$doc = $dom->ownerDocument;
$html = '';
foreach ($dom->childNodes as $child)
{
$html .= ($child instanceof DOMText) ? $child->textContent : $doc->saveXML($child);
}
return $html;
}
/**
* Return the XML content of current node as a string
*
* @return string Content of current node
*/
public function innerXML()
{
$xml = $this->outerXML();
$pos = 1 + strpos($xml, '>');
$len = strrpos($xml, '<') - $pos;
return substr($xml, $pos, $len);
}
/**
* Return the XML representing this node and its child nodes
*
* NOTE: unlike asXML() it doesn't return the XML prolog
*
* @return string Content of current node
*/
public function outerXML()
{
$dom = dom_import_simplexml($this);
return $dom->ownerDocument->saveXML($dom);
}
/**
* Return all elements with the given class name
*
* Should work like DOM0's method
*
* @param string $class Class name
* @return array Array of SimpleDOM nodes
*/
public function getElementsByClassName($class)
{
if (strpos($class, '"') !== false
|| strpos($class, "'") !== false)
{
return array();
}
$xpath = './/*[contains(concat(" ", @class, " "), " ' . htmlspecialchars($class) . ' ")]';
return $this->xpath($xpath);
}
/**
* Test whether current node has given class
*
* @param string $class Class name
* @return bool
*/
public function hasClass($class)
{
return in_array($class, explode(' ', $this['class']));
}
/**
* Add given class to current node
*
* @param string $class Class name
* @return SimpleDOM Current node
*/
public function addClass($class)
{
if (!$this->hasClass($class))
{
$current = (string) $this['class'];
if ($current !== ''
&& substr($current, -1) !== ' ')
{
$this['class'] .= ' ';
}
$this['class'] .= $class;
}
return $this;
}
/**
* Remove given class from current node
*
* @param string $class Class name
* @return SimpleDOM Current node
*/
public function removeClass($class)
{
while ($this->hasClass($class))
{
$this['class'] = substr(str_replace(' ' . $class . ' ', ' ', ' ' . $this['class'] . ' '), 1, -1);
}
return $this;
}
//=================================
// Utilities
//=================================
/**
* Return the current element as a DOMElement
*
* @return DOMElement
*/
public function asDOM()
{
return dom_import_simplexml($this);
}
/**
* Return the current node slightly prettified
*
* Elements will be indented, empty elements will be minified. The result isn't mean to be
* perfect, I'm sure there are better prettifiers out there.
*
* @param string $filepath If set, save the result to this file
* @return mixed If $filepath is set, will return TRUE if the file was
* succesfully written or FALSE otherwise. If $filepath isn't set,
* it returns the result as a string
*/
public function asPrettyXML($filepath = null)
{
/**
* Dump and reload this node's XML with LIBXML_NOBLANKS.
*
* Also import it as a DOMDocument because some older of XSLTProcessor rejected
* SimpleXMLElement as a source.
*/
$xml = dom_import_simplexml(new SimpleXMLElement(
$this->asXML()
));
$xsl = new DOMDocument;
$xsl->loadXML(
'
');
$xslt = new XSLTProcessor;
$xslt->importStylesheet($xsl);
$result = trim($xslt->transformToXML($xml));
if (isset($filepath))
{
return (bool) file_put_contents($filepath, $result);
}
return $result;
}
/**
* Transform current node and return the result
*
* Will take advantage of {@link http://pecl.php.net/package/xslcache PECL's xslcache}
* if available
*
* @param string $filepath Path to stylesheet
* @param bool $use_xslcache If TRUE, use the XSL Cache extension if available
* @return string Result
*/
public function XSLT($filepath, $use_xslcache = true)
{
if ($use_xslcache && extension_loaded('xslcache'))
{
$xslt = new XSLTCache;
$xslt->importStylesheet($filepath);
}
else
{
$xsl = new DOMDocument;
$xsl->load($filepath);
$xslt = new XSLTProcessor;
$xslt->importStylesheet($xsl);
}
return $xslt->transformToXML(dom_import_simplexml($this));
}
/**
* Run an XPath query and sort the result
*
* This method accepts any number of arguments in a way similar to {@link
* http://docs.php.net/manual/en/function.array-multisort.php array_multisort()}
*
*
* // Retrieve all nodes, sorted by @foo ascending, @bar descending
* $root->sortedXPath('//x', '@foo', '@bar', SORT_DESC);
*
* // Same, but sort @foo numerically and @bar as strings
* $root->sortedXPath('//x', '@foo', SORT_NUMERIC, '@bar', SORT_STRING, SORT_DESC);
*
*
* @param string $xpath XPath expression
* @return void
*/
public function sortedXPath($xpath)
{
$nodes = $this->xpath($xpath);
$args = func_get_args();
$args[0] =& $nodes;
call_user_func_array(array(get_class($this), 'sort'), $args);
return $nodes;
}
/**
* Sort this node's children
*
* ATTENTION: text nodes are not supported. If current node has text nodes, they may be lost in
* the process
*
* @return SimpleDOM This node
*/
public function sortChildren()
{
$nodes = $this->removeNodes('*');
$args = func_get_args();
array_unshift($args, null);
$args[0] =& $nodes;
call_user_func_array(array(get_class($this), 'sort'), $args);
foreach ($nodes as $node)
{
$this->appendChild($node);
}
return $this;
}
/**
* Sort an array of nodes
*
* Note that nodes are sorted in place, nothing is returned
*
* @see sortedXPath
*
* @param array &$nodes Array of SimpleXMLElement
* @return void
*/
static public function sort(array &$nodes)
{
$args = func_get_args();
unset($args[0]);
$sort = array();
$tmp = array();
foreach ($args as $k => $arg)
{
if (is_string($arg))
{
$tmp[$k] = array();
if (preg_match('#^@?[a-z_0-9]+$#Di', $arg))
{
if ($arg[0] === '@')
{
$name = substr($arg, 1);
foreach ($nodes as $node)
{
$tmp[$k][] = (string) $node[$name];
}
}
else
{
foreach ($nodes as $node)
{
$tmp[$k][] = (string) $node->$arg;
}
}
}
elseif (preg_match('#^current\\(\\)|text\\(\\)|\\.$#i', $arg))
{
/**
* If the XPath is current() or text() or . we use this node's textContent
*/
foreach ($nodes as $node)
{
$tmp[$k][] = dom_import_simplexml($node)->textContent;
}
}
else
{
foreach ($nodes as $node)
{
$_nodes = $node->xpath($arg);
$tmp[$k][] = (empty($_nodes)) ? '' : (string) $_nodes[0];
}
}
}
else
{
$tmp[$k] = $arg;
}
/**
* array_multisort() wants everything to be passed as reference so we have to cheat
*/
$sort[] =& $tmp[$k];
}
$sort[] =& $nodes;
call_user_func_array('array_multisort', $sort);
}
//=================================
// Internal stuff
//=================================
/**#@+
* @ignore
*/
protected function _xpath($xpath)
{
$use_errors = libxml_use_internal_errors(true);
$nodes = $this->xpath($xpath);
libxml_use_internal_errors($use_errors);
if ($nodes === false)
{
throw new InvalidArgumentException('Invalid XPath expression ' . $xpath);
}
return $nodes;
}
protected function insert($type, $content, $mode)
{
$tmp = dom_import_simplexml($this);
$method = 'create' . $type;
$node = $tmp->ownerDocument->$method($content);
return $this->insertNode($tmp, $node, $mode);
}
protected function insertNode(DOMNode $tmp, DOMNode $node, $mode)
{
if ($mode === 'before'
|| $mode === 'after')
{
if ($node instanceof DOMText
|| $node instanceof DOMElement
|| $node instanceof DOMDocumentFragment)
{
if ($tmp->isSameNode($tmp->ownerDocument->documentElement))
{
throw new BadMethodCallException('Cannot insert a ' . get_class($node) . ' node outside of the root node');
}
}
if ($mode === 'before')
{
return $tmp->parentNode->insertBefore($node, $tmp);
}
if ($tmp->nextSibling)
{
return $tmp->parentNode->insertBefore($node, $tmp->nextSibling);
}
return $tmp->parentNode->appendChild($node);
}
return $tmp->appendChild($node);
}
/**
* NOTE: in order to support LSB, __CLASS__ would need to be replaced by get_called_class() and
* this method would need to be invoked via static:: instead of self::
*/
static protected function fromHTML($method, $arg, &$errors)
{
$old = libxml_use_internal_errors(true);
$cnt = count(libxml_get_errors());
$dom = new DOMDocument;
$dom->$method($arg);
$errors = array_slice(libxml_get_errors(), $cnt);
libxml_use_internal_errors($old);
return simplexml_import_dom($dom, __CLASS__);
}
/**#@-*/
}