<?php
namespace Tlf;
class Scrawl {
/** array for get/set */
public array $stuff = [];
public array $extensions = [
'code'=>[],
];
/** absolute path to your documentation dir */
public ?string $dir_docs = null;
/** absolute path to the root of your project */
public ?string $dir_root = null;
/** absolute path to the documentation source dir */
public ?string $dir_src = null;
/** array of relative path to dirs to scan, within your dir_root */
public ?array $dir_scan = [];
/** array of handlers for the mdverb extension */
public array $verb_handlers = [];
/** absolute path to directories that contain md templates */
public array $template_dirs = [];
/** if true, append two spaces to every line so all new lines are parsed as new lines */
public bool $markdown_preserveNewLines = true;
/** if true, add an html comment to md docs saying not to edit directly */
public bool $markdown_prependGenNotice = true;
/** if true, copies `docs/README.md` to project root `README.md` */
public bool $readme_copyFromDocs = true;
/** If true will delete all files in your docs dir before running */
public bool $deleteExistingDocs = false;
/**
* @parma $options `key=>value` array to set properties
*/
public function __construct(array $options=[]){
$this->options = $options;
foreach ($this->options as $k=>$o){
$k = str_replace('.','_',$k);
$this->$k = $o;
}
$this->template_dirs[] = __DIR__.'/Template/';
}
public function get_template(string $name, array $args){
foreach ($this->template_dirs as $path){
if (file_exists($file = $path.'/'.$name.'.md.php')){}
else if (file_exists($file=$path.'/'.$name.'.php')){}
else continue;
$out = (function(array $args, string $file) {
ob_start();
require($file);
$out = ob_get_clean();
return $out;
})($args, $file);
return $out;
}
$this->warn("@template", $msg="Template '$name' does not exist.");
return $msg;
}
public function get(string $group, string $key){
if (!isset($this->stuff[$group])){
$this->warn("Group not set", $group);
return null;
} else if (!isset($this->stuff[$group][$key])){
$this->warn("Group.Key not set", "$group.$key");
return null;
}
return $this->stuff[$group][$key];
}
public function get_group(string $group){
if (!isset($this->stuff[$group])){
$this->warn("Group not set", $group);
return null;
}
return $this->stuff[$group];
}
public function set(string $group, string $key, $value){
$this->stuff[$group][$key] = $value;
}
public function parse_str($str, $ext){
$out = [];
foreach ($this->extensions['code'][$ext] as $ext){
$out = $ext->parse_str($str);
}
return $out;
}
/**
* save a file to disk in the documents directory
*/
public function write_doc(string $rel_path, string $content){
$content = $this->prepare_md_content($content);
$rel_path = str_replace('../','/', $rel_path);
$path = $this->dir_docs.'/'.$rel_path;
$dir = dirname($path);
if (!is_dir($dir))mkdir($dir,0755,true);
if (is_file($path)){
$this->good('Overwrite',$rel_path);
} else {
$this->good('Write',$rel_path);
}
file_put_contents($path, $content);
}
/**
* save a file to disk in the root directory
*/
public function write_file(string $rel_path, string $content){
$rel_path = str_replace('../','/', $rel_path);
$path = $this->dir_root.'/'.$rel_path;
$dir = dirname($path);
if (!is_dir($dir))mkdir($dir,0755,true);
if (is_file($path)){
$this->good('Overwrite',$rel_path);
} else {
$this->good('Write',$rel_path);
}
file_put_contents($path, $content);
}
/**
* Read a file from disk, from the project root
*/
public function read_file(string $rel_path){
return file_get_contents($this->dir_root.'/'.$rel_path);
}
/**
* Read a file from disk, from the project docs dir
*/
public function read_doc(string $rel_path){
return file_get_contents($this->dir_docs.'/'.$rel_path);
}
/** get a path to a docs file */
public function doc_path(string $rel_path){
return $this->dir_docs.'/'.$rel_path;
}
/**
* Output a message to cli (may do logging later, idk)
*/
public function report(string $msg){
echo "\n$msg";
}
/**
* Output a message to cli, header highlighted in red
*/
public function warn($header, $message){
echo "\033[0;31m$header:\033[0m $message\033[0;31m\033[0m\n";
}
/**
* Output a message to cli, header highlighted in red
*/
public function good($header, $message){
echo "\033[0;32m$header:\033[0m $message\033[0;31m\033[0m\n";
}
/** apply small fixes to markdown */
public function prepare_md_content(string $markdown){
if ($this->markdown_preserveNewLines){
$markdown = str_replace("\n"," \n",$markdown);
}
if ($this->markdown_prependGenNotice){
// @TODO give relative path to source file
$markdown = "<!-- DO NOT EDIT. This file generated from template by Code Scrawl https://tluf.me/php/code-scrawl/ --> \n".$markdown;
}
return $markdown;
}
public function get_all_docsrc_files(){
$files = \Tlf\Scrawl\Utility\Main::allFilesFromDir($this->dir_src, '');
return $files;
}
/**
* get array of all files in `$scrawl->dir_scan`
* @return array or relative paths within `$scrawl->dir_scan`
*/
public function get_all_scan_files(): array{
$all = [];
foreach ($this->dir_scan as $f){
$files = \Tlf\Scrawl\Utility\Main::allFilesFromDir($this->dir_root, $f);
$all = array_merge($all, $files);
}
return $all;
}
/**
* Generate api docs for all files
* (currently only php files)
*/
public function generate_apis() {
foreach ($this->get_all_scan_files() as $file){
$this->generate_api($file);
}
}
/**
* Generate api doc for a signle file
* (currently only php files)
*
* @param $rel_path relative path inside `$scrawl->dir_root`
*/
public function generate_api($rel_path){
if (strtolower(pathinfo($rel_path,PATHINFO_EXTENSION))!=='php')return;
$php_ext = new \Tlf\Scrawl\FileExt\Php($this);
$ast = $php_ext->parse_file($rel_path);
$path = $ast['path'];
// $rel_path = substr($path, strlen($scrawl->dir_root));
// $ast['path'] = $rel_path;
// $scrawl->set('ast','file.'.$rel_path, $ast);
$classes = array_merge($ast['class'] ?? [], $ast['namespace']['class'] ?? []);
if (count($classes)==0)return;
$doc = "# File ".$rel_path."\n";
foreach ($classes as $c){
$markdown = $this->get_template('ast/class', [null,$c,null]);
$doc .="\n".$markdown;
}
$this->write_doc('api/'.$rel_path.'.md', $doc);
}
public function get_all_classes(){
$php_ext = new \Tlf\Scrawl\FileExt\Php($this);
$files = $this->get_all_scan_files();
$classes = [];
foreach ($files as $f){
if (strtolower(pathinfo($f,PATHINFO_EXTENSION))!=='php')continue;
$this->report("Generate Ast: ".$f);
$ast = $php_ext->parse_file($f);
$new_classes = array_merge($ast['class'] ?? [], $ast['namespace']['class'] ?? []);
foreach ($new_classes as &$c){
$c['file'] = $f;
$classes[$c['fqn']] = $c;
}
}
return $classes;
}
/**
* Execute scrawl in its entirety
*/
public function run(){
// 1. process all scan dirs
// parse into ast,
// set asts (class) on scrawl
// 2. Generate api dir
// 3. Scan docs source dir
// 4. Process docs source into docs
if ($this->deleteExistingDocs){
$del_dir = realpath($this->dir_docs);
$cwd = realpath(getcwd());
$len = strlen($cwd);
if (substr($del_dir,0,$len)===$cwd
&& strlen($cwd)>6
&&count(explode('/',$cwd))>=4
){
$this->warn("Delete Dir", $del_dir);
\Tlf\Scrawl\Utility\Main::DANGEROUS_removeNonEmptyDirectory($del_dir);
}
}
$this->report("Generate Asts");
//////////
// process php files into 'ast'
//////////
$classes = $this->get_all_classes();
foreach ($classes as $c){
$this->set('ast', 'class.'.$c['fqn'], $c);
}
$this->report("Generate APIs");
$this->generate_apis();
//////////
// process all code files using code extensions
//////////
$export_docblock = new \Tlf\Scrawl\FileExt\ExportDocBlock();
$export_startend = new \Tlf\Scrawl\FileExt\ExportStartEnd();
// $php_ext = new \Tlf\Scrawl\FileExt\Php();
$code_files = $this->get_all_scan_files();
foreach ($code_files as $f){
$path = $this->dir_root.'/'.$f;
$file_content = file_get_contents($path);
// docblock @export()s
$docblocks = $export_docblock->get_docblocks($file_content);
$exports = $export_docblock->get_exports($docblocks);
foreach ($exports as $k=>$e)$this->set('export',$k, $e);
// @export_start/@export_end()
$exports = $export_startend->get_exports($file_content);
foreach ($exports as $k=>$e)$this->set('export',$k, $e);
}
//////////
// process all documentation source files
//////////
$src_files = $this->get_all_docsrc_files();
$mdverb_ext = $this->get_mdverb_ext();
foreach ($src_files as $sf){
$path = $this->dir_src.'/'.$sf;
$content = file_get_contents($path);
// process mdverbs
$content = $mdverb_ext->replace_all_verbs($content);
if (substr($sf,-7)=='.src.md')$sf = substr($sf,0,-7).'.md';
$this->write_doc($sf, $content);
}
$readme_path = $this->doc_path('README.md');
if ($this->readme_copyFromDocs && file_exists($readme_path)){
$this->write_file('README.md', $this->read_doc('README.md'));
}
$this->good("Finished",'Code Scrawl Ran');
}
/**
* get an array ast from a file
* Currently only supports php files
* Also sets the ast to scrawl
*/
public function get_ast(string $file): ?array {
$ext = pathinfo($file, PATHINFO_EXTENSION);
if ($ext!='php'){
$this->report("File '$file' not .php. Skip ast parse.");
return null;
}
$php_ext = new \Tlf\Scrawl\FileExt\Php($this);
$ast = $php_ext->parse_file($file);
// $php_ext->set_ast($ast);
return $ast;
}
/** get the class ast
* @param $class fully qualified class name
*/
public function get_class_ast(string $class): ?array{
$ast = $this->get('ast','class.'.$class);
return $ast;
}
public function get_mdverb_ext(){
$mdverb_ext = new \Tlf\Scrawl\Ext\MdVerbs($this);
$main_verbs_ext = new \Tlf\Scrawl\Ext\MdVerb\MainVerbs($this);
$main_verbs_ext->setup_handlers($mdverb_ext);
$ast_ext = new \Tlf\Scrawl\Ext\MdVerb\Ast($this);
$mdverb_ext->handlers['ast'] = [$ast_ext, 'get_markdown'];
foreach ($this->verb_handlers as $k=>$v){
$mdverb_ext->handlers[$k] = $v;
}
return $mdverb_ext;
}
}