Node.php

<?php

namespace Taeluf\PHTML;

/**
* less sucky HTML DOM Element
*
* This class extends PHP's DOMElement to make it suck less
*
* The original version of this file was a pre-made script by the author below.
* The only meaningful pieces of code I kept are the two `if 'innerHTML'` blocks of code.
* No license information was available in the copied code and I don't remember what it said on the author's website.
*
* The original package had the following notes from the author:
* 
*	- Authored by: Keyvan Minoukadeh - http://www.keyvan.net - keyvan@keyvan.net
*   - See: http://fivefilters.org (the project this was written for)
*
* 
*/
class Node extends \DOMElement
{


    // public $hideOwnTag = false;

	// protected $children = [];
    //

	public function __construct(){
		parent::__construct();

		// $children = [];
        // foreach ($this->childNodes as $childNode){
            // $children[] = $childNode;
        // }
        // $this->children = $children;
	}

    /** is this node the given tag
    */
    public function is(string $tagName): bool{
        if (strtolower($this->tagName)==strtolower($tagName))return true;
        return false;
    }

    /**
     * @alias for DOMDocument::hasAttribute();
     */
    public function has(string $attribute): bool {
        return $this->hasAttribute($attribute);
    }

    /** get an array of DOMAttrs on this node
     */
	public function attributes(){
		$attrs = $this->attributes;
		$list = [];
		foreach ($attrs as $attr){
			$list[] = $attr;
		}
		return $list;
	}

    /** return an array of attributes ['attributeName'=>'value', ...];
     */
    public function attributesAsArray(){
        $list = [];
        foreach ($this->attributes as $attr){
            $list[$attr->name] = $attr->value;
        }
        return $list;
    }

	/**
	* Used for setting innerHTML like it's done in JavaScript:
	* @code
	* $div->innerHTML = '<h2>Chapter 2</h2><p>The story begins...</p>';
	* @endcode
	*/
	public function __set($name, $value) {
		if (strtolower($name) == 'innerhtml') {
			// first, empty the element
			for ($x=$this->childNodes->length-1; $x>=0; $x--) {
				$this->removeChild($this->childNodes->item($x));
			}
			// $value holds our new inner HTML
			if ($value != '') {
				$f = $this->ownerDocument->createDocumentFragment();
				// appendXML() expects well-formed markup (XHTML)
				$result = @$f->appendXML($value); // @ to suppress PHP warnings
				if ($result) {
					if ($f->hasChildNodes()) $this->appendChild($f);
				} else {
					// $value is probably ill-formed
					$f = new \DOMDocument();
					$value = mb_convert_encoding($value, 'HTML-ENTITIES', 'UTF-8');
					// Using <htmlfragment> will generate a warning, but so will bad HTML
					// (and by this point, bad HTML is what we've got).
					// We use it (and suppress the warning) because an HTML fragment will 
					// be wrapped around <html><body> tags which we don't really want to keep.
					// Note: despite the warning, if loadHTML succeeds it will return true.
					$result = @$f->loadHTML('<htmlfragment>'.$value.'</htmlfragment>');
					if ($result) {
						$import = $f->getElementsByTagName('htmlfragment')->item(0);
						foreach ($import->childNodes as $child) {
							$importedNode = $this->ownerDocument->importNode($child, true);
							$this->appendChild($importedNode);
						}
					} else {
						// oh well, we tried, we really did. :(
						// this element is now empty
					}
				}
			}
		} else {
			$this->setAttribute($name,$value);
			return;
			$trace = debug_backtrace();
			trigger_error('Undefined property via __set(): '.$name.' in '.$trace[0]['file'].' on line '.$trace[0]['line'], E_USER_NOTICE);
		}
	}

    /**
     * if the node has the named attribute, it will be removed. Otherwise, nothing happens
     */
  public function __unset($name){
        if ($this->hasAttribute($name))$this->removeAttribute($name);
  }
    public function __isset($name){
        return $this->hasAttribute($name);
    }
	/**
	* Used for getting innerHTML like it's done in JavaScript:
	* @code
	* $string = $div->innerHTML;
	* @endcode
	*/	
	public function __get($name)
	{
        if (method_exists($this, $getter = 'get'.strtoupper($name))){
            return $this->$getter();
        } else if ($name=='doc'){
            return $this->ownerDocument;
        } else if (strtolower($name) == 'innerhtml') {
                $inner = '';
                foreach ($this->childNodes as $child) {
                    $inner .= $this->ownerDocument->saveXML($child);
                }
                return $inner;
            } else if ($name=='form'&&strtolower($this->tagName)=='input'){
            $parent = $this->parentNode ?? null;
            while ($parent!=null&&strtolower($parent->tagName)!='form')$parent = $parent->parentNode ?? null;
            return $parent;
        } else if ($name=='inputs' && strtolower($this->tagName)=='form'){
            $inputList = $this->doc->xpath('//input', $this);
            // var_dump($inputList);
            // exit;
            return $inputList;
        } else if ($name=='children'){
            $children = [];
            for ($i=0;$i<$this->childNodes->count();$i++){
                $children[] = $this->childNodes->item($i);
            }
            return $children;
        }
        else if ($this->hasAttribute($name)){
            return $this->getAttribute($name);
        } else {
            return null;
        }
		// $trace = debug_backtrace();
		// trigger_error('Undefined property via __get(): '.$name.' in '.$trace[0]['file'].' on line '.$trace[0]['line'], E_USER_NOTICE);
		// return null;
	}

    public function __toString()
    {
        // echo $this->ownerDocument->saveHTML($this);
        if ($this->has('hideowntag')&&!$this->has('hideOwnTag')){
            $this->hideOwnTag = $this->hideowntag;
            unset($this->hideowntag);
        }
        if (!$this->has('hideOwnTag')||$this->hideOwnTag==false||$this->hideOwnTag=='false'){
            unset($this->hideOwnTag);
            $html = $this->ownerDocument->saveHTML($this);
            $html = $this->ownerDocument->fill_php($html);
            return $html;
        } else {
            $html = '';
            foreach ($this->childNodes as $c){
                $html .= $c;
            }
        }

        $html = $this->ownerDocument->fill_php($html);
        return $html;
    }

    public function xpath($xpath){
        return $this->doc->xpath($xpath, $this);
    }

	/**
	 * Adds a hidden input to a form node
	 * If a hidden input already exists with that name, do nothing
	 * If a hidden input does not exist with that name, create and append it
	 *
	 * @param  mixed $key
	 * @param  mixed $value
	 * @throws \BadMethodCallException if this method is called on a non-form node
	 * @return void
	 */
	public function addHiddenInput($inputName, $value){
		if (strtolower($this->tagName)!='form')throw new \BadMethodCallException("addHiddenInput can only be called on a Form node");
        $xPath = new \DOMXpath($this->ownerDocument);
        $inputs = $xPath->query('//input[@name="'.$inputName.'"][@type="hidden"]');
        if (count($inputs)>0)return;
		$input = $this->ownerDocument->createElement('input');
		$input->setAttribute('name',$inputName);
		$input->setAttribute('value',$value);
		$input->setAttribute('type','hidden');
		$this->appendChild($input);
	}
	
	/**
	 * Find out if this node has a true value for the given attribute name.
	 * Literally just returns $this->hasAttribute($attributeName)
	 * 
	 * I wanted to implement an attribute="false" option... but that goes against the standards of HTML5, so that idea is on hold.
	 * 
	 * See https://stackoverflow.com/questions/4139786/what-does-it-mean-in-html-5-when-an-attribute-is-a-boolean-attribute 
	 *
	 * @param  mixed $attributeName The name of the attribute we're checking for.
	 * @return bool
	 */
	public function boolAttribute($attributeName){
		return $this->hasAttribute($attributeName);
	}

    public function getInnerText(){
        return $this->textContent;
    }

    public function __call($method, $args){
        if (substr($method,0,2)=='is'){
            $prop = lcfirst(substr($method,2));
            if ($this->has($prop)&&$this->$prop != 'false')return true;
            return false;
        }

        throw new \BadMethodCallException("Method '$method' does not exist on ".get_class($this));
    }
}