DOMParser.php

<?php

namespace Phad;

class DomParser {


    /**
     * Get all property nodes
     * @return array of nodes
     */
    protected function get_prop_nodes($item){
        if ($item->tagName=='form')return $item->ownerDocument->xpath('descendant::*[@name]',$item);
        else return $item->ownerDocument->xpath('descendant::*[@prop]', $item);
    }

    /**
     * Get array of apis for this view
     * @return array of string api names
     */
    public function get_item_apis($type, $node){
        $apis = [];
        if ($type=='form'){
            $apis[] = 'form';
            $apis[] = 'create';
            $apis[] = 'update';
            if ($node->isDeletable())$apis[] = 'delete';
        } else {
            $apis[] = 'view';
            if (!isset($node->json)||$node->isJson()){
                $apis[] = 'data';
            }
        }

        return $apis;
    }

    /**
     * parse `<p-data>` nodes
     * @param $item_node the item this data node is in
     * @param &$on_nodes will contain all `<on>`
     * @param &$delete_nodes will contain all the data nodes, to be deleted from the DOM at a later step (after parsing on_nodes)
     *
     * @return array of info infos for each data node, where info info is an array as well
     */
    public function parse_data_nodes(\DOMNode $item_node, ?array &$on_nodes, ?array &$delete_nodes){
        $data_node_info = [
            // ['type'=>'default']
        ];
        $data_nodes = [];
        /* index_of_data_node => [array of on nodes] */
        $on_nodes = [];
        $index = 0;
        $has_default = false;
        foreach ($item_node->children as $data_node){
            if (!$data_node->is('p-data'))continue;
            $info = $data_node->attributesAsArray();
            $info['type'] = $info['type'] ?? 'node';
            if ($info['type']=='default')$has_default = true;
            $data_node_info[] = $info;

            foreach ($data_node->children as $on_node){
                if (!$on_node->is('on'))continue;
                $on_nodes[$index][] = $on_node;
            }

            $index++;
            $delete_nodes[] = $data_node;
        }

        if (!$has_default){
            $data_node_info[] = [
                'type'=>'default',
            ];
        }
        return $data_node_info;
    }

    
    /**
     * parse `<on>` nodes
     * @param $on_nodes array of nodes
     * @param $doc the document these nodes come from
     * 
     * @return array of template fillers
     */
    public function parse_on_nodes(array $on_nodes, \Taeluf\PHTML $doc){
        $template_fillers = [];
        // this is only used for <on s=200>
        $on_node_template_200 = $this->template('on_node_200');
        // this one is used by all but the s=200
        $on_node_template_other = $this->template('on_node_other');
        $compiler = new \Phad\TemplateCompiler();
        $code = 
            ['ItemInfo'=>'@$$ItemInfo','data_index'=>'@$$data_index',
            'on_code'=>null];
        $params_200 = $compiler->get_template_args($on_node_template_200, $code);
        $params_other = $compiler->get_template_args($on_node_template_other, $code);


        foreach ($on_nodes as $data_node_index=>$on_node_list){
            $code['data_index'] = $data_node_index;
            $str = '';
            foreach ($on_node_list as $on_node){
                if (((int)$on_node->s)==200){
                    $params = $params_200;
                    $template = $on_node_template_200;
                }
                else{
                    $params = $params_other;
                    $template = $on_node_template_other;
                }
                $code['on_code'] = $on_node->innerHtml;
                $compiled = $compiler->fill_template($template,$params, $code);

                $compiled = $doc->fill_php($compiled);

                $template_fillers['code_for_response_'.$on_node->s] = 
                    ($template_fillers['code_for_response_'.$on_node->s] ?? '')
                   .$compiled."\n";
                $template_fillers['response_code_'.$on_node->s] = true;
                $template_fillers['status_codes'] = true;
                $str .= $compiled;
            }
        }
        return $template_fillers;
    }

    /** Parse all prop nodes in an item
     * @param $item_node 
     * @param $item_name 
     */
    public function parse_prop_nodes(\Taeluf\PHTML\Node $item_node, string $item_name){
        unset($item_node->item);
        $prop_nodes = $this->get_prop_nodes($item_node);
        foreach ($prop_nodes as $pn){
            $this->parse_prop_node($item_node, $pn, $item_name);
        }
    }

    /**
     * modifies the `<form>` node, sets up `<onsubmit>` and enables `submit` code
     * @return array of paramaters that will be used for filling in the template in the compiler
     */
    public function parse_form_node($form_node, $item_name){
        
        $template_fillers = [
            'form_properties'=>true,
            'form_submit'=>true,
        ];

        /** @var $itemDataProperties contains information about the form's inputs, like maxlen, tagname, type, etc */
        $itemDataProperties = [];
        $propNodes = $this->get_prop_nodes($form_node);
        //array_map(function($v){echo $v."\n";}, $propNodes);
        //var_dump($propNodes);
        //exit;
        foreach ($propNodes as $pn){
            if ($pn->tagName=='input'&&$pn->type=='file'){
                $form_node->enctype = "multipart/form-data";
            }
            $propsData = $pn->attributesAsArray();
            unset($propsData['prop']);
            unset($propsData['name']);
            $propsData['tagName'] = $pn->tagName;
            if ($pn->is('select')){
                foreach($pn->xpath('option') as $optNode){
                    if (!$optNode->hasAttribute('value'))continue;
                    $propsData['options'][] = $optNode->value;
                }
            } 
            else if ($pn->tagName=='input' && $pn->type=='radio'){
                $index = $pn->prop ?? $pn->name;
                if (!isset($itemDataProperties[$index])){
                    $itemDataProperties[$index] = $propsData;
                }
                if (!isset($itemDataProperties[$index]['options'])){
                    $itemDataProperties[$index]['options'] = [];
                }
                $itemDataProperties[$index]['options'][] = $pn->value;
                continue;
            }

            $itemDataProperties[$pn->prop ?? $pn->name] = $propsData;
        }

        $form_node->action = $form_node->action ?? '';
        $form_node->method = $form_node->method ?? 'POST';

        if (isset($form_node->target)){
            $target = var_export($form_node->target,true);
            $target_template = "'target' => $target,";
            $template_fillers['FormTarget'] = $target_template;
            unset($form_node->target);
        }

        /** auto-add id property to forms .. except i think i still need to add it to the html */
        if (!isset($itemDataProperties['id'])){
            $itemDataProperties['id'] = ['tagName'=>'input', 'type'=>'hidden'];
        }
        $template_fillers['form_properties_array'] = var_export($itemDataProperties, true);
        // $itemDataPropertiesExported = var_export($itemDataProperties, true);
        // $code[] = $this->indent("{$itemDataVar}->properties = $itemDataPropertiesExported;",4);


        // add candelete to item info
        $candelete = false;
        $template_fillers['form_is_candelete']=true;
        if (isset($form_node->candelete)){
            $candelete = $form_node->candelete;
            if ($candelete=='')$candelete = true;
            else if ($candelete=='false')$candelete = false;
            // if ($candelete!==false){
            //     $template_fillers['form_is_candelete']=true;
            // }
            unset($form_node->candelete);
        } 
        $template_fillers['candelete'] = "'candelete' => ". var_export($candelete, true).',';

        // add diddelete to item info
        $diddelete = false;
        if (isset($form_node->diddelete)){
            $diddelete = $form_node->diddelete;
            if ($diddelete=='')$diddelete = true;
            else if ($diddelete=='false')$diddelete = false;
            if ($diddelete!==false){
                $template_fillers['form_is_diddelete']=true;
            }
            unset($form_node->diddelete);
        } 
        $template_fillers['diddelete'] = "'diddelete' => ". var_export($diddelete, true).',';


        // add cansubmit to item info
        $cansubmit = false;
        if (isset($form_node->cansubmit)){
            $cansubmit = $form_node->cansubmit;
            if ($cansubmit=='')$cansubmit = true;
            else if ($cansubmit=='false')$cansubmit = false;
            if ($cansubmit!==false){
                $template_fillers['form_is_cansubmit']=true;
            }
            unset($form_node->cansubmit);
        } 
        $template_fillers['cansubmit'] = "'cansubmit' => ". var_export($cansubmit, true).',';


        // $onSubmitNode = $form_node->xpath('onsubmit')[0] ?? null;
        // if ($onSubmitNode!==null){
        //     $onSubmitCode = $onSubmitNode->innerHTML;
        //     $placeholder = $onSubmitNode->innerHTML;
        //     $placeheldCode= $onSubmitNode->doc->codeFromPlaceholder($placeholder);
        //     if ($placeheldCode!==null){
        //         $onSubmitCode = $placeheldCode;
        //     }
        //     $phpClose = '?\>'; // stops my editor from being upset
        //     $phpOpen = '<?php'; //just mildly convenient
        //     $onSubmitForm = $phpClose.trim($onSubmitCode).$phpOpen;
        //     $onSubmitForm = trim($this->indent($onSubmitForm, 8));
        //     $template_fillers['form_on_submit'] = $onSubmitForm;
        //     $onSubmitNode->parentNode->removeChild($onSubmitNode);
        // }


        $this->node_to_php($form_node, 'onsubmit', 'form_on_submit', $template_fillers);
        $this->node_to_php($form_node, 'didsubmit', 'form_did_submit', $template_fillers);
        $this->node_to_php($form_node, 'failsubmit', 'form_fail_submit',$template_fillers);
        $this->node_to_php($form_node, 'diddelete', 'form_did_delete', $template_fillers);
        $this->node_to_php($form_node, 'willdelete', 'form_will_delete', $template_fillers);

        // if ($form_node->tagName!='form')$submit_code = '';

        return $template_fillers;
    }

    /**
     * Take a node like `<onsubmit><?php echo "cats";?></onsubmit>` and make it so it just shows the inner html
     * @param $parent_node
     * @param $node_name the name of the node within the parent
     * @param $template_param_name the key for the code in the item template
     * @param &$template_fillers array of `key=>value` pairs to insert into the item template
     */
    public function node_to_php($parent_node, $node_name, $template_param_name, &$template_fillers){
        $didSubmitNode = $parent_node->xpath($node_name)[0] ?? null;
        if ($didSubmitNode!==null){
            $didSubmitCode = $didSubmitNode->innerHTML;
            $placeholder = $didSubmitNode->innerHTML;
            $placeheldCode= $didSubmitNode->doc->codeFromPlaceholder($placeholder);
            if ($placeheldCode!==null){
                $didSubmitCode = $placeheldCode;
            }
            $phpClose = '?>'; // stops my editor from being upset
            $phpOpen = '<?php'; //just mildly convenient
            $didSubmitForm = $phpClose.trim($didSubmitCode).$phpOpen;
            $didSubmitForm = trim($this->indent($didSubmitForm, 8));
            $template_fillers[$template_param_name] = $didSubmitForm;
            $didSubmitNode->parentNode->removeChild($didSubmitNode);
        }

    }


    /**
     * Parse and compile all nodes like `<div access="role:admin">`
     * @param $doc the full doc to find nodes in
     */
    public function parse_independent_access_nodes(\Taeluf\PHTML $doc){
        $accessNodeList = $doc->xpath('//*[@access]');
        foreach ($accessNodeList as $node){
            if ($node->is('p-data'))continue;
            $node_info = $node->attributesAsArray();
            $node_info['tagName'] = $node->tagName;
            $node_info = var_export($node_info,true);
            $node->doc->insertCodeBefore($node, "<?php if (\$phad->can_read_node($node_info)): ?>\n");
            $node->doc->insertCodeAfter($node, 
                    "\n<?php else: \$phad->read_node_failed($node_info); ?>"
                    ."\n<?php endif; ?>"
                );
            unset($node->access);
        }
    }

    /**
     * Parse & compile a doc
     *
     * @param $doc 
     * @return an array of template fillers
     *
     * @note(jan 25, 2022) the template fillers return is important for tests, but won't likely be useful in production, as `parse_doc()` now runs the `TemplateCompiler` and finishes inserts the compiled template output into the document
     */
    public function parse_doc(\Taeluf\PHTML $doc){

        $template_fillers_extra = [];
        $template_filers = [];
        $itemNodeList = $doc->xpath('//*[@item]');
        // echo $doc;
        // print_r($itemNodeList);
        // exit;

        $this->parse_independent_access_nodes($doc);
        $this->parse_sitemap_nodes($doc);
        $this->parse_route_nodes($doc);
        // foreach ($itemNodeList as $node){

        /**
         * Iterate over the itemnode list ... ensuring we process child-most item nodes FIRST
         * @var $iter is the # of times we've run the while loop & `$iter<50` just makes sure we never have an infinite loop
         *
         */
        $iter = 0;
        while ($iter++<50&&count($itemNodeList)>0){
        foreach ($itemNodeList as $index=>$node){

            // can_read_row as part of compiled view is commented out & is planned for later deletion. can_read_row() is now being called during read_data() from within the phad method, rather than from within the compiled view.
            // likely a different method will be added for html-defined can_read_row()
            if (isset($node->can_read_row)){
                unset($node->can_read_row);
                $template_fillers_extra['can_read_row'] = false;
            }
            // add error node
            $this->setup_error_node($node);
            $children = $doc->xpath('descendant::*[@item]', $node);
            // `continue` if there are children & then it gets processed in a later `while()` itertion
            if (count($children) > 0)continue;
            unset($itemNodeList[$index]);


            // create an `<x-item>` node to facilite `loop="inner"` feature on item nodes & move the item declaration to the inner `x-item` node
            if ($node->loop=='inner'){
                $xitem = $doc->createElement('x-item');

                foreach ($node->children as $c){
                    $node->removeChild($c);
                    $xitem->appendChild($c);
                }
                $xitem->item = $node->item;
                $xitem->loop = $node->loop;
                if ($node->has('table'))$xitem->table = $node->table;
                unset($node->item);
                unset($node->loop);
                unset($node->table);

                // for formatting purposes
                $node->appendChild(new \Taeluf\PHTML\TextNode("\n"));

                $node->appendChild($xitem);
                $node = $xitem;
                // continue;
            }

            // hide `<x-item>` tags
            if ($node->tagName=='x-item'){
                $node->hideOwnTag = true;
            }
            if ($node->tagName=='x-prop'){
                // echo 'should hide here';
                $node->hideOwnTag = true;
            }

            // scan nodes & put info together
            $type = $node->is('form') ? 'form' : 'view';
            $apis = $this->get_item_apis($type, $node);

            $name = $node->item ?? ucfirst($node->table);
            $delete_nodes = [];

            if ($type=='form'){
                $form_node_params = $this->parse_form_node($node, $name);
                $template_fillers_extra = array_merge($template_fillers_extra,$form_node_params);
            }


            $this->parse_prop_nodes($node, $name);

            $data_node_info = $this->parse_data_nodes($node, $on_nodes, $delete_nodes);

            // get <on> nodes that are NOT descendants of <p-data> nodes (they are direct descendants of the item node)
            $data_index = count($on_nodes);
            foreach ($node->children as $item_child_node){
                if ($item_child_node->is('on')){
                    $on_nodes[$data_index][] = $item_child_node;
                    $delete_nodes[] = $item_child_node;
                }
            }

            $on_node_compiler_params = $this->parse_on_nodes($on_nodes, $doc);
            $template_fillers_extra = array_merge($template_fillers_extra, $on_node_compiler_params);




            // cleanup
            foreach ($delete_nodes as $n){
                $n->parentNode->removeChild($n);
            }


            //output code from PHTML Doc
            $html_code = $doc->fill_php($node.'');
            $html_code = $doc->restoreHtml($html_code);


            // build item to put into array of items
            $item = [
                'item_name' => $name,
                'item_type' => $type,
                'apis' => var_export($apis, true),
                'DataNodes' => var_export($data_node_info, true), 
                'ItemForeach'=>"$name => \${$name}Row",
                'ItemName'=> '$'.$name,
                'Item_Row'=> '$'.$name.'Row',
                'ItemInfo'=> '$'.$name.'Info',
                'ItemInfoSubmitErrorsList'=> $name.'SubmitErrorsList',

                'html_code' => $html_code,
            ];
            $item = array_merge($item, $template_fillers_extra);


            // apply the item template to the node & update the doc
            $compiler = new \Phad\TemplateCompiler();
            $template = file_get_contents(__DIR__.'/../template/main.php');
            $compiler->precompile_ifs($template, $item);
            $template_params = $compiler->get_template_args($template, $item);
            $compiled = $compiler->fill_template($template, $template_params, $item);

            $node->doc->replaceNode($node, $compiled);

            $template_filers[] = $item;
            // echo "\n\n\n\n\nDID IT\n\n\n\n\n\n";
        }
        }
        // echo $doc;

        // exit;

        return $template_filers;
    }


    /**
     * Parse and compile a prop node
     *
     * @param $item_node
     * @param $prop
     * @param $itemName
     */
    public function parse_prop_node(\Taeluf\PHTML\Node $item_node, \Taeluf\PHTML\Node $prop_node, string $itemName){
        $phtml = $item_node->ownerDocument;
        $p = $prop_node;
        $propName = $p->hasAttribute('prop') ? $p->prop : $p->name;
        if (!$p->hasAttribute('value')){
            $propCode = '$'.$itemName.'->'.$propName;
        } else {
            $propCode = '$'.$itemName.'->'.$propName.'??'.var_export($p->value,true);
        }
        $propCodeValueOnly = $propCode = '$'.$itemName.'->'.$propName;

        $phpCode = null;
        if ($p->hasAttribute('filter')){
            $filterCode = var_export($p->filter,true);
            $phpCode = '<?=$phad->filter('.$filterCode.','.$propCode.')?>';
        } else {
            $phpCode = '<?='.$propCode.'?>';
        }

        if ($p->tagName == 'input' && $p->type == 'backend'){
            $p->parentNode->removeChild($p);
        } else if ($p->tagName == 'input' && $p->type == 'radio'){
            $form_value = $p->value;
            $form_value_code = var_export($form_value, true);
            $checked_code = "<?=($propCodeValueOnly==$form_value_code)?'checked':'';?>"; 
            $phtml->addPhpToTag($p,$checked_code);
            // $p->value
        } else if ($p->tagName=='input'&& ( $p->type=='checkbox' )){
            $checked_code = "<?=($propCode)?'checked':'';?>";
            $phtml->addPhpToTag($p,$checked_code);
            $p->value = '1';
        } elseif ($p->tagName=='input' && $p->type != 'file'){
            $p->value = $phtml->phpPlaceholder($phpCode);
        } else if ($p->tagName=='select'){
            $options = $phtml->xpath('descendant::option', $p);
            foreach ($options as $opt){
                $optVal = var_export($opt->value,true);
                $code = "<?=($optVal==$propCode)? ' selected=\"\" ' : ' '?>";
                $phtml->addPhpToTag($opt, $code);
            }
        } else {
            $p->innerHTML=$phtml->phpPlaceholder($phpCode);
        }

        unset($p->filter);
        unset($p->prop);

        // if ($p->tagName=='x-prop'){
            // echo 'should hide here';
            // $p->hideOwnTag = true;
        // }
    }

    public function parse_route_nodes($doc){
        $template = $this->template('route_info');

        $routeNodeList = $doc->xpath('//route');
        $routes = [];
        $prev_node = null;
        $delete_prev = false;
        // the strange logic here is so i can put the return routes portion before the first `<route>` node  ... i don't like this code, but it works
        foreach ($routeNodeList as $rn){
            $route = [];
            foreach ($rn->attributes() as $attr){
                $route[$attr->name] = $attr->value;
            }
            $routes[] = $route;
            $prev_node = $rn->previousSibling;
            if ($prev_node==null){
                $prev_node = $rn;
            } 
        }
        // if (count($routes)==0)return;

        $code = trim(str_replace('@$$Routes', var_export($routes,true), $template));


        if ($prev_node == null){
            $doc->insertCodeBefore($doc->childNodes[0]->childNodes[0], $code);
            return;
        }

        $doc->insertCodeAfter($prev_node, $code);

        // $remaining_route_nodes = $doc->xpath('//route');
        foreach ($routeNodeList as $n){
            $n->parentNode->removeChild($n);
        }
    }

    public function parse_sitemap_nodes($doc){
        //build sitemap array from sitemap nodes
        $sitemapNodeList = $doc->xpath('//sitemap');
        $sitemaps = [];
        $prev_node = null;

        foreach ($sitemapNodeList as $sn){
            $pattern = $sn->parentNode->pattern;
            // $sitemap = ['pattern'=>$pattern];
            $sitemap = [];
            foreach ($sn->attributes() as $attr){
                $sitemap[$attr->name] = $attr->value;
            }
            $sitemap['pattern']=$pattern;
            $sitemaps[$pattern] = $sitemap;
            $prev_node = $sn->parentNode->previousSibling;
            if ($prev_node==null)$prev_node = $sn->parentNode;
        }
        // var_export sitemaps array into the compiled output & conditionally return the sitemap data
        // if (count($sitemaps)==0){
            // $sitemaps = [];
        // }

        $Sitemaps_code = var_export($sitemaps, true);
        $code = $this->template('sitemap_info');
        $code = str_replace('@$$Sitemaps_code', $Sitemaps_code, $code);
        $code = trim($code);

        if ($prev_node == null){
            $doc->insertCodeBefore($doc->childNodes[0]->childNodes[0], $code);
            return;
        }

        $doc->insertCodeAfter($prev_node, $code);

        foreach ($sitemapNodeList as $sn){
            $sn->parentNode->removeChild($sn);
        }

    }

    public function indent(string $str, int $numSpaces, $padFirstLine = true){
        $str = explode("\n", $str);
        $pad = str_pad('',$numSpaces);
        $str = implode("\n$pad",$str);
        if ($padFirstLine)$str = $pad.$str;
        return $str;
    }

    public function setup_error_node($item_node){
        // return;
        $error_nodes = $item_node->xpath('//errors');
        if (count($error_nodes)==0)return;
        $node = $error_nodes[0];

        $item_name = $item_node->item;
        $code = $this->template('form_errors');
        $code = str_replace('@$$ItemInfo', "\${$item_name}Info", $code);
        $debug_code = <<<HTML
            <div class="errors">
            <?php
             unset(\${$item_name}Info->args['phad']);
             print_r((array)\${$item_name}Info);
            ?>
            $code;
        HTML;

        $node->doc->insertCodeBefore($node, $code);
        $node->parentNode->removeChild($node);
    }

    public function replace_node_with_code($node, $code){
        $placeholder = $didSubmitNode->doc->codeFromPlaceholder($code);

        // if ($placeholder==null)
        if ($didSubmitNode!==null){
            $didSubmitCode = $didSubmitNode->innerHTML;
            $placeholder = $didSubmitNode->innerHTML;
            $placeheldCode= $didSubmitNode->doc->codeFromPlaceholder($placeholder);
            if ($placeheldCode!==null){
                $didSubmitCode = $placeheldCode;
            }
            $phpClose = '?>'; // stops my editor from being upset
            $phpOpen = '<?php'; //just mildly convenient
            $didSubmitForm = $phpClose.trim($didSubmitCode).$phpOpen;
            $didSubmitForm = trim($this->indent($didSubmitForm, 8));
            $template_fillers[$template_param_name] = $didSubmitForm;
            $didSubmitNode->parentNode->removeChild($didSubmitNode);
        }
    }

    public function template(string $name){
        $file = dirname(__DIR__).'/template/'.$name.'.php';
        return file_get_contents($file);
    }
}