MainVerbs.php

<?php

namespace Tlf\Scrawl\Ext\MdVerb;

class MainVerbs implements \Tlf\Scrawl\Extension {

    /**
     * a scrawl instance
     */
    public \Tlf\Scrawl $scrawl;
    /** array<string absolute_file_path, string relPath> array of files that didn't exist when see_file() was called */
    protected array $see_file_failures = [];

    protected array $claims_passed = [];
    protected array $claims_failed = [];


    protected array $import_failures = [];

    public function __construct(\Tlf\Scrawl $scrawl){
        $this->scrawl = $scrawl;
    }

    /**
     * add callbacks to `$md_ext->handlers`
     */
    public function setup_handlers(\Tlf\Scrawl\Ext\MdVerbs $md_ext){
        $handlers = [
            'import'=>'at_import',
            'file' => 'at_file',
            'template'=> 'at_template',
            'link'=> 'at_link',
            'easy_link'=> 'at_easy_link',
            'hard_link'=> 'at_hard_link',
            'see_file'=> 'at_see_file',
            'see_files'=> 'at_see_files',
            'see'=> 'at_see_file',
            'system' => 'at_system',
            'claim' => 'at_claim',
        ];
        foreach ($handlers as $verb=>$func_name){
            $md_ext->handlers[$verb] = [$this, $func_name];
        }
    }

    // public function okay(){}

    /**
     * Run a system command: `@system(system_command, ...options)`. Options can be `trim`, `trim_empty_lines`, `last_x_lines, int`
     *
     * @usage `@system(bin/something)`, `@system(bin/something, trim)`, `@system(bin/something, last_x_lines, 5)`
     * @output the output of the `system_command`, modified by the provided options.
     *
     * @param $system_command string command to run on the system, like `git log`
     * @param $options array of options. 
     *
     * @return whatever is output by the system command
     */
    public function at_system(string $system_command, ...$options){
        //if (substr($system_command,0,3) == 'git'){
            //echo "\n$system_command\n";
            //exit;
        //}

        $this->scrawl->good("@system()", $system_command);
        
        ob_start();
        system("$system_command");
        $output = ob_get_clean();

        if (in_array('trim', $options)){
            $output = trim($output);
        }

        if (in_array('trim_empty_lines', $options)){
            $parts = explode("\n", $output);
            while ($parts[0] == ''){
                array_shift($parts);
            }

            while ($parts[count($parts)-1] == ''){
                array_pop($parts);
            }

            $output = implode("\n", $parts);
        }

        if (in_array('last_x_lines', $options)){
            $index = array_search('last_x_lines', $options);
            $x = $options[$index+1];
            $lines = explode("\n", $output);
            $last_x_lines = array_slice($lines,-((int)$x));
            $output = implode("\n", $last_x_lines);
        }

        return $output;
    }

    /**
     * Verify that a specific test is passing, outputting a pass/fail string, and linking to its file & line number. (*REQUIRES `taeluf/tester`, and does not have an integration with PHPUnit.*)
     *
     * @usage `@claim(TestName)` - All tests prefix with `test`. Do NOT include `test` in the name.
     * @output The configured `claim.pass` or `claim.fail` string and a relative link to the test (*tested working on GitLab & GitHub*)
     *
     * @param $test_name string the name of the the test. For a method like `testSomeThing`, this value is `SomeThing`.
     *
     * @return the configured pass/fail string & relative link to the test.
     */
    public function at_claim(string $test_name){

        $root = getcwd();
        $path = $root.'/vendor/bin/phptest';
        if (!file_exists($path) || !is_executable($path)){
            $this->scrawl->warn("Claim unavailable", "The testlib script vendor/bin/phptest was not found, so @claim() cannot be processed");
            return '';
        }

        $cmd = "\"$path\" -test \"$test_name\" -output_type pass_fail";
        ob_start();
        system($cmd, $result_code);
        $output = trim(ob_get_clean());


        $probe_cmd = "\"$path\" probe -test \"$test_name\"";
        ob_start();
        system($probe_cmd, $result_code);
        $probe_json = trim(ob_get_clean());
        $probe = json_decode($probe_json, true);

        $link = null;
        if (isset($probe['result'][0])){
            $result = $probe['result'][0];
            if (!empty($result['rel_file'])){
                $link = $result['rel_file'];
            }
            if (!empty($result['line_start'])){
                $link .= '#L'.$result['line_start'];
            }
        }

        if ($output === "1"){
            $str = $this->scrawl->claim_pass;
            $this->claims_passed[] = $test_name;
            $this->scrawl->good("@claim() passed", "$test_name");
        } else {
            $str = $this->scrawl->claim_fail;
            $this->claims_failed[] = $test_name;
            $this->scrawl->warn("@claim() failed", "$test_name");
        }

        if ($link == null || $str == ''){
            return $str;
        } else {
            return "[$str]($link)";
        }
    }

    /**
     * Load a built-in template or a template from a configured template directory.
     *
     * @usage `@template(template_name, arg1, arg2)` the args are passed to the template as an array
     * @output the string output of the executed `.md.php` file.
     */
    public function at_template(string $templateName, ...$templateArgs){
        return $this->scrawl->get_template($templateName, $templateArgs);
    }

    /**
     * Import something previously exported with `@export` or `@export_start/@export_end`
     *
     * @usage `@import(Namespace.Key)`
     * @output whatever was exported by `@export(key)` or `@export_start(key)` to `@export_end(key)`
     */
    public function at_import(string $key){
        $output = $this->scrawl->get('export',$key);

        if ($output===null){
            $this->scrawl->warn('@import', '@import('.$key.') failed');
            $this->import_failures[] = $key;
            $replacement = '# Import key "'.$key.'" not found.';
        } else {
            $replacement = $output;
        }
        return $replacement;
    }

    /**
     * Copy a file's content into your markdown.
     *
     * @usage `@file(rel/path/to/file.ext)`
     * @output the file's content, `trim`med.
     */
    public function at_file(string $relFilePath){
        $file = $this->scrawl->dir_root.'/'.$relFilePath;

        if (!is_file($file)){
            $this->scrawl->warn('@file', "@file($relFilePath) failed. File does not exist.");
            return "'$file' is not a file.";
        }

        return trim(file_get_contents($file));
    }



    /**
     * Outputs multiple markdown links.
     *
     * @usage `@see_files(/rel/path1.md; Name1, /rel/path2.md; Name2, ...)` names are optional.
     * @output `[Name1](/rel/path1.md), [Name2](/rel/path2.md`)
     */
    public function at_see_files(...$files){
        $links = [];
        foreach ($files as $arg){
            $parts = explode(';', $arg);
            $name = null;
            if (isset($parts[1]))$name = trim($parts[1]);
            $links[] = $this->at_see_file(trim($parts[0]), $name);
        }

        return implode(", ", $links);
    }

    /**
     * Get a link to a file in your repo
     *
     * @usage `@see_file(rel/path.ext, Optional Name)`
     * @output `[Optional Name](rel/path.ext)` or `[rel/path.ext](rel/path.ext)` if no name provided
     */
    public function at_see_file(string $relFilePath, ?string $link_name = null){
        $path = $this->scrawl->dir_root.'/'.$relFilePath;
        if (!is_file($path) && !is_dir($path)
            &&filter_var($relFilePath, FILTER_VALIDATE_URL)===false
        ){
            $this->see_file_failures[$path] = $relFilePath;
            $this->scrawl->warn("@see()","'$relFilePath' does not exist");
        }
        $urlPath = $relFilePath;
        if ($urlPath[0]!='/')$urlPath = '/'.$urlPath;
        $link_name = $link_name ?? $relFilePath;
        $link = '['.$link_name.']('.$urlPath.')';
        return $link;
    }


    /** 
     * Returns a markdown link. (*May provide validation in the future*)
     *
     * @usage `@hard_link(https://example.com, Optional Name)`
     * @output `[Optional Name](https://example.com)`
     */
    public function at_hard_link(string $url, string $name=null){
        if ($name==null) $name = $url;
        return '['.$name.']('.$url.')';
    }

    /** 
     * Output links configured in your `scrawl.json` config file, under the `"links"` key. Config format is `{..., "links": { "link_name": "https://example.org"} }`
     *
     * @usage `@link(link_name)`
     * @output `[LinkName](https://url.com)`
     */
    public function at_link(string $link_name, ?string $alternative_text = null){
        $url = $this->scrawl->options['links'][$link_name] ?? null;
        if ($url == null){
            $this->scrawl->warn("Link not found", "Your config json does not include link \"$link_name\". Define like: `\"links\":{\"$link_name\": \"https://example.com\"}`");
            return '(*error: link "'.$link_name.'" not found*)';
        }

        $header = "Link printed ($link_name)";

        if ($alternative_text!=null)$link_name = $alternative_text;

        $mdlink = '['.$link_name.']('.$url.')';

        $this->scrawl->good("@link()", $mdlink);

        return $mdlink;
    }

    /**
     * Get a link to common services (twitter, gitlab, github, facebook)
     *
     * @usage `@easy_link(twitter, TaelufDev)`
     * @output `[TaelufDef](https://twitter.com/TaelufDev)`
     *
     */ 
    public function at_easy_link(string $service, string $target){
        $sites = [
            'twitter'=>'https://twitter.com/',
            'gitlab'=>'https://gitlab.com/',
            'github'=>'https://github.com/',
            'facebook'=>'https://facebook.com/',
            'tlf'=>'https://tluf.me/',
        ];

        $host = $sites[strtolower($service)] ?? null;
        if ($host==null){
            $this->scrawl->warn('@easy_link', "@easy_link($service,$target): Service '$service' is not valid. Options are "
                .implode(', ', array_keys($sites))
            );
            return "--service '$service' not found--";
        }
        $url = $host.$target;
        $linkName = $target;
        $mdLink = "[$linkName]($url)";
        return $mdLink;
    }





    public function bootstrap(){}
    public function ast_generated(string $className, array $ast){}
    public function astlist_generated(array $asts){}
    public function scan_filelist_loaded(array $code_files){}
    public function scan_file_processed(string $path, string $relPath, string $file_content, array $file_exports){}
    public function scan_filelist_processed(array $code_files, array $all_exports){}
    public function doc_filelist_loaded(array $doc_files, \Tlf\Scrawl\Ext\MdVerbs $mdverb_ext){}
    public function doc_file_loaded($path,$relPath,$file_content){}
    public function doc_file_processed($path,$relPath,$file_content){}
    public function doc_filelist_processed($doc_files){}

    public function scrawl_finished(){

        $this->scrawl->header("@see() summary");

        //////
        // summary report of @see()
        //////
        $actual_failures = [];
        foreach ($this->see_file_failures as $abs_path => $rel_path){
            if (is_file($abs_path) || is_dir($abs_path))continue;
            $actual_failures[] = $rel_path;
        }
        if (count($actual_failures)==0){
            $this->scrawl->good("Success", "All referenced files exist.");
        } else {
            $this->scrawl->warn("Failure", '@see() failed for the following: ');
            $this->scrawl->report("  ".implode(", ", $actual_failures));
        }


        $this->scrawl->header("@claim() summary");
        //////
        // summary report of @claim()
        //////
        $cf = count($this->claims_failed);
        $cp = count($this->claims_passed);
        $ct = $cp + $cf;
        if ($ct == 0){
            $this->scrawl->report("No @claim()s presesnt");
        } 

        if ($cp > 0){ 
            $this->scrawl->good("$cp Successes", implode(",", $this->claims_passed));
        } else if ($ct>0) {
            $this->scrawl->report("No successful claims");
        }

        if ($cf > 0) {
            $this->scrawl->warn("$cf Failures", implode(",", $this->claims_failed));
        } else if ($ct > 0){
            $this->scrawl->good("No failures", '');
        }



        $this->scrawl->header("@import() summary");
        //////
        // summary report of @import() failures
        //////
        if (count($this->import_failures)==0){
            $this->scrawl->good("Success!", "All imports were successful");
        } else {
            $this->scrawl->warn(count($this->import_failures)." Failures", implode(", ", $this->import_failures));
        }

    }
}