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);
}
}