From: Michael Wallner Date: Wed, 6 Mar 2019 15:12:15 +0000 (+0100) Subject: stub file generation X-Git-Url: https://git.m6w6.name/?a=commitdiff_plain;h=29d603eb211a184106cf3caa224777615bcd61a8;p=mdref%2Fmdref stub file generation --- diff --git a/bin/ref2stub b/bin/ref2stub new file mode 100755 index 0000000..d2dd686 --- /dev/null +++ b/bin/ref2stub @@ -0,0 +1,29 @@ +#!/usr/bin/env php +[ ...]\n", $argv[0]); + exit(1); +} + +$ref = new Reference(array_slice($argv, 1)); +/** @var $repo Repo */ +foreach ($ref as $repo) { + $fd = fopen($repo->getName().".stub.php", "w"); + ob_start(function($s) use($fd) { + fwrite($fd, $s); + return $s; + }); + + printf("getRootEntry(); + $root->getStructure()->format(); + + ob_end_flush(); + fclose($fd); +} + diff --git a/mdref/Action.php b/mdref/Action.php index df974b9..deb62e5 100644 --- a/mdref/Action.php +++ b/mdref/Action.php @@ -4,6 +4,7 @@ namespace mdref; use http\Env\Request; use http\Env\Response; +use http\Message\Body; /** * Request handler @@ -95,6 +96,21 @@ class Action { $this->response->send(); } + /** + * Server a PHP stub + */ + private function serveStub() { + $name = $this->request->getQuery("ref", "s"); + $repo = $this->reference->getRepoForEntry($name); + if (!$repo->hasStub($stub)) { + throw new Exception(404, "Stub not found"); + } + $this->response->setHeader("Content-Type", "application/x-php"); + $this->response->setContentDisposition(["attachment" => ["filename" => "$name.stub.php"]]); + $this->response->setBody(new Body(fopen($stub, "r"))); + $this->response->send(); + } + /** * Serve a preset * @param \stdClass $pld @@ -114,6 +130,9 @@ class Action { case "index.js": $this->serveJavascript(); break; + case "stub": + $this->serveStub(); + break; default: throw new Exception(404, "$pld->ref not found"); } diff --git a/mdref/Entry.php b/mdref/Entry.php index 9135dd7..589cf41 100644 --- a/mdref/Entry.php +++ b/mdref/Entry.php @@ -105,7 +105,7 @@ class Entry implements \IteratorAggregate { } /** - * Read the description of the ref entry file + * Read the first line of the description of the ref entry file * @return string */ public function getDescription() { @@ -118,6 +118,20 @@ class Entry implements \IteratorAggregate { return $this; } + /** + * Read the full description of the ref entry file + * @return string + */ + public function getFullDescription() { + if ($this->isFile()) { + return trim($this->getFile()->readFullDescription()); + } + if ($this->isRoot()) { + return trim($this->repo->getRootEntry()->getFullDescription()); + } + return $this; + } + /** * Read the intriductory section of the refentry file * @return string @@ -192,6 +206,22 @@ class Entry implements \IteratorAggregate { return ctype_upper($base{0}); } + public function getEntryName() { + return end($this->list); + } + + public function getNsName() { + if ($this->isRoot()) { + return $this->getName(); + } elseif ($this->isFunction()) { + $parts = explode("/", trim($this->getName(), "/")); + $self = array_pop($parts); + return implode("\\", $parts) . "::" . $self; + } else { + return strtr($this->getName(), "/", "\\"); + } + } + /** * Display name * @return string @@ -266,4 +296,8 @@ class Entry implements \IteratorAggregate { function getIterator() { return new Tree($this->getBasename(), $this->repo); } + + function getStructure() { + return new Structure($this); + } } diff --git a/mdref/File.php b/mdref/File.php index 4afd3b4..33cfa6f 100644 --- a/mdref/File.php +++ b/mdref/File.php @@ -24,30 +24,46 @@ class File { * @return string */ public function readTitle() { - if (0 === fseek($this->fd, 1, SEEK_SET)) { + if ($this->rewind(1)) { return fgets($this->fd); } } /** - * Read the description of the refentry + * Read the description (first line) of the refentry * @return string */ public function readDescription() { - if (0 === fseek($this->fd, 0, SEEK_SET) + if ($this->rewind() && (false !== fgets($this->fd)) && (false !== fgets($this->fd))) { return fgets($this->fd); } } + /** + * Read the full description (first section) of the refentry + * @return string + */ + public function readFullDescription() { + $desc = $this->readDescription(); + while (false !== ($line = fgets($this->fd))) { + if ($line{0} === "#") { + break; + } else { + $desc .= $line; + } + } + return $desc; + } + /** * Read the first subsection of a global refentry * @return string */ public function readIntro() { $intro = ""; - if (0 === fseek($this->fd, 0, SEEK_SET)) { + if ($this->rewind()) { $header = false; while (!feof($this->fd)) { @@ -55,7 +71,7 @@ class File { break; } /* search first header and read until next header*/ - if ("## " === substr($line, 0, 3)) { + if ($this->isHeading($line)) { if ($header) { break; } else { @@ -70,4 +86,42 @@ class File { } return $intro; } + + public function readSection($title) { + $section = ""; + if ($this->rewind()) { + while (!feof($this->fd)) { + if (false === ($line = fgets($this->fd))) { + break; + } + /* search for heading with $title and read until next heading */ + if ($this->isHeading($line, $title)) { + do { + if (false === $line = fgets($this->fd)) { + break; + } + if ($this->isHeading($line)) { + break; + } + $section .= $line; + } while (true); + } + } + } + return $section; + } + + private function rewind($offset = 0) { + return 0 === fseek($this->fd, $offset, SEEK_SET); + } + + private function isHeading(string $line, string $title = null) { + if ("## " !== substr($line, 0, 3)) { + return false; + } + if (isset($title)) { + return !strncmp(substr($line, 3), $title, strlen($title)); + } + return true; + } } diff --git a/mdref/Repo.php b/mdref/Repo.php index 8f4b830..4b291ab 100644 --- a/mdref/Repo.php +++ b/mdref/Repo.php @@ -111,6 +111,11 @@ class Repo implements \IteratorAggregate { } } + public function hasStub(&$path = null) { + $path = $this->getPath($this->getName() . ".stub.php"); + return is_file($path) && is_readable($path); + } + /** * Get an Entry instance * @param string $entry diff --git a/mdref/Structure.php b/mdref/Structure.php new file mode 100644 index 0000000..aed3589 --- /dev/null +++ b/mdref/Structure.php @@ -0,0 +1,436 @@ +entry = $entry; + + if ($entry->isRoot() || $entry->isNsClass()) { + if ($entry->isRoot()) { + $this->type = self::OF_NAMESPACE; + $this->getStructureOfRoot(); + } elseif (!strncmp($entry->getTitle(), "namespace", strlen("namespace"))) { + $this->type = self::OF_NAMESPACE; + $this->getStructureOfNs(); + } else { + $this->type = self::OF_CLASS; + $this->getStructureOfClass(); + } + } elseif ($entry->isFunction()) { + $this->type = self::OF_FUNC; + $this->getStructureOfFunc(); + } else { + $this->type = self::OF_OTHER; + } + } + + static function of(Entry $entry) : StructureOf { + return (new static($entry))->getStruct(); + } + + function getStruct() : StructureOf { + return $this->struct; + } + + function format() { + $this->struct->format(); + } + + private function getStructureOfFunc() : StructureOfFunc { + return $this->struct = new StructureOfFunc([ + "ns" => $this->entry->getParent()->getNsName(), + "name" => $this->entry->getEntryName(), + "desc" => $this->entry->getFullDescription(), + "returns" => $this->getReturns(), + "params" => $this->getParams(), + "throws" => $this->getThrows() + ]); + } + + private function getStructureOfClass() : StructureOfClass { + return $this->struct = new StructureOfClass([ + "ns" => $this->entry->getParent()->getNsName(), + "name" => $this->prepareClassName(), + "desc" => $this->entry->getFullDescription(), + "consts" => $this->getConstants(), + "props" => $this->getProperties(), + "funcs" => $this->getFunctions(), + "classes" => $this->getClasses(), + ]); + } + + private function getStructureOfNs() : StructureOfNs { + return $this->struct = new StructureOfNs([ + "name" => $this->entry->getNsName(), + "desc" => $this->entry->getFullDescription(), + "consts" => $this->getConstants(), + "classes" => $this->getClasses(), + ]); + } + + private function getStructureOfRoot() : StructureOfRoot { + return $this->struct = new StructureOfRoot([ + "name" => $this->entry->getName(), + "desc" => $this->entry->getFile()->readIntro(), + "consts" => $this->getConstants(), + "classes" => $this->getClasses() + ]); + } + + private function getSection(string $section) : string { + return $this->entry->getFile()->readSection($section); + } + + private function prepareClassName() { + return preg_replace_callback_array([ + '/(?Pclass|interface|trait)\s+([\\\\\w]+\\\)?(?P\w+)\s*/' => function($match) { + return $match["type"] . " " . $match["name"] . " "; + }, + '/(?Pextends|implements)\s+(?P[\\w]+(?:(?:,\s*[\\\\\w]+)*))/' => function ($match) { + return $match["op"] . " " . preg_replace('/\b(?entry->getTitle()); + } + + private function splitList(string $pattern, string $text) : array { + $text = trim($text); + if (strlen($text) && !preg_match("/^None/", $text)) { + if (preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) { + return $matches; + } + } + return []; + } + + private function getConstantValue(string $name) { + $ns = $this->entry->getNsName(); + if (defined("\\$ns::$name")) { + return constant("\\$ns::$name"); + } + if (defined("\\$ns\\$name")) { + return constant("\\$ns\\$name"); + } + return null; + } + + private function getConstants() : array { + static $pattern = '/ + \*\s+ + (?\w+) + (?:\s*=\s*(?P.+))? + (?P(?:\s*\n\s*[^\*\n#].*)*) + /x'; + + $structs = []; + $consts = $this->splitList($pattern, $this->getSection("Constants")); + foreach ($consts as $const) { + if (!isset($const["value"]) || !strlen($const["value"])) { + $const["value"] = $this->getConstantValue($const["name"]); + } + $structs[$const["name"]] = new StructureOfConst($const); + } + return $structs; + } + + private function getProperties() : array { + static $pattern = '/ + \*\s+ + (?P\w+\s+)* + (?P[\\\\\w]+)\s+ + (?\$\w+) + (?:\s*=\s*(?P.+))? + (?P(?:\s*\n\s*[^\*].*)*) + /x'; + + $structs = []; + $props = $this->splitList($pattern, $this->getSection("Properties")); + foreach ($props as $prop) { + $structs[$prop["name"]] = new StructureOfVar($prop); + } + return $structs; + } + + private function getFunctions() : array { + $structs = []; + foreach ($this->entry as $sub) { + if ($sub->isFunction()) { + $structs[$sub->getEntryName()] = static::of($sub); + } + } + return $structs; + } + + private function getClasses() : array { + $structs = []; + foreach ($this->entry as $sub) { + if ($sub->isNsClass()) { + $structs[$sub->getEntryName()] = static::of($sub); + } + } + return $structs; + } + + private function getParams() : array { + static $pattern = '/ + \*\s+ + (?P\w+\s+)* + (?P[\\\\\w]+)\s+ + (?\$\w+) + (?:\s*=\s*(?P.+))? + (?P(?:\s*[^*]*\n(?!\n)\s*[^\*].*)*) + /x'; + + $structs = []; + $params = $this->splitList($pattern, $this->getSection("Params")); + foreach ($params as $param) { + $structs[$param["name"]] = new StructureOfVar($param); + } + return $structs; + } + + private function getReturns() : array { + static $pattern = '/ + \*\s+ + (?[\\\\\w]+) + \s*,?\s* + (?P(?:.|\n(?!\s*\*))*) + /x'; + + $returns = $this->splitList($pattern, $this->getSection("Returns")); + $retdesc = ""; + foreach ($returns as list(, $type, $desc)) { + if (strlen($retdesc)) { + $retdesc .= "\n\t or $type $desc"; + } else { + $retdesc = $desc; + } + } + return [implode("|", array_unique(array_column($returns, "type"))), $retdesc]; + } + + private function getThrows() : array { + static $pattern = '/ + \*\s+ + (?P[\\\\\w]+)\s* + /x'; + + $throws = $this->splitList($pattern, $this->getSection("Throws")); + return array_column($throws, "exception"); + } +} + +abstract class StructureOf { + function __construct(array $props = []) { + foreach ($props as $key => $val) { + if (is_int($key)) { + continue; + } + if (!property_exists(static::class, $key)) { + throw new \UnexpectedValueException( + sprintf("Property %s::\$%s does not exist", static::class, $key) + ); + } + if ($key === "desc" || $key === "modifiers" || $key === "defval") { + $val = trim($val); + } + $this->$key = $val; + } + } + + // abstract function format(); + + function formatDesc(int $level, array $tags = []) { + $indent = str_repeat("\t", $level); + $desc = trim($this->desc); + if (false !== stristr($desc, "deprecated in")) { + $tags[] = "@deprecated"; + } + if ($tags) { + $desc .= "\n\n@" . implode("\n@", $tags); + } + $desc = preg_replace('/[\t ]*\n/',"\n$indent * ", $desc); + printf("%s/**\n%s * %s\n%s */\n", $indent, $indent, $desc, $indent); + } +} + +class StructureOfRoot extends StructureOf { + public $name; + public $desc; + public $consts; + public $classes; + + function format() { + $this->formatDesc(0); + + foreach ($this->consts as $const) { + $const->format(0); + printf(";\n"); + } + + printf("namespace %s;\nuse %s;\n", $this->name, $this->name); + StructureOfNs::$last = $this->name; + + foreach ($this->getClasses() as $class) { + $class->format(); + } + } + + function getClasses() { + yield from $this->classes; + foreach ($this->classes as $class) { + yield from $class->getClasses(); + } + } +} +class StructureOfNs extends StructureOfRoot { + public $funcs; + + public static $last; + + function format() { + print $this->formatDesc(0); + + if (strlen($this->name) && $this->name !== StructureOfNs::$last) { + StructureOfNs::$last = $this->name; + printf("namespace %s;\n", $this->name); + } + foreach ($this->consts as $const) { + $const->format(0); + printf(";\n"); + } + } +} + +class StructureOfClass extends StructureOfNs +{ + public $ns; + public $props; + + static $lastNs; + + function format() { + if ($this->ns !== StructureOfNs::$last) { + printf("namespace %s;\n", $this->ns); + StructureOfNs::$last = $this->ns; + } + + print $this->formatDesc(0); + printf("%s {\n", $this->name); + + foreach ($this->consts as $const) { + $const->format(1); + printf(";\n"); + } + + foreach ($this->props as $prop) { + $prop->formatAsProp(1); + printf(";\n"); + } + + foreach ($this->funcs as $func) { + $func->format(1); + if (strncmp($this->name, "interface", strlen("interface"))) { + printf(" {}\n"); + } else { + printf(";\n"); + } + } + + printf("}\n"); + } +} + +class StructureOfFunc extends StructureOf { + public $ns; + public $class; + public $name; + public $desc; + public $params; + public $returns; + public $throws; + + function format(int $level) { + $tags = []; + foreach ($this->params as $param) { + $tags[] = "param {$param->type} {$param->name} {$param->desc}"; + } + foreach ($this->throws as $throws) { + $tags[] = "throws $throws"; + } + if ($this->name !== "__construct" && $this->returns[0]) { + $tags[] = "return {$this->returns[0]} {$this->returns[1]}"; + } + $this->formatDesc(1, $tags); + printf("\tfunction %s(", $this->name); + $comma = ""; + foreach ($this->params as $param) { + print $comma; + $param->formatAsParam($level); + $comma = ", "; + } + printf(")"); + } +} + +class StructureOfConst extends StructureOf { + public $name; + public $desc; + public $value; + + function format(int $level) { + $indent = str_repeat("\t", $level); + $this->formatDesc($level); + printf("%sconst %s = ", $indent, $this->name); + var_export($this->value); + } +} + +class StructureOfVar extends StructureOf { + public $name; + public $type; + public $desc; + public $modifiers; + public $defval; + + function formatDefval() { + if (strlen($this->defval)) { + if (false && defined($this->defval)) { + printf(" = "); + var_export(constant($this->defval)); + } else if (strlen($this->defval)) { + printf(" = %s", $this->defval); + } + } elseif ($this->modifiers) { + if (stristr($this->modifiers, "optional") !== false) { + printf(" = NULL"); + } + } + } + function formatAsProp(int $level) { + $indent = str_repeat("\t", $level); + $this->formatDesc($level, + preg_split('/\s+/', $this->modifiers, -1, PREG_SPLIT_NO_EMPTY) + + [-1 => "var {$this->type}"] + ); + printf("%s%s %s", $indent, $this->modifiers, $this->name); + $this->formatDefval(); + } + + function formatAsParam(int $level) { + printf("%s", $this->name); + $this->formatDefval(); + } +} diff --git a/views/index.phtml b/views/index.phtml index 78d9561..0a25061 100644 --- a/views/index.phtml +++ b/views/index.phtml @@ -14,6 +14,20 @@ getTitle()) ?>
getIntro()) ?>
+ hasStub($stub)) : ?> +
+

Download the Stub file:

+ +
+