--- /dev/null
+#!/usr/bin/env php
+<?php
+
+namespace mdref;
+
+require_once __DIR__."/../vendor/autoload.php";
+
+if ($argc < 3) {
+ fprintf(STDERR, "Usage: cd ref-<ns> && %s <ns> <stub.php>\n", $argv[0]);
+ exit(1);
+}
+
+$namespace = $argv[1];
+require_once $argv[2];
+
+if (!file_exists("$namespace.mdref")) {
+ fprintf(STDERR, "Missing $namespace.mdref; generated default.\n");
+ file_put_contents("$namespace.mdref", "./%s");
+}
+if (!file_exists("$namespace.md")) {
+ fprintf(STDERR, "Missing $namespace.md; hard linking README.md\n");
+ link(dirname($argv[2]) . "/README.md", "$namespace.md");
+}
+
+$inspector = new Inspector;
+$inspector->inspectNamespace($namespace);
+
+$generator = new Generator;
+$generator->generateFunctions($inspector->getFunctions());
+$generator->generateClasses($inspector->getClasses());
}
],
"bin": [
+ "bin/ext2ref",
+ "bin/ref2html",
"bin/ref2stub",
- "bin/ref2html"
+ "bin/stub2ref"
],
"autoload": {
"psr-4": {
"ext-filter": "*",
"ext-pcre": "*",
"ext-http": "^3.2 || ^4.2",
- "league/commonmark": "^2.0"
+ "league/commonmark": "^2.0",
+ "phpdocumentor/reflection-docblock": "^5.3"
},
"suggests": {
"ext-discount": "A Markdown renderer is needed, this is the preferred one."
include ROOT."/views/layout.phtml";
$this->response->addHeader("Link", "<" . $this->baseUrl->path . "index.css>; rel=preload; as=style");
$this->response->addHeader("Link", "<" . $this->baseUrl->path . "index.js>; rel=preload; as=script");
+ if (isset($exception) && $exception->getCode()) {
+ $this->response->setResponseCode($exception->getCode());
+ }
if (is_resource($this->output)) {
$this->response->send($this->output);
} else {
try {
$pld = $this->createPayload();
- if (strlen($pld->ref)) {
+ if (isset($pld->ref) && strlen($pld->ref)) {
$cnn = null;
if (($repo = $this->reference->getRepoForEntry($pld->ref, $cnn))) {
- if (strlen($cnn)) {
+ if (isset($cnn) && strlen($cnn)) {
/* redirect */
$this->serveCanonical($cnn);
return;
* @return bool
*/
function hasIterator(?string $glob = null, bool $loose = false) : bool {
- if (strlen($glob)) {
+ if (isset($glob) && strlen($glob)) {
return glob($this->getNextDirname() . "/$glob") ||
($loose && glob($this->getNextDirname() . "/*/$glob"));
} elseif ($this->isRoot()) {
--- /dev/null
+<?php
+
+namespace mdref;
+
+use mdref\Generator\{Cls, Func};
+
+class Generator {
+ /**
+ * @param array<string, array<string, \ReflectionFunctionAbstract>> $functions
+ * @return void
+ */
+ public function generateFunctions(array $functions) : void {
+ foreach ($functions as $ns => $funcs) {
+ $ns_path = strtr($ns, "\\", "/");
+ foreach ($funcs as $fn => $rf) {
+ $fn_file = "$ns_path/$fn.md";
+ fprintf(STDERR, "Generating %s\n", $fn_file);
+ is_dir($ns_path) || mkdir($ns_path, 0770, true);
+ file_put_contents($fn_file, new Func($this, $rf));
+ }
+ }
+ }
+
+ /**
+ * @param array<string, array<string, \ReflectionClass>> $classes
+ * @return void
+ */
+ public function generateClasses(array $classes) : void {
+ foreach ($classes as $ns => $cls) {
+ $ns_path = strtr($ns, "\\", "/");
+ foreach ($cls as $cn => $rc) {
+ $cn_path = "$ns_path/$cn";
+ $cn_file = "$cn_path.md";
+ fprintf(STDERR, "Generating %s\n", $cn_file);
+ is_dir($ns_path) || mkdir($ns_path, 0770, true);
+ file_put_contents($cn_file, new Cls($this, $rc));
+ $this->generateMethods($rc);
+ }
+ }
+ }
+
+ private function generateMethods(\ReflectionClass $rc) : void {
+ $funcs = [];
+ foreach ($rc->getMethods(\ReflectionMethod::IS_PUBLIC) as $rm) {
+ if ($rm->getDeclaringClass()->getName() === $rc->getName()) {
+ foreach ($rc->getInterfaces() as $ri) {
+ if ($ri->hasMethod($rm->getName())) {
+ continue 2;
+ }
+ }
+ $funcs[$rc->getName()][$rm->getName()] = $rm;
+ }
+ }
+ $this->generateFunctions($funcs);
+ }
+}
--- /dev/null
+<?php
+
+namespace mdref\Generator;
+
+use mdref\Generator;
+use phpDocumentor\Reflection\{DocBlock, DocBlock\Tags};
+
+class Arg extends Scrap {
+ public function __toString() : string {
+ return parent::toString(__FILE__, __COMPILER_HALT_OFFSET__, [
+ "tag" => $this->getParamTag($this->ref->getName())
+ ]);
+ }
+
+}
+
+/** @var $gen Generator */
+/** @var $ref \ReflectionParameter */
+/** @var $doc ?DocBlock */
+/** @var $tag ?Tags\Param */
+
+__HALT_COMPILER();
+<?= $ref->hasType() ? $ref->getType() : str_replace("\\ref", "", $tag?->getType() ?? "mixed")
+?> <?php
+if ($ref->isVariadic()) : ?>
+ ?>...<?php
+endif;
+if ($ref->isPassedByReference()) :
+ ?>&<?php
+endif;
+?>$<?=$ref->getName()
+?><?php
+if ($ref->isDefaultValueAvailable()) :
+ ?> = <?php var_export($ref->getDefaultValue()) ?><?php
+endif;
--- /dev/null
+<?php
+
+namespace mdref\Generator;
+
+use mdref\Generator;
+use phpDocumentor\Reflection\DocBlock;
+
+class Cls extends Scrap {
+ public function __toString() :string {
+ return parent::toString(__FILE__, __COMPILER_HALT_OFFSET__);
+ }
+}
+
+/** @var $gen Generator */
+/** @var $ref \ReflectionClass */
+/** @var $doc DocBlock */
+/** @var $patch callable as function(string, \Reflector) */
+
+__HALT_COMPILER();
+# <?php
+if ($ref instanceof \ReflectionEnum) :
+ ?>enum<?php
+else :
+ ?><?= implode(" ", \Reflection::getModifierNames($ref->getModifiers()));
+ ?> <?= $ref->isInterface() ? "interface" : "class"
+ ?><?php
+endif;
+
+?> <?= $ref->getName() ?><?php
+
+if (($parent = $ref->getParentClass())) :
+ ?> extends <?= $parent->getName() ?><?php
+endif;
+if (($implements = $ref->getInterfaceNames())) : sort($implements);
+ ?> implements <?= implode(", ", $implements); ?><?php
+endif;
+?>
+
+
+<?= $doc?->getSummary() ?>
+
+
+<?= $doc?->getDescription() ?>
+
+
+<?php $patch(SeeAlso::class, $ref) ?>
+
+
+
+## Constants:
+
+<?php
+if (!($consts = array_filter($ref->getReflectionConstants(), fn($rc) => $rc->getDeclaringClass()->getName() === $ref->getName()))) :
+ ?>None.<?php
+else:
+ /** @var \ReflectionClassConstant $rc */
+ foreach ($consts as $rc) :
+ ?> * <span class="constant"><?= $rc->getName();
+ ?></span> = <span><?php
+ if ($rc->getValue() instanceof \UnitEnum) :
+ var_export($rc->getValue()->value);
+ else :
+ var_export($rc->getValue());
+ endif;
+ ?><?= "</span>\n"
+ ?><?php
+ endforeach;
+endif;
+?>
+
+
+## Properties:
+
+<?php
+if (!($props = array_filter($ref->getProperties(), fn($rp) => $rp->getDeclaringClass()->getName() === $ref->getName()))) :
+ ?>None.<?php
+else:
+ foreach ($props as $rp) :
+ ?> * <?php
+ $patch(Prop::class, $rp);
+ endforeach;
+endif;
+?>
+
+<?php
--- /dev/null
+<?php
+
+namespace mdref\Generator;
+
+use mdref\Generator;
+use phpDocumentor\Reflection\{DocBlock, DocBlock\Tags, DocBlockFactory};
+
+class Func extends Scrap {
+ public function __toString() : string {
+ return parent::toString(__FILE__, __COMPILER_HALT_OFFSET__);
+ }
+}
+
+/** @var $gen Generator */
+/** @var $ref \ReflectionFunctionAbstract */
+/** @var $doc DocBlock */
+/** @var $patch callable as function(string, \Reflector) */
+
+__HALT_COMPILER();
+# <?php
+if ($ref instanceof \ReflectionMethod) :
+ if ($ref->isFinal()) :
+ ?>final <?php
+ endif;
+ if ($ref->isStatic()) :
+ ?>static <?php
+ endif;
+endif;
+?><?= $ref->hasReturnType() ? $ref->getReturnType() : "void"
+?> <?php
+if ($ref instanceof \ReflectionMethod) :
+ ?><?=$ref->getDeclaringClass()->getName()
+ ?>::<?php
+endif;
+?><?= $ref->getName() ?>
+(<?php
+ $optional = 0;
+ foreach ($ref->getParameters() as $i => $param) :
+ if ($param->isOptional()) : $optional++
+ ?>[<?php
+ endif;
+ $patch(Arg::class, $param);
+ if ($i < $ref->getNumberOfParameters()-1):
+ ?>, <?php
+ endif;
+ endforeach;
+ echo str_repeat("]", $optional);
+?>
+)
+
+<?= $doc?->getSummary() ?>
+
+
+<?= $doc?->getDescription()?->getBodyTemplate() ?>
+
+
+<?php $patch(SeeAlso::class, $ref) ?>
+
+
+
+## Params:
+
+<?php
+if (!($params = $ref->getParameters())) :
+ ?>None.<?php
+else :
+ foreach ($params as $i => $param) :
+ $patch(Param::class, $param);
+ endforeach;
+endif;
+
+if (($tags = $doc?->getTagsWithTypeByName("return")) || ($ref->hasReturnType() && $ref->hasReturnType() != "void")) :
+?>
+
+
+## Returns:
+
+<?php
+ if ($tags) :
+ foreach ($tags as $tag) :
+ ?>* <?= $tag->getType()
+ ?>, <?= $tag->getDescription()
+ ?><?="\n"
+ ?><?php
+ endforeach;
+ else :
+ ?>* <?= $ref->getReturnType()
+ ?><?php
+ endif;
+endif;
+?>
+
+
+<?php
--- /dev/null
+<?php
+
+namespace mdref\Generator;
+
+use mdref\Generator;
+use phpDocumentor\Reflection\{DocBlock, DocBlock\Tags};
+
+
+class Param extends Scrap {
+ public function __toString() : string {
+ return parent::toString(__FILE__, __COMPILER_HALT_OFFSET__, [
+ "tag" => $this->getParamTag($this->ref->getName())
+ ]);
+ }
+}
+
+/** @var $gen Generator */
+/** @var $ref \ReflectionParameter */
+/** @var $doc ?DocBlock */
+/** @var $tag ?Tags\Param */
+
+__HALT_COMPILER();
+* <?php
+if ($ref->isOptional()) :
+ ?>Optional <?php
+endif;
+?><?= $ref->hasType() ? $ref->getType() : str_replace("\\ref", "Reference",$tag?->getType() ?? "mixed")
+?> <?php
+if ($ref->isVariadic()) : ?>
+ ?>...<?php
+endif;
+if ($ref->isPassedByReference()) :
+ ?>&<?php
+endif;
+?>$<?= $ref->getName()
+?><?php
+if ($ref->isDefaultValueAvailable()) :
+ ?> = <?php var_export($ref->getDefaultValue()) ?><?php
+endif;
+
+if (($desc = $tag?->getDescription() ?? $doc?->getSummary())) :
+ ?><?= " \n "
+ ?><?= $desc
+ ?><?php
+endif;
+?>
+
+<?php
--- /dev/null
+<?php
+
+namespace mdref\Generator;
+
+use mdref\Generator;
+use phpDocumentor\Reflection\{DocBlock, DocBlock\Tags};
+
+class Prop extends Scrap {
+ public function __toString() : string {
+ return parent::toString(__FILE__, __COMPILER_HALT_OFFSET__, [
+ "tag" => $this->getVarTag($this->ref->getName())
+ ]);
+ }
+
+}
+
+/** @var $gen Generator */
+/** @var $ref \ReflectionParameter */
+/** @var $doc ?DocBlock */
+/** @var $tag ?Tags\Param */
+
+__HALT_COMPILER();
+<?= implode(" ", \Reflection::getModifierNames($ref->getModifiers()))
+?> <?= $ref->hasType() ? $ref->getType() : ($tag?->getType() ?? "mixed")
+?> $<?=$ref->getName() ?><?php
+if ($ref->hasDefaultValue()) :
+ ?> = <?php var_export($ref->getDefaultValue()) ?><?php
+endif;
+
+if (($desc = $doc?->getSummary())) :
+ ?><?= " \n $desc"
+ ?><?php
+endif;
+
+?><?= "\n"
+?><?php
+
--- /dev/null
+<?php
+
+namespace mdref\Generator;
+
+use mdref\Generator;
+use mdref\Generator\{Arg, Param};
+use phpDocumentor\Reflection\{DocBlock, DocBlockFactory, DocBlock\Tags};
+
+use Reflector;
+
+class Scrap {
+ public function __construct(
+ protected Generator $gen,
+ protected Reflector $ref,
+ protected ?DocBlock $doc = null,
+ bool $overrideDocFromRef = false,
+ ) {
+ if ($overrideDocFromRef || !$this->doc) {
+ $this->createDocBlock();
+ }
+ if (!$this->doc) {
+ fprintf(STDERR, ... match (get_class($ref)) {
+ \ReflectionClass::class => ["Missing docs for class %s\n", $ref->name],
+ \ReflectionMethod::class => ["Missing docs for method %s::%s()\n", $ref->class, $ref->name],
+ \ReflectionProperty::class => ["Missing docs for property %s %s::\$%s\n", $ref->getType(), $ref->class, $ref->name],
+ \ReflectionClassConstant::class => ["Missing docs for constant %s::%s\n", $ref->class, $ref->name],
+ \ReflectionFunction::class => ["Missing docs for function %s()\n", $ref->name],
+ \ReflectionParameter::class => ($ref->getDeclaringClass()
+ ? ["Missing docs for method arg %s::%s(%s $%s)\n", $ref->getDeclaringClass()->name]
+ : ["Missing docs for function arg %s(%s $%s)\n"])
+ + [3=>$ref->getDeclaringFunction()->name, $ref->getType(), $ref->name],
+ default => ["Missing docs for ??? %s\n", $ref->name],
+ });
+ }
+ }
+
+ protected function createDocBlock() {
+ if (method_exists($this->ref, "getDocComment")) {
+ $docs = $this->ref->getDocComment();
+ } elseif (($this->ref instanceof \ReflectionParameter) && $this->ref->getDeclaringClass()?->hasProperty($this->ref->name)) {
+ // ctor promoted properties
+ $docs = $this->ref->getDeclaringClass()->getProperty($this->ref->name)->getDocComment();
+ }
+ if (isset($docs) && $docs !== false && strlen($docs)) {
+ $this->doc = DocBlockFactory::createInstance()->create((string) $docs);
+ }
+ }
+
+ protected function toString(string $file, int $offset, array $imports = []) : string {
+ $tpl = (string) new Template($file, $offset);
+ $patch = function(string $scrap_class, Reflector $ref) : void {
+ echo new $scrap_class($this->gen, $ref, $this->doc, true);
+ };
+ return (static function(Generator $gen, Reflector $ref, ?DocBlock $doc) use($patch) {
+ $imports = func_get_arg(3); extract($imports); unset($imports);
+ ob_start(null, 0x4000);
+ include func_get_arg(4);
+ return ob_get_clean();
+ })($this->gen, $this->ref, $this->doc, $imports, $tpl);
+ }
+
+ protected function getParamTag(string $var_name) : ?Tags\Param {
+ if ($this->doc) foreach ($this->doc->getTagsByName("param") as $param) {
+ if ($param->getVariableName() === $var_name) {
+ return $param;
+ }
+ }
+ return null;
+ }
+
+ protected function getVarTag(string $var_name) : ?Tags\Var_ {
+ if ($this->doc) foreach ($this->doc->getTagsByName("var") as $prop) {
+ if ($prop->getVariableName() === $var_name) {
+ return $prop;
+ }
+ }
+ return null;
+ }
+}
--- /dev/null
+<?php
+
+namespace mdref\Generator;
+use phpDocumentor\Reflection\DocBlock;
+
+
+class SeeAlso extends Scrap {
+ public function __toString() : string {
+ return parent::toString(__FILE__, __COMPILER_HALT_OFFSET__);
+ }
+}
+
+/** @var $doc DocBlock */
+/** @var $patch callable as function(string, \Reflector) */
+
+__HALT_COMPILER();
+<?php
+if (($sees = $doc?->getTagsByName("see"))) :
+ ?>See also <?php
+ foreach ($sees as $i => $see) :
+ /** @var $see DocBlock\Tags\See */
+ if (($desc = $see->getDescription())) :
+ ?>[<?= $see->getDescription() ?>](<?= $see->getReference() ?>)<?php
+ else :
+ ?><?= $see->getReference() ?><?php
+ endif;
+ if ($i < count($sees)) :
+ if ($i === count($sees) - 1) :
+ ?>.<?php
+ else :
+ ?>, <?php
+ endif;
+ if ($i === count($sees) - 2) :
+ ?>and <?php
+ endif;
+ endif;
+ endforeach;
+endif;
+
--- /dev/null
+<?php
+
+namespace mdref\Generator;
+
+use http\Exception\RuntimeException;
+
+class Template {
+ public function __construct(
+ private string $file,
+ private int $offset
+ ) {}
+
+ public function source() {
+ return file_get_contents($this->file, false, null, $this->offset);
+ }
+
+ private function cache(string $cache_file) : string {
+ $cache_path = dirname($cache_file);
+ if (!is_dir($cache_path) && !mkdir($cache_path, 0700, true)) {
+ throw new RuntimeException(error_get_last()["message"]);
+ }
+ if (!file_put_contents($cache_file, '<?php namespace mdref\\Generator; ?>' . $this->source())) {
+ throw new RuntimeException(error_get_last()["message"]);
+ }
+ return $cache_file;
+ }
+
+ public function __toString() : string {
+ $cache_file = sys_get_temp_dir() . "/mdref/generate." . basename($this->file) . ".md.tmp";
+ $cache_stat = @stat($cache_file);
+ if ($cache_stat && $cache_stat["mtime"] >= filemtime($this->file)) {
+ return $cache_file;
+ }
+
+ return $this->cache($cache_file);
+ }
+}
--- /dev/null
+<?php
+
+namespace mdref;
+
+class Inspector {
+ /**
+ * @var array
+ */
+ private $constants = [];
+ /**
+ * @var array
+ */
+ private $classes = [];
+ /**
+ * @var array
+ */
+ private $functions = [];
+
+ public function inspectExtension($ext) {
+ if (!($ext instanceof \ReflectionExtension)) {
+ $ext = new \ReflectionExtension($ext);
+ }
+
+ $this->addClasses($ext->getClasses());
+ $this->addFunctions($ext->getFunctions());
+ $this->addConstants($ext->getConstants());
+ }
+
+ public function inspectNamespace(string $namespace) {
+ $grep = function(array $a) use($namespace) {
+ return preg_grep("/^" . preg_quote($namespace) . "\\\\/", $a);
+ };
+
+ $this->addClasses($this->wrap(\ReflectionClass::class, $grep(get_declared_interfaces())));
+ $this->addClasses($this->wrap(\ReflectionClass::class, array_filter(
+ $grep(get_declared_classes()),
+ fn($cn) => !is_subclass_of($cn, \UnitEnum::class)
+ )));
+ $this->addClasses($this->wrap(\ReflectionEnum::class, array_filter(
+ $grep(get_declared_classes()),
+ fn($cn) => is_subclass_of($cn, \UnitEnum::class)
+ )));
+ $this->addFunctions($this->wrap(\ReflectionFunction::class, $grep(get_defined_functions()["internal"])));
+ $this->addFunctions($this->wrap(\ReflectionFunction::class, $grep(get_defined_functions()["user"])));
+ $this->addConstants($grep(get_defined_constants()));
+ }
+
+ private function wrap(string $klass, array $list) : array {
+ $res = [];
+ foreach ($list as $entry) {
+ $res[] = new $klass($entry);
+ }
+ return $res;
+ }
+
+ private function addClasses(array $classes) {
+ foreach ($classes as $c) {
+ /* @var $c \ReflectionClass */
+ $ns_name = $c->inNamespace() ? $c->getNamespaceName() : "";
+ $this->classes[$ns_name][$c->getShortName()] = $c;
+ }
+ }
+
+ private function addFunctions(array $functions) {
+ foreach ($functions as $f) {
+ /* @var $f \ReflectionFunction */
+ $ns_name = $f->inNamespace() ? $f->getNamespaceName() : "";
+ $this->functions[$ns_name][$f->getShortName()] = $f;
+ }
+ }
+
+ private function addConstants(array $constants) {
+ foreach ($constants as $constant => $value) {
+ $ns_name = ($ns_end = strrpos($constant, "\\")) ? substr($constant, 0, $ns_end++) : "";
+ $cn = substr($constant, $ns_end);
+ $this->constants[$ns_name][$cn] = $value;
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getConstants() : array {
+ return $this->constants;
+ }
+
+ /**
+ * @return array
+ */
+ public function getClasses(): array {
+ return $this->classes;
+ }
+
+ /**
+ * @return array
+ */
+ public function getFunctions(): array {
+ return $this->functions;
+ }
+}
} elseif ($bb[0] === ":") {
return 1;
}
-
+/*
$ad = is_dir(dirname($a)."/$ab");
$bd = is_dir(dirname($b)."/$bb");
} elseif ($bd) {
return 1;
}
-
+*/
$au = preg_match("/^\p{Lu}/", $ab);
$bu = preg_match("/^\p{Lu}/", $bb);
* Implements \RecursiveIterator
* @return \mdref\Tree
*/
- public function getChildren() : Iterator {
+ public function getChildren() : Tree {
return $this->current()->getIterator();
}
}
case "Traversable":
case "Iterator":
case "IteratorAggregate":
+ case "RecursiveIterator":
case "ArrayAccess":
case "ArrayObject":
case "ArrayIterator":
return "<br> <em>";
}
case "class":
+ case "enum":
case "interface":
case "namespace":
case "public":