stub file generation
authorMichael Wallner <mike@php.net>
Wed, 6 Mar 2019 15:12:15 +0000 (16:12 +0100)
committerMichael Wallner <mike@php.net>
Wed, 6 Mar 2019 15:15:25 +0000 (16:15 +0100)
bin/ref2stub [new file with mode: 0755]
mdref/Action.php
mdref/Entry.php
mdref/File.php
mdref/Repo.php
mdref/Structure.php [new file with mode: 0644]
views/index.phtml

diff --git a/bin/ref2stub b/bin/ref2stub
new file mode 100755 (executable)
index 0000000..d2dd686
--- /dev/null
@@ -0,0 +1,29 @@
+#!/usr/bin/env php
+<?php
+
+namespace mdref;
+
+require_once __DIR__."/../vendor/autoload.php";
+
+if ($argc < 2) {
+       fprintf(STDERR, "Usage: %s <ref>[ <ref> ...]\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("<?php\n");
+       $root = $repo->getRootEntry();
+       $root->getStructure()->format();
+
+       ob_end_flush();
+       fclose($fd);
+}
+
index df974b9f1f1595d9fb48f0ad2822475874ffbb1d..deb62e5a64c1260d59607950e29d0cefc8867ba6 100644 (file)
@@ -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");
                }
index 9135dd788479e04d44e7510fee0cc668948ca95e..589cf412eb72b4093d485261cba99d9d33ac64e2 100644 (file)
@@ -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);
+       }
 }
index 4afd3b430559a16b804945f1e14aa0eb6b4dadf2..33cfa6f413627049dff644014af1a1d9f85425a7 100644 (file)
@@ -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;
+       }
 }
index 8f4b830295a138f87d50222bc2bb27b7e6ddf59f..4b291ab6579020923cdb1e305dc66cbeb1716d5d 100644 (file)
@@ -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 (file)
index 0000000..aed3589
--- /dev/null
@@ -0,0 +1,436 @@
+<?php
+
+namespace mdref;
+
+/**
+ * Structure of an entry
+ */
+class Structure {
+       const OF_OTHER = "other";
+       const OF_NAMESPACE = "ns";
+       const OF_CLASS = "class";
+       const OF_FUNC = "func";
+
+       private $type;
+       private $struct;
+       private $entry;
+
+       function __construct(Entry $entry) {
+               $this->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([
+                       '/(?P<type>class|interface|trait)\s+([\\\\\w]+\\\)?(?P<name>\w+)\s*/' => function($match) {
+                               return $match["type"] . " " . $match["name"] . " ";
+                       },
+                       '/(?P<op>extends|implements)\s+(?P<names>[\\w]+(?:(?:,\s*[\\\\\w]+)*))/' => function ($match) {
+                               return $match["op"] . " " . preg_replace('/\b(?<!\\\)(\w)/', '\\\\\\1', $match["names"]);
+                       }
+               ], $this->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+
+                       (?<name>\w+)
+                       (?:\s*=\s*(?P<value>.+))?
+                       (?P<desc>(?:\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<modifiers>\w+\s+)*
+                       (?P<type>[\\\\\w]+)\s+
+                       (?<name>\$\w+)
+                       (?:\s*=\s*(?P<defval>.+))?
+                       (?P<desc>(?:\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<modifiers>\w+\s+)*
+                       (?P<type>[\\\\\w]+)\s+
+                       (?<name>\$\w+)
+                       (?:\s*=\s*(?P<defval>.+))?
+                       (?P<desc>(?:\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+
+                       (?<type>[\\\\\w]+)
+                       \s*,?\s*
+                       (?P<desc>(?:.|\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<exception>[\\\\\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();
+       }
+}
index 78d95617242e2c7a33bfda605984cc207a3dcd04..0a25061bf639283f30e4a619b6d8add5cf7de838 100644 (file)
                        <a href="<?= $esc($entry->getName()) ?>"
                        ><?= $esc($entry->getTitle()) ?></a></h2>
                <div><?= $quick($entry->getIntro()) ?></div>
+                       <?php if ($repo->hasStub($stub)) : ?>
+                               <div>
+                                       <p><strong>Download the Stub file:</strong></p>
+                                       <ul style="list-style-type: '&raquo;'">
+                                               <li>
+                                                       <a href="stub?ref=<?= $entry->getName() ?>"><?= $entry->getName() ?>.stub.php</a><br>
+                                                       <small>
+                                                               Last modified:
+                                                               <?= date_create("@".filemtime($stub))->format("Y-m-d H:i:s") ?>
+                                                       </small>
+                                               </li>
+                                       </ul>
+                               </div>
+                       <?php endif; ?>
                <?php endforeach; ?>
        <?php endforeach; ?>
 <?php endif; ?>