AstVerb.php
<?php
namespace Tlf\Scrawl\Ext\MdVerb;
class Ast extends \Tlf\Scrawl\DoNothingExtension {
public \Tlf\Scrawl $scrawl;
public array $status = [
'keys_found'=>[],
'keys_missed'=>[],
'filters_found'=>[],
'filters_missed'=>[],
'templates_found'=>[],
'templates_missed'=>[],
];
public function __construct($scrawl){
$this->scrawl = $scrawl;
}
/**
* Print information about a parsed PHP class. Use template `ast/debug` to get information about the AST path. (*Note: The templates are not very good, but you can create your own*)
*
* @usage `@ast(ast_path, template_name, filters)`, like `@ast(class[ClassName].methods[MethodName].docblock[Description], ast/default, filter1.filter2.filter3)`. See available [filters](/docs/api/src/Utility/Filters.php.md) & [templates](/docs/ASTTemplates.md)
* @output the template's string output or the value pointed to by the `ast_path` if a template is not used and the value is a string.
*
* @note `@ast()` can accept a dot-path like `class.ClassName.methods.MethodName`, but the array-access version is recommended.
*/
public function get_markdown(string $key, string $template='ast/default', ?string $filters=null): string {
$this->scrawl->info('@ast()', $key);
$value = $this->get_ast($key);
$template_out = $this->scrawl->get_template($template, [$key, $value, $this], $did_find_template);
if ($did_find_template===true){
$this->status['templates_found'][$template] = $template;
} else {
$this->status['templates_missed'][$template] = $template;
}
if ($filters!=null){
$template_out = $this->apply_filters($template_out, $filters);
}
return $template_out;
}
/**
*
*/
protected function apply_filters(string $input, string $filters_list): string {
$filters = explode(".", $filters_list);
$out = $input;
foreach ($filters as $f){
if (method_exists(\Tlf\Scrawl\Utility\Filters::class, $f)){
$this->status['filters_found'][$f] = $f;
$out = \Tlf\Scrawl\Utility\Filters::$f($out);
} else {
$this->status['filters_missed'][$f] = $f;
$this->scrawl->warn("Filter Not Found", "'$f' is not a valid filter.");
}
}
return $out;
}
/**
* @param $key a dotproperty like `class.ClassName.methods.MethodName.docblock.description`
* @param $length the number of dots in the dotproperty to traverse. -1 means all
*/
public function get_ast(string $key, int $length=-1){
$dot_parts = explode('.',$key);
$parts = [];
foreach($dot_parts as $p){
if (substr($p,-1)==']'){
$pos = strpos($p, '[');
if ($pos===false)$parts[] = $p;
else {
$parts[] = substr($p,0,$pos);
$parts[] = substr($p, $pos+1, -1);
}
} else {
$parts[] = $p;
}
}
if ($length!=-1){
$parts = array_slice($parts, 0,$length);
}
$group = array_shift($parts);
if ($group == 'file'){
$file = implode(".", $parts);
$ast = $this->scrawl->get_ast($file);
if (is_array($ast)){
$ast['relPath'] = $file;
} else {
$this->status['keys_missed'][$key] = $key;
$this->scrawl->warn("File '$file' not found", "Can't load file ast");
return null;
}
$this->status['keys_found'][$key] = $key;
return $ast;
} else {
$class = array_shift($parts);
$ast = $this->scrawl->get('ast', $group.'.'.$class);
if ($ast==null){
$this->status['keys_missed'][$key] = $key;
$this->scrawl->warn("Ast $group not found", "Can't load '$class'.");
return;
}
// echo "\n\nAST:";
// var_dump($ast);
// var_dump($group);
// var_dump($class);
// echo "\n\n";
$stack = $group.'.'.$class;
$next = $ast;
foreach ($parts as $p){
$current = $next;
$stack .= '.'.$p;
// echo "\n\nStack: $stack";
if (!isset($current[$p])&&is_array($current)){
foreach ($current as $i=>$item){
if (!is_numeric($i))continue;
//echo "\n\n".$p."::";
//echo $item['name']."\n\n";
if ($item['name']==$p){
$next = $item;
continue 2;
}
}
$this->scrawl->warn("Ast Not Found", "Can't load '$key'. Failed at '$current.$p'");
$this->status['keys_missed'][$key] = $key;
return null;
} else if (!isset($current[$p])){
$this->scrawl->warn("Ast Not Found", "Can't load '$key'. Failed at '$p'");
$this->status['keys_missed'][$key] = $key;
return null;
}
$next = $current[$p];
}
$this->status['keys_found'][$key] = $key;
return $next;
}
$this->status['keys_missed'][$key] = $key;
}
public function getVerbs(): array{
return [
'ast'=>'verbAst', //alias for @ast_class()
'classMethods'=>'getClassMethodsTemplate',
'ast_class'=> 'getAstClassInfo',
];
}
/**
*
* @param $fqn The fully qualified name, like a class name or function with its namespace
* @param $dotProperty For class, something like 'methods.methodName.docblock' to get a docblock for the given class.
*
* @example @ast(\Tlf\Scrawl\Ext\MdVerb\Ast, methods.getAstClassInfo.docblock)
* @mdverb ast
*
*/
public function getAstClassInfo(array $info, string $fqn, string $dotProperty){
// @ast(class,Phad\Test\Documentation,methods.testWriteAView.docblock)
$class = $this->scrawl->getOutput('astClass', $fqn);
// var_dump(array_keys($this->scrawl->getOutputs('astClass')));
if ($class == 'null') return "class '$fqn' not found in ast.";
$propStack = explode('.', $dotProperty);
$head = $class;
if (!is_array($head)){
$file = $info['file']->path;
// $this->scrawl->error('@ast or @ast_class in '.$file,'requires "astClass" output for fqn "'.$fqn.'" to be an array, but a '. gettype($class).' was returned.');
$this->scrawl->error("@ast($fqn, $dotProperty) failed", 'in '.$file);
return "@ast($fqn) failed";
}
foreach ($propStack as $prop){
if ($prop=='*'){
return print_r($head,true);
}
if (!isset($head[$prop])){
$options = [];
foreach ($head as $key=>$value){
if (is_numeric($key) && ($value['name']??null)==$prop){
$head = $head[$key];
continue 2;
} else if (is_numeric($key) && isset($value['name'])){
$options[] = $value['name'];
}
}
$options = array_merge($options, array_keys($head));
$msg = "Cannot find '$prop' part of '$dotProperty' on '$fqn'. You may try one of: ". implode(", ", $options);
$this->scrawl->error('@ast or @ast_class', $msg);
return $msg;
}
$head = $head[$prop];
}
if (is_array($head)){
if (isset($head['body']))return $head['body'];
else if (isset($head['description']))return $head['description'];
else if (isset($head['src']))return $head['src'];
$msg="Found an array for '$dotProperty' on '$fqn' with keys: ".implode(", ", array_keys($head));
$this->scrawl->error('@ast or @ast_class', $msg);
return $msg;
}
return $head;
}
/**
*
* @return string replacement
*/
public function verbAst($info, $className, $dotProperty){
//
// This method not currently functional. Ugggh!!!
//
// $parts = explode('.', $classDotThing);
// $class = array_shift($parts);
// return $this->getAstClassInfo($info, $class, 'method.'.implode('.',$parts));
return $this->getAstClassInfo($info, $className, $dotProperty);
// $key = $class;
// var_dump($key);
// $output = $this->scrawl->getOutput('api',$key);
//
// if (trim($output)=='')return "--ast '${class}' not found--";
//
// return $output;
}
public function getClassMethodsTemplate($verb, $argListStr, $line){
if ($verb!='classMethods')return;
$args = explode(',', $argListStr);
$className = $args[0];
$visibility = '*';
if (isset($args[1])){
$visibility = trim($args[1]);
}
$class = $this->scrawl->getOutput('astClass', $className);
$template = dirname(__DIR__,2).'/Template/classMethods.md.php';
ob_start();
require($template);
$final = ob_get_clean();
return $final;
}
public function scrawl_finished(){
$this->scrawl->header("@ast() summary");
$this->scrawl->good("ASTs found", count($this->status['keys_found']));
if (count($this->status['keys_missed'])==0){
$this->scrawl->info("All ASTs found");
} else {
$this->scrawl->warn("ASTs missed", implode(", ", $this->status['keys_missed']));
}
$this->scrawl->good("Templates found", count($this->status['templates_found']));
if (count($this->status['templates_missed'])==0){
$this->scrawl->info("All templates found");
} else {
$this->scrawl->warn("Templates missed", implode(", ", $this->status['templates_missed']));
}
$this->scrawl->good("Filters found", count($this->status['filters_found']));
if (count($this->status['filters_missed'])==0){
$this->scrawl->info("All filters found");
} else {
$this->scrawl->warn("Filters missed", implode(", ", $this->status['filters_missed']));
}
}
}