PHTML.php

<?php

namespace Taeluf;

/**
 * Makes DUMDocument... less terrible, but still not truly good
 * 
 */
class PHTML extends \DOMDocument {
    
    /**
     * The source HTML + PHP code
     *
     * @var string
     */
    protected string $src;    


    /** @alias for $src */
    public string $srcHTML;
    // /**
    //  * True if the source html had a '<html>' tag
    //  * Except we're not implementing that???
    //  * @var bool
    //  */
    // protected bool $isHTMLDoc = false;

    /**
     * The source code with all the PHP replaced by placeholders
     */
    protected string $cleanSrc;
        
    /**
     * [ 'phpplaceholder' => $phpCode, 'placeholder2' => $morePHP ]
     *
     */
    protected array $php;

    /**
     * A random string used when adding php code to a node's tag declaration. This string is later removed during output()
     */
    protected $phpAttrValue;
    
    /**
     * Create a DOMDocument, passing your HTML + PHP to `__construct`.
     * 
     *
     * @param mixed $html a block of HTML + PHP code. It does not have to have PHP. PHP will be handled gracefully.
     * @return void
     */
    public function __construct($html)
    {
        parent::__construct();

        $this->srcHTML = $html;
        $this->src = &$this->srcHTML;

        $parser = new PHTML\PHPParser($html);
        $enc = $parser->pieces();
        $this->php = $enc->php;
        $this->cleanSrc = $enc->html;
        $this->cleanSrc = $this->cleanHTML($this->cleanSrc);
        $hideXmlErrors=true;
        libxml_use_internal_errors($hideXmlErrors);
        $this->registerNodeClass('DOMElement', '\\Taeluf\\PHTML\\Node');
        $this->registerNodeClass('DOMText', '\\Taeluf\\PHTML\\TextNode');
        $this->registerNodeClass('DOMComment', '\\Taeluf\\PHTML\\DOMComment');
        // $this->registerNodeClass('DOMText', 'RBText');

        $html = '<root>'.$this->cleanSrc.'</root>';
        $this->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        $this->formatOutput = true;
        libxml_use_internal_errors(false);

    }    


    /**
     * get a string placeholder for a block of code. This placeholder, if inserted into the doc, will be replace by the original code on render
     * @param $string the string you want in your doc
     * @return a string like `phpRandomCharsphp`
     */
    public function placeholder(string $string): string{
        $placeholder = PHTML\PHPParser::getRandomAlpha();
        $placeholder = 'php'.$placeholder.'php';
        $this->php[$placeholder] = $string;
        return $placeholder;
    }

    /**
     * Get the code that's represented by the placeholder
     * @return the stored code or null
     */
    public function codeFromPlaceholder(string $placeholder): ?string {
        $code = $this->php[trim($placeholder)] ?? null;
        return $code;
    }

    /**
     * Get a placeholder for the given block of code
     * Intention is to parse a single '<?php //piece of php code ?>' and not '<?php //stuff ?><?php //more stuff?>'
     * When used as intended, will return a single 'word' that is the placeholder for the given code
     *
     * @param  mixed $enclosedPHP an HTML + PHP string
     * @return string the parsed block of content where PHP code blocks are replaced by placeholders.
     *
     * @deprecate(jan 25, 2022) ... placeholder() is a simpler method that gets the job done
     */
    public function phpPlaceholder(string $enclosedPHP): string{
        $parser = new PHTML\PHPParser($enclosedPHP);
        $enc = $parser->pieces();

        // This block doesn't work because it's over-eager. A workaround to just add code, regardless of open/close tags, would be good.
        // if (count($enc->php)==0){
            // $code = \PHTML\PHPParser::getRandomAlpha();
            // $enc->php = ["php${code}php"=>$enclosedPhp];
            // $enc->html = $code;
        // }
        $this->php = array_merge($this->php,$enc->php);
        return $enc->html;
    }

    /**
     * Decode the given code by replacing PHP placeholders with the PHP code itself
     *
     * @param  mixed $str
     * @return void
     */
    public function fillWithPHP(string $codeWithPlaceholders): string{
        $decoded = str_replace(array_keys($this->php),$this->php,$codeWithPlaceholders);
        return $decoded;
    }    
    /**
     * See output()
     *
     * @return string 
     */
    public function __toString()
    {
        return $this->output();
    }

    /**
     * Return the decoded document as as tring. All PHP will be back in its place
     *
     * @param  mixed $withPHP passing FALSE means placeholders will still be present & PHP code will not be
     * @return string the final document with PHP where it belongs
     */
    public function output($withPHP=true){
        // echo "\n".'-start output call-'."\n";
        $list = $this->childNodes[0]->childNodes;

        $hiddenTagsNodes = $this->xpath('//*[@hideowntag or @hideOwnTag]');
        foreach ($hiddenTagsNodes as $htn){
            if ($htn->has('hideowntag')&&!$htn->has('hideOwnTag')){
                $htn->hideOwnTag = $htn->hideowntag;
                unset($this->hideowntag);
            }
            if ($htn->hideOwnTag==false||$htn->hideOwnTag=='false'){
                unset($htn->hideOwnTag);
                return;
            }
            $this->hide_own_tag($htn);
        }
        $html = '';
        foreach ($list as $item){
            $html .= $this->saveHTML($item);
        }

        /** Run the php-code-replacer as long as there is a placeholder (while preventing infinite looping) */
        $html = $this->fill_php($html, $withPHP);
        
        $html = $this->restoreHtml($html);

        return $html;
    }

    /**
     *
     * @returns the parent node of the node being removed
     */
    public function hide_own_tag($htn){
        $parent = $htn->parentNode;
        $childNodeList = $htn->children;
        foreach ($childNodeList as $child){
            $htn->removeChild($child);
            $parent->insertBefore($child, $htn);
        }
        $parent->removeChild($htn);
        return $parent;
    }

    public function fill_php($html, $withPHP=true){
        $maxIters = 25;
        $iters = 0;
        while ($iters++<$maxIters&&preg_match('/php([a-zA-Z]{26})php/', $html, $match)){
            foreach ($this->php as $id=>$code){
                if ($withPHP)$html = str_replace($id,$code,$html);
                else $html = str_replace($id,'',$html);
            }
        }

        if (($phpAttrVal=$this->phpAttrValue)!=null){
            $html = str_replace("=\"$phpAttrVal\"", '', $html);
        }
        return $html;
    }
    
    /**
     * get the results of an xpath query as array
     *
     * @param  mixed $xpath the xpath query, such as: //tagname[@attributename="value"]
     *                  If you use a refnode, prepend '.' at the beginning of your xpath query string
     * @param  mixed $refNode a parent-node to search under
     * @return array the resulting DomNodeList is converted to an array & returned
     */
    public function xpath($xpath,$refNode=null){
        $xp = new \DOMXpath($this);
        if ($refNode==null)$list =  $xp->query($xpath);
        else $list = $xp->query($xpath,$refNode);
        $arr = [];
        foreach ($list as $item){
            $arr[] = $item;
        }
        return $arr;
    }

    /**
     * Set an attribute that will place PHP code inside the tag declartion of a node. 
     * Basically: `<node phpCodePlaceholder>`, which pHtml will later convert to `<node <?='some_stuff'?>>`. 
     * This avoids problems caused by attributes requiring a `=""`, which `DOMDocument` automatically places.
     *
     * @param $phpCode A block of php code with opening & closing tags like <?='some stuff'?>
     * @return \Taeluf\PHTML\ValuelessAttribute
     */
    public function addPhpToTag($node, $phpCode){
        $this->phpAttrValue = $this->phpAttrValue  ??  PHTML\PHPParser::getRandomAlpha();

        $placeholder = $this->phpPlaceholder($phpCode);
        $node->setAttribute($placeholder, $this->phpAttrValue);
        return $placeholder;
    }

    public function insertCodeBefore(\DOMNode $node, $phpCode){
        // $placeholder = $this->phpPlaceholder($phpCode);
        $placeholder = $this->placeholder($phpCode);
        $text = new \DOMText($placeholder);
        return $node->parentNode->insertBefore($text, $node);
    }

    public function insertCodeAfter(\DOMNode $node, $phpCode){
        $placeholder = $this->placeholder($phpCode);
        $text = new \DOMText($placeholder);
        if ($node->nextSibling!==null)return $node->parentNode->insertBefore($text,$node->nextSibling);

        return $node->parentNode->insertBefore($text);
    }

    public function cleanHTML($html){
        // fix doctype
        $html = preg_replace('/\<\!DOCTYPE(.*)\>/i', '<tlfphtml-doctype$1></tlfphtml-doctype>',$html);
        // fix <html> tag
        $html = preg_replace('/<html([ >])/','<tlfphtml-html$1',$html);
        $html = str_ireplace('</html>', '</tlfphtml-html>', $html);

        // fix <head> tag
        $html = preg_replace('/<head([ >])/', '<tlfphtml-head$1',$html);
        $html = str_ireplace('</head>', '</tlfphtml-head>',$html);
        return $html;
    }

    public function restoreHtml($html){
        $html = preg_replace('/\<tlfphtml\-doctype(.*)\>\<\/tlfphtml\-doctype\>/i', '<!DOCTYPE$1>',$html);
        // fix <html> tag
        $html = str_ireplace('<tlfphtml-html','<html',$html);
        $html = str_ireplace('</tlfphtml-html>', '</html>', $html);

        // fix <head> tag
        $html = str_ireplace('<tlfphtml-head', '<head', $html);
        $html = str_ireplace('</tlfphtml-head>', '</head>', $html);
        return $html; 
    }

    public function __get($param){
        if ($param == 'form'){
            return $this->xpath('//form')[0] ?? null;
        }
    }

    /**
     * Replace a node
     * @param $node a DomNode
     * @param $with_str a string to use in its place
     *
     * @note(jan 25, 2022) uses insertBefore() to create a new text node & uses the php_placeholder mechanic so all code is handled gracefully
     */
    public function replaceNode(\DOMNode $node, string $with_str){
        $this->insertCodeBefore($node,$with_str);
        $node->parentNode->removeChild($node);
    }
}