re-implement all the things
authorMichael Wallner <mike@php.net>
Wed, 8 Oct 2014 09:34:42 +0000 (11:34 +0200)
committerMichael Wallner <mike@php.net>
Wed, 8 Oct 2014 09:34:42 +0000 (11:34 +0200)
21 files changed:
.gitignore
VERSION
bin/cli-server
mdref/Action.php
mdref/Entry.php [new file with mode: 0644]
mdref/ExceptionHandler.php
mdref/File.php [new file with mode: 0644]
mdref/Finder.php [deleted file]
mdref/Markdown.php [deleted file]
mdref/Path.php [deleted file]
mdref/RefEntry.php [deleted file]
mdref/RefListing.php [deleted file]
mdref/Reference.php [new file with mode: 0644]
mdref/Repo.php [new file with mode: 0644]
mdref/Tree.php [new file with mode: 0644]
public/index.css
public/index.php
views/index.phtml
views/layout.phtml
views/mdref.phtml
views/sidebar.phtml

index 030f4e0e6e9fe4ac8c7067e656cfb60f041a8aa4..bc34c0b98d1ac55243cc4bfb045dae9258012596 100644 (file)
@@ -1,3 +1,4 @@
 /nbproject/
 /composer.phar
 /vendor/
+/refs/
diff --git a/VERSION b/VERSION
index a1631314f082896603c2248d1af4ae00e2f7a779..3eefcb9dd5b38e2c1dc061052455dd97bcd51e6c 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.1.0alpha
+1.0.0
index 15b4b55939fea41497b6436e11b4d7c271df8e96..05d700b8137ec7e1a7b67c177712c39494677787 100755 (executable)
@@ -35,8 +35,11 @@ then
        printf "\t          list of those (optional, multiple)\n\n"
        printf "Environment:\n"
        printf "\tREFPATH   colon separated list of refpaths\n\n"
-       printf "At least one refpath must be given, either through the environment "
-       printf "with REFPATH or as command line argument.\n\n"
+       printf "Examples:\n"
+       printf "\t\$ REFPATH=refs/foo:refs/bar ./bin/cli-server\n\n"
+       printf "\t\$ ./bin/cli-server refs/*\n\n"
+       printf "\tAt least one refpath must be given, either through the environment\n"
+       printf "\twith REFPATH or as command line argument.\n\n"
        exit 1
 fi
 
index d85e300fed5fc36663505d57943372efe4cd0959..dafc8e55f493346e5f324fd9592bba7b2c69ce1f 100644 (file)
@@ -5,89 +5,145 @@ namespace mdref;
 use http\Controller\Observer;
 
 /**
- * The sole action controller of mdref
+ * Request handler
  */
-class Action extends Observer
-{
-       private function serveReference(\http\Url $url, \stdClass $payload) {
-               $finder = new Finder($this->baseUrl, REFS);
-               $path = $finder->find($url);
-               $payload->listing = new RefListing($path, 
-                               $finder->glob($path, "/[:_a-zA-Z]*.md"));
-               $payload->title = $payload->listing->getSelf()->formatLink();
-               $payload->refs = $finder;
-               if ($path->isFile()) {
-                       $payload->html = new Markdown($path);
-                       $payload->sublisting = new RefListing($path, 
-                                       $finder->glob($path, "/[_a-z]*.md"));
-                       return true;
-               }
-       }
+class Action extends Observer {
+       /**
+        * Reference paths
+        * @var string
+        */
+       protected $refpath;
        
-       private function serveInternal(\http\Url $url, \stdClass $payload) {
-               $finder = new Finder($this->baseUrl, ROOT);
-               $path = $finder->find($url, "");
-               if ($path->isFile("")) {
-                       $payload->html = $path->toHtml();
-                       return true;
-               }
+       /**
+        * The reference
+        * @var \mdref\Reference
+        */
+       private $reference;
+       
+       /**
+        * Initialize the reference
+        */
+       protected function init() {
+               $this->reference = new Reference(explode(PATH_SEPARATOR, $this->refpath));
        }
        
-       private function getType($file) {
-               static $inf = null;
-               static $typ = array(".css" => "text/css", ".js" => "applicatin/javascript");
+       /**
+        * Create the view payload
+        * @param \http\Controller $ctl
+        * @return \stdClass
+        */
+       private function createPayload(\http\Controller $ctl) {
+               $pld = new \stdClass;
                
-               $ext = strrchr($file, ".");
-               if (isset($typ[$ext])) {
-                       return $typ[$ext];
+               try {
+                       $pld->quick = function($string) {
+                               $md = \MarkdownDocument::createFromString($string);
+                               $md->compile(\MarkdownDocument::AUTOLINK);
+                               return $md->getHtml();
+                       };
+                       
+                       $pld->file = function($file) {
+                               $fd = fopen($file, "r");
+                               $md = \MarkdownDocument::createFromStream($fd);
+                               $md->compile(\MarkdownDocument::AUTOLINK | \MarkdownDocument::TOC);
+                               $html = $md->getHtml();
+                               fclose($fd);
+                               return $html;
+                       };
+                       
+                       $pld->ref = implode("/",  $this->baseUrl->params(
+                               $this->baseUrl->mod($ctl->getRequest()->getRequestUrl())));
+                       
+                       $pld->refs = $this->reference;
+                       $pld->baseUrl = $this->baseUrl;
+                       
+               } catch (\Exception $e) {
+                       $pld->exception = $e;
                }
                
-               if (!$inf) {
-                       $inf = new \FINFO(FILEINFO_MIME_TYPE);
-               }
-               return $inf->file($file);
+               return $pld;
+       }
+       
+       /**
+        * Redirect to canononical url
+        * @param \http\Controller $ctl
+        * @param string $cnn
+        */
+       private function serveCanonical($ctl, $cnn) {
+               $ctl->detachAll(Observer\View::class);
+               $ctl->getResponse()->setHeader("Location", $this->baseUrl->mod($cnn));
+               $ctl->getResponse()->setResponseCode(301);
+       }
+       
+       /**
+        * Serve index.css
+        * @param \http\Controller $ctl
+        */
+       private function serveStylesheet($ctl) {
+               $ctl->detachAll(Observer\View::class);
+               $ctl->getResponse()->setHeader("Content-Type", "text/css");
+               $ctl->getResponse()->setBody(new \http\Message\Body(fopen(ROOT."/public/index.css", "r")));
        }
        
-       private function servePublic(\http\Url $url, \http\Env\Response $res) {
-               $finder = new Finder($this->baseUrl, ROOT."/public");
-               $path = $finder->find($url, "");
-               if ($path->isFile("")) {
-                       $res->setHeader("Content-Type", $this->getType($path->getFullPath("")));
-                       $res->setBody(new \http\Message\Body(fopen($path->getFullPath(""),"r")));
-                       return true;
+       /**
+        * Serve index.js
+        * @param \http\Controller $ctl
+        */
+       private function serveJavascript($ctl) {
+               $ctl->detachAll(Observer\View::class);
+               $ctl->getResponse()->setHeader("Content-Type", "application/javascript");
+               $ctl->getResponse()->setBody(new \http\Message\Body(fopen(ROOT."/public/index.js", "r")));
+       }
+       
+       /**
+        * Serve a preset
+        * @param \http\Controller $ctl
+        * @param \stdClass $pld
+        * @throws \http\Controller\Exception
+        */
+       private function servePreset($ctl, $pld) {
+               switch ($pld->ref) {
+               case "AUTHORS":
+               case "LICENSE":
+               case "VERSION":
+                       $pld->text = file_get_contents(ROOT."/$pld->ref");
+                       break;
+               case "index.css":
+                       $this->serveStylesheet($ctl);
+                       break;
+               case "index.js":
+                       $this->serveJavascript($ctl);
+                       break;
+               default:
+                       throw new \http\Controller\Exception(404, "$pld->ref not found");
                }
        }
-
+       
        /**
-        * Implements \SplObserver
-        * @param \SplSubject $ctl
+        * Implements Observer
+        * @param \SplSubject $ctl \http\Controller
         */
-       function update(\SplSubject $ctl) {
-               /* @var \http\Controller $ctl */
-               try {
-                       $pld = new \stdClass;
-                       $ctl[Observer\View::class] = function() use($pld) {
-                               return $pld;
-                       };
-                       
-                       $pld->baseUrl = $this->baseUrl;
-                       $url = $this->baseUrl->mod($ctl->getRequest()->getRequestUrl());
-                       $pld->permUrl = implode("/", $this->baseUrl->params($url));
-                       if ($this->serveReference($url, $pld) || $this->serveInternal($url, $pld)) {
-                               return;
-                       } elseif ($this->servePublic($url, $ctl->getResponse())) {
-                               $ctl->detachAll(Observer\View::class);
-                               return;
-                       }
-                       
-                       /* fallthrough */
-                       if (strcmp($url->path, $this->baseUrl->path)) {
-                               throw new \http\Controller\Exception(404, "Could not find '$url'");
-                       }
-               } catch (\Exception $exception) {
-                       $ctl[Observer\View::class] = function() use($exception) {
-                                       return compact("exception");
-                       };
+       public function update(\SplSubject $ctl) {
+               /* @var http\Controller $ctl */
+               $pld = $this->createPayload($ctl);
+               $ctl[Observer\View::class] = function() use($pld) {
+                       return $pld;
+               };
+               
+               if (!isset($pld->ref) || !strlen($pld->ref)) {
+                       /* front page */
+                       return;
+               }
+               
+               if (($repo = $this->reference->getRepoForEntry($pld->ref, $cnn))) {
+                       /* direct match */
+                       $pld->entry = $repo->getEntry($pld->ref);
+               } else if (strlen($cnn)) {
+                       /* redirect */
+                       $this->serveCanonical($ctl, $cnn);
+               } else {
+                       $this->servePreset($ctl, $pld);
                }
        }
-}
+
+}
\ No newline at end of file
diff --git a/mdref/Entry.php b/mdref/Entry.php
new file mode 100644 (file)
index 0000000..587cd1e
--- /dev/null
@@ -0,0 +1,254 @@
+<?php
+
+namespace mdref;
+
+/**
+ * A single reference entry
+ */
+class Entry implements \IteratorAggregate {
+       /**
+        * Compound name
+        * @var string
+        */
+       private $name;
+       
+       /**
+        * Split name
+        * @var array
+        */
+       private $list;
+       
+       /**
+        * The containing repository
+        * @var \mdref\Repo
+        */
+       private $repo;
+       
+       /**
+        * The file path, if the refentry exists
+        * @var type 
+        */
+       private $path;
+       
+       /**
+        * The file instance of this entry
+        * @var \mdref\File
+        */
+       private $file;
+       
+       /**
+        * @param string $name the compound name of the ref entry, e.g. "pq/Connection/exec"
+        * @param \mdref\Repo $repo the containing repository
+        */
+       public function __construct($name, Repo $repo) {
+               $this->repo = $repo;
+               $this->name = $name;
+               $this->list = explode("/", $name);
+               $this->path = $repo->hasEntry($name);
+       }
+       
+       /**
+        * Get the compound name, e.g. "pq/Connection/exec"
+        * @return string
+        */
+       public function getName() {
+               return $this->name;
+       }
+       
+       /**
+        * Get the containing repository
+        * @return \mdref\Repo
+        */
+       public function getRepo() {
+               return $this->repo;
+       }
+       
+       /**
+        * Get the file path, if any
+        * @return string
+        */
+       public function getPath() {
+               return $this->path;
+       }
+       
+       /**
+        * Get the file instance of this entry
+        * @return \mdref\File
+        */
+       public function getFile() {
+               if (!$this->file) {
+                       $this->file = new File($this->path);
+               }
+               return $this->file;
+       }
+       
+       /**
+        * Read the title of the ref entry file
+        * @return string
+        */
+       public function getTitle() {
+               if ($this->isFile()) {
+                       return $this->getFile()->readTitle();
+               }
+               if ($this->isRoot()) {
+                       return $this->repo->getRootEntry()->getTitle();
+               }
+               return $this->name;
+       }
+       
+       /**
+        * Read the description of the ref entry file
+        * @return string
+        */
+       public function getDescription() {
+               if ($this->isFile()) {
+                       return $this->getFile()->readDescription();
+               }
+               if ($this->isRoot()) {
+                       return $this->repo->getRootEntry()->getDescription();
+               }
+               return $this;
+       }
+       
+       /**
+        * Read the intriductory section of the refentry file
+        * @return string
+        */
+       public function getIntro() {
+               if ($this->isFile()) {
+                       return $this->getFile()->readIntro();
+               }
+               if ($this->isRoot()) {
+                       return $this->repo->getRootEntry()->getIntro();
+               }
+               return "";
+       }
+       
+       /**
+        * Check if the refentry exists
+        * @return bool
+        */
+       public function isFile() {
+               return strlen($this->path) > 0;
+       }
+       
+       /**
+        * Check if this is the first entry of the reference tree
+        * @return bool
+        */
+       public function isRoot() {
+               return count($this->list) === 1;
+       }
+       
+       /**
+        * Get the parent ref entry
+        * @return \mdref\Entry
+        */
+       public function getParent() {
+               if ("." !== ($dirn = dirname($this->name))) {
+                       return $this->repo->getEntry($dirn);
+               }
+       }
+       
+       /**
+        * Get the list of parents up-down
+        * @return array
+        */
+       public function getParents() {
+               $parents = array();
+               for ($parent = $this->getParent(); $parent; $parent = $parent->getParent()) {
+                       array_unshift($parents, $parent);
+               }
+               return $parents;
+       }
+       
+       /**
+        * Guess whether this ref entry is about a function or method
+        * @return bool
+        */
+       public function isFunction() {
+               $base = end($this->list);
+               return $base{0} === "_" || ctype_lower($base{0});
+       }
+       
+       /**
+        * Guess whether this ref entry is about a namespace, interface or class
+        * @return bool
+        */
+       public function isNsClass() {
+               $base = end($this->list);
+               return ctype_upper($base{0});
+       }
+       
+       /**
+        * Display name
+        * @return string
+        */
+       public function __toString() {
+               $parts = explode("/", trim($this->getName(), "/"));
+               $myself = array_pop($parts);
+               if (!$parts) {
+                       return $myself;
+               }
+               $parent = end($parts);
+               
+               switch ($myself{0}) {
+               case ":":
+                       return "★" . substr($myself, 1);
+                       
+               default:
+                       if (!ctype_lower($myself{0}) || ctype_lower($parent{0})) {
+                               return $myself;
+                       }
+               case "_":
+                       return $parent . "::" . $myself;
+               }
+       }
+       
+       /**
+        * Get the base name of this ref entry
+        * @return string
+        */
+       public function getBasename() {
+               return dirname($this->path) . "/" . basename($this->path, ".md");
+       }
+       
+       /**
+        * Guess whether there are any child nodes
+        * @param string $glob
+        * @return boolean
+        */
+       function hasIterator($glob = null) {
+               if (strlen($glob)) {
+                       return glob($this->getBasename() . "/$glob");
+               } elseif ($this->isRoot()) {
+                       return true;
+               } else {
+                       return is_dir($this->getBasename());
+               }
+       }
+       
+       /**
+        * Guess whether there are namespace/interface/class child nodes
+        * @return bool
+        */
+       function hasNsClasses() {
+               return $this->hasIterator("/[A-Z]*.md");
+       }
+       
+       /**
+        * Guess whether there are function/method child nodes
+        * @return bool
+        */
+       function hasFunctions() {
+               return $this->hasIterator("/[a-z_]*.md");
+       }
+       
+       /**
+        * Implements \IteratorAggregate
+        * @return \mdref\Tree child nodes
+        */
+       function getIterator() {
+               return new Tree($this->getBasename(), $this->repo, $this->isRoot());
+       }
+}
index 1c9b7da8a60474c09f9f505e06013e1a38a4b63b..13e7f18fbea7bd513878d2d475666cb8e7152fc8 100644 (file)
@@ -5,26 +5,50 @@ namespace mdref;
 use http\Env as HTTP;
 
 /**
- * mdref exception handler
+ * Exception and error handler
  */
 class ExceptionHandler
 {
-       function __construct() {
+       /**
+        * Set up error/exception/shutdown handler
+        */
+       public function __construct() {
                set_exception_handler($this);
                set_error_handler($this);
+               register_shutdown_function($this);
        }
        
-       function __invoke($e, $msg = null) {
+       /**
+        * The exception/error/shutdown handler callback
+        */
+       public function __invoke($e = null, $msg = null) {
                if ($e instanceof \Exception) {
                        try {
-                               echo static::html($e);
+                               echo static::htmlException($e);
                        } catch (\Exception $ignore) {
                                headers_sent() or HTTP::setResponseCode(500);
+                               die("FATAL ERROR");
                        }
-               } else {
+               } elseif (isset($e, $msg)) {
                        throw new \Exception($msg, $e);
+               } elseif (($error = error_get_last())) {
+                       switch ($error["type"]) {
+                       case E_PARSE:
+                       case E_ERROR:
+                       case E_USER_ERROR:
+                       case E_CORE_ERROR:
+                       case E_COMPILE_ERROR:
+                               while (ob_get_level()) {
+                                       if (!@ob_end_clean()) {
+                                               break;
+                                       }
+                               }
+                               $message = sprintf("%s in %s at line %d", 
+                                       $error["message"], $error["file"], $error["line"]);
+                               echo static::htmlError("Application Error", $message, 500, "");
+                               break;
+                       }
                }
-               return true;
        }
        
        /**
@@ -35,7 +59,7 @@ class ExceptionHandler
         * @param array $trace_tag
         * @return string
         */
-       static function html(\Exception $e, array $title_tag = ["h1"], array $message_tag = ["p"], array $trace_tag = ["pre", "style='font-size:smaller'"]) {
+       public static function htmlException(\Exception $e, array $title_tag = ["h1"], array $message_tag = ["p"], array $trace_tag = ["pre", "style='font-size:smaller;overflow-x:scroll'"]) {
                if ($e instanceof \http\Controller\Exception) {
                        $code = $e->getCode() ?: 500;
                        foreach ($e->getHeaders() as $key => $val) {
@@ -44,15 +68,44 @@ class ExceptionHandler
                } else {
                        $code = 500;
                }
+               
+               for ($html = ""; $e; $e = $e->getPrevious()) {
+                       $html .= static::htmlError(HTTP::getResponseStatusForCode($code),
+                               $e->getMessage(), $code, $e->getTraceAsString(), 
+                               $title_tag, $message_tag, $trace_tag);
+               }
+               return $html;
+       }
+       
+       /**
+        * Format an error as HTML
+        * @param string $title
+        * @param string $message
+        * @param int $code
+        * @param string $trace
+        * @param array $title_tag
+        * @param array $message_tag
+        * @param array $trace_tag
+        * @return string
+        */
+       public static function htmlError($title, $message, $code, $trace = null, array $title_tag = ["h1"], array $message_tag = ["p"], array $trace_tag = ["pre", "style='font-size:smaller;overflow-x:scroll'"]) {
                HTTP::setResponseCode($code);
-               $name = HTTP::getResponseStatusForCode($code);
+               
                $html = sprintf("<%s>%s</%s>\n<%s>%s</%s>\n",
-                               implode(" ", $title_tag), $name, $title_tag[0],
-                               implode(" ", $message_tag), $e->getMessage(), $message_tag[0]);
+                               implode(" ", $title_tag), $title, $title_tag[0],
+                               implode(" ", $message_tag), $message, $message_tag[0]);
                if ($trace_tag) {
-                       $html .= sprintf("<%s>%s</%s>\n",
-                                       implode(" ", $trace_tag), $e->getTraceAsString(), $trace_tag[0]);
+                       if (!isset($trace)) {
+                               ob_start();
+                               debug_print_backtrace();
+                               $trace = ob_get_clean();
+                       }
+                       if (!empty($trace)) {
+                               $html .= sprintf("<%s>%s</%s>\n",
+                                               implode(" ", $trace_tag), $trace, $trace_tag[0]);
+                       }
                }
+               
                return $html;
        }
 }
diff --git a/mdref/File.php b/mdref/File.php
new file mode 100644 (file)
index 0000000..0ac3da2
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+
+namespace mdref;
+
+/**
+ * A ref entry file
+ */
+class File {
+       /**
+        * @var resource
+        */
+       private $fd;
+       
+       /**
+        * Open the file
+        * @param string $path
+        */
+       public function __construct($path) {
+               $this->fd = fopen($path, "rb");
+       }
+       
+       /**
+        * Read the title of the refentry
+        * @return string
+        */
+       public function readTitle() {
+               if (0 === fseek($this->fd, 1, SEEK_SET)) {
+                       return fgets($this->fd);
+               }
+       }
+       
+       /**
+        * Read the description of the refentry
+        * @return string
+        */
+       public function readDescription() {
+               if (0 === fseek($this->fd, 0, SEEK_SET)
+               && (false !== fgets($this->fd))
+               && (false !== fgets($this->fd))) {
+                       return fgets($this->fd);
+               }
+       }
+       
+       /**
+        * Read the first subsection of a global refentry
+        * @return string
+        */
+       public function readIntro() {
+               $intro = "";
+               if (0 === fseek($this->fd, 0, SEEK_SET)) {
+                       $header = false;
+                       
+                       while (!feof($this->fd)) {
+                               if (false === ($line = fgets($this->fd))) {
+                                       break;
+                               }
+                               /* search first header and read until next header*/
+                               if ("## " === substr($line, 0, 3)) {
+                                       if ($header) {
+                                               break;
+                                       } else {
+                                               $header = true;
+                                               continue;
+                                       }
+                               }
+                               if ($header) {
+                                       $intro .= $line;
+                               }
+                       }
+               }
+               return $intro;
+       }
+}
diff --git a/mdref/Finder.php b/mdref/Finder.php
deleted file mode 100644 (file)
index e3734e3..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-namespace mdref;
-
-/**
- * Find markdown reference files in several REFPATH paths. 
- * 
- * The base URL is used to extract the relative identifier out of the request 
- * url in Finder::find().
- * 
- * Use the created Path of Finder::find() for Finder::glob() to find subrefs.
- */
-class Finder
-{
-       /**
-        * Base URL
-        * @var \http\Controller\Url 
-        */
-       protected $baseUrl;
-       
-       /**
-        * Reference paths
-        * @var array
-        */
-       protected $refs = array();
-       
-       /**
-        * @param \http\Controller\Url $baseUrl
-        * @param mixed $paths array or string of paths with markdown references
-        */
-       function __construct(\http\Controller\Url $baseUrl, $paths = ".") {
-               if (!is_array($paths)) {
-                       $paths = explode(PATH_SEPARATOR, $paths);
-               }
-               $this->refs = $paths;
-               $this->baseUrl = $baseUrl;
-       }
-       
-       /**
-        * @return \http\Controller\Url
-        */
-       function getBaseUrl() {
-               return $this->baseUrl;
-       }
-       
-       /**
-        * Find a markdown reference file in one REFPATH. If nothing could be found
-        * an empty Path will be returned.
-        * 
-        * @param \http\Url $requestUrl
-        * @return Path
-        */
-       function find(\http\Url $requestUrl, $ext = ".md") {
-               $file = implode(DIRECTORY_SEPARATOR, $this->baseUrl->params($requestUrl));
-               
-               foreach ($this->refs as $base) {
-                       $path = new Path($base, $file);
-                       if ($path->isFile($ext)) {
-                               return $path;
-                       }
-               }
-               
-               return new Path;
-       }
-
-       /**
-        * Glob either in a Path's base dir, or, if the path does not have a base 
-        * dir set, in each REFPATH paths.
-        * 
-        * @param \mdref\Path $path
-        * @param string $pattern glob pattern
-        * @param int $flags glob flags
-        * @return array glob result
-        */
-       function glob(Path $path, $pattern, $flags = GLOB_BRACE) {
-               if (strlen($path->getBaseDir())) {
-                       $glob = glob($path->getFullPath($pattern), $flags) ?: array();
-               } else {
-                       $glob = array();
-                       foreach ($this->refs as $ref) {
-                               $glob = array_merge($glob, array_map(function ($fn) use ($ref) {
-                                       return substr($fn, strlen($ref));
-                               }, glob($ref . $pattern, $flags)));
-                       }
-               }
-               sort($glob, SORT_STRING|SORT_FLAG_CASE);
-               return $glob;
-       }
-}
diff --git a/mdref/Markdown.php b/mdref/Markdown.php
deleted file mode 100644 (file)
index e8c5cb6..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-namespace mdref;
-
-class Markdown
-{
-       /**
-        * @var \mdref\Path
-        */
-       protected $path;
-       
-       /**
-        * @param \mdref\Path $path
-        */
-       function __construct(Path $path = null) {
-               $this->path = $path;
-       }
-       
-       /**
-        * @return string
-        */
-       function __toString() {
-               if (!$this->path) {
-                       return "";
-               }
-               try {
-                       $r = fopen($this->path->getFullPath(".md"), "r");
-                       $md = \MarkdownDocument::createFromStream($r);
-                       $md->compile(\MarkdownDocument::AUTOLINK | \MarkdownDocument::TOC);
-                       $html = $md->getHtml();
-                       fclose($r);
-               } catch (\Exception $e) {
-                       $html = ExceptionHandler::html($e);
-               }
-               return $html;
-       }
-
-       function quick($string) {
-               $md = \MarkdownDocument::createFromString($string);
-               $md->compile(\MarkdownDocument::AUTOLINK);
-               return $md->getHtml();
-       }
-}
diff --git a/mdref/Path.php b/mdref/Path.php
deleted file mode 100644 (file)
index e0d00c7..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-<?php
-
-namespace mdref;
-
-/**
- * A path made out of a base dir and an thereof relative path name.
- */
-class Path
-{
-       /**
-        * Computed path
-        * @var string
-        */
-       protected $path = "";
-       
-       /**
-        * The base directory where path is located
-        * @var string
-        */
-       protected $baseDir = "";
-       
-       /**
-        * @param string $baseDir
-        * @param string $path
-        */
-       function __construct($baseDir = "", $path = "") {
-               $this->baseDir = $baseDir;
-               $this->path = $path;
-       }
-       
-       /**
-        * Create a copy of this path with a different path name
-        * 
-        * @param string $path
-        * @return \mdref\Path
-        */
-       function __invoke($path) {
-               $that = clone $this;
-               $that->path = $path;
-               return $that;
-       }
-       
-       /**
-        * Retrurns the full path as string
-        * @return string
-        */
-       function __toString() {
-               return $this->getFullPath();
-       }
-
-       /**
-        * The base directory
-        * @return string
-        */
-       function getBaseDir() {
-               return $this->baseDir;
-       }
-       
-       /**
-        * The path name relative to the base dir
-        * @return string
-        */
-       function getPathName() {
-               return $this->path;
-       }
-       
-       /**
-        * The full path
-        * @param string $ext extension
-        * @return string
-        */
-       function getFullPath($ext = "") {
-               $path = "";
-               if (strlen($this->baseDir)) {
-                       $path .= $this->baseDir . DIRECTORY_SEPARATOR;
-               }
-               if (strlen($this->path)) {
-                       $path .= $this->path;
-               }
-               $path .= $ext;
-               return $path;
-       }
-       
-       /**
-        * Retrieve a another subpath within the base dir
-        * @param type $path
-        * @return string
-        */
-       function getSubPath($path) {
-               return trim(substr($path, strlen($this->baseDir)), DIRECTORY_SEPARATOR);
-       }
-       
-       function isFile($ext = ".md") {
-               return is_file($this->getFullPath($ext));
-       }
-       
-       function toHtml() {
-               $head = sprintf("<h1>%s</h1>\n", htmlspecialchars(basename($this->getPathName())));
-               if ($this->isFile()) {
-                       $html = htmlspecialchars(file_get_contents($this->getFullPath()));
-               } elseif ($this->isFile("")) {
-                       $html = htmlspecialchars(file_get_contents($this->getFullPath("")));
-               } else {
-                       throw new \http\Controller\Exception(404, "Not Found: {$this->getPathName()}");
-               }
-               return $head . "<pre>" . $html ."</pre>"; 
-       }
-}
diff --git a/mdref/RefEntry.php b/mdref/RefEntry.php
deleted file mode 100644 (file)
index 6c54674..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-<?php
-
-namespace mdref;
-
-/**
- * The RefEntry class represents a reference entry, i.e. a .md file
- */
-class RefEntry
-{
-       /**
-        * @var \mdref\Path
-        */
-       protected $path;
-       
-       /**
-        * @var string
-        */
-       protected $entry;
-       
-       /**
-        * @var resource
-        */
-       protected $file;
-       
-       /**
-        * @param \mdref\Path $path
-        * @param type $entry
-        */
-       function __construct(Path $path, $entry = null) {
-               $this->path = $path;
-               $this->entry = trim($entry ?: $path->getPathName(), DIRECTORY_SEPARATOR);
-       }
-       
-       /**
-        * Clean up the file handle
-        */
-       function __destruct() {
-               if (is_resource($this->file)) {
-                       fclose($this->file);
-               }
-       }
-       
-       /**
-        * Format as URL
-        * @return string
-        */
-       function formatUrl() {
-               return htmlspecialchars($this->entry);
-       }
-       
-       private function joinLink(array $parts) {
-               $link = "";
-               $upper = ctype_upper($parts[0][0]);;
-               for ($i = 0; $i < count($parts); ++$i) {
-                       if (!strlen($parts[$i]) || $parts[$i] === ".") {
-                               continue;
-                       }
-                       if (strlen($link)) {
-                               if ($parts[$i][0] === ":") {
-                                       $link = "";
-                               } elseif ($upper && !ctype_upper($parts[$i][0])) {
-                                       $link .= "::";
-                               } else {
-                                       $link .= "\\";
-                               }
-                       }
-                       $link .= trim($parts[$i], ": ");
-                       $upper = ctype_upper($parts[$i][0]);
-               }
-               return $link;
-       }
-
-       /**
-        * Format as link text
-        * @param bool $basename whether to use the basename only
-        * @return string
-        */
-       function formatLink($basename = false) {
-               $link = "";
-               if (strlen($this->entry)) {
-                       $parts = explode(DIRECTORY_SEPARATOR, $this->entry);
-                       $link = $basename ? end($parts) : $this->joinLink($parts);
-               }
-               return htmlspecialchars($link);
-       }
-       
-       /**
-        * Create a consolidated Path of this entry
-        * @return \mdref\Path
-        */
-       function getPath() {
-               $path = $this->path;
-               $file = $path($this->entry);
-               return $file;
-       }
-       
-       private function openFile() {
-               if (!is_resource($this->file)) {
-                       $file = $this->getPath();
-                       
-                       if (!$file->isFile()) {
-                               throw new \Exception("Not a file: '{$file}'");
-                       }
-                       if (!$this->file = fopen($file->getFullPath(".md"), "r")) {
-                               throw new \Exception("Could not open {$file}");
-                       }
-               }
-       }
-       
-       /**
-        * Read the title of the refentry
-        * @return string
-        */
-       function readTitle() {
-               $this->openFile();
-               fseek($this->file, 1, SEEK_SET);
-               return fgets($this->file);
-       }
-       
-       /**
-        * Read the description of the refentry
-        * @return string
-        */
-       function readDescription() {
-               $this->openFile();
-               fseek($this->file, 0, SEEK_SET);
-               fgets($this->file);
-               fgets($this->file);
-               return fgets($this->file);
-       }
-       
-       /**
-        * Format a "Edit me" URL. The project reference top directory needs a 
-        * »name«.mdref file besides its »name«.md entry point with the edit URL
-        * printf template as content. The sole printf argument is the relative 
-        * path of the entry.
-        * @return string
-        */
-       function formatEditUrl() {
-               $path = $this->path;
-               $base = current(explode(DIRECTORY_SEPARATOR, $path->getPathName()));
-               $file = $path($base);
-               if ($file->isFile(".mdref")) {
-                       return sprintf(file_get_contents($file->getFullPath(".mdref")),
-                                       $this->entry);
-               }
-       }
-       
-       /**
-        * Recurse into the reference tree
-        * @param \mdref\Finder $refs
-        * @param string $pattern
-        * @param callable $cb
-        */
-       function recurse(Finder $refs, $pattern, callable $cb) {
-               $path = $refs->find($refs->getBaseUrl()->mod($this->entry));
-               foreach (new RefListing($path, $refs->glob($path, $pattern)) as $entry) {
-                       /* @var $entry RefEntry */
-                       $cb($entry, $pattern, function($entry, $pattern) use ($refs, $cb) {
-                               $entry->recurse($refs, $pattern, $cb);
-                       });
-               }
-       }
-}
diff --git a/mdref/RefListing.php b/mdref/RefListing.php
deleted file mode 100644 (file)
index 4c3b629..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-
-namespace mdref;
-
-/**
- * A list of markdown reference files
- */
-class RefListing implements \Countable, \Iterator
-{
-       /**
-        * @var \mdref\Path
-        */
-       protected $path;
-       
-       /**
-        * @var array
-        */
-       protected $entries;
-       
-       /**
-        * @param \mdref\Path $path
-        * @param array $files
-        */
-       function __construct(Path $path, array $files) {
-               $this->path = $path;
-               $this->entries = array_map(function($fn) {
-                       return substr(trim($fn, DIRECTORY_SEPARATOR), 0, -3);
-               }, $files);
-       }
-       
-       /**
-        * Implements \Countable
-        * @return int
-        */
-       function count() {
-               return count($this->entries);
-       }
-       
-       /**
-        * Implements \Iterator
-        */
-       function rewind() {
-               reset($this->entries);
-       }
-       
-       /**
-        * Implements \Iterator
-        * @return bool
-        */
-       function valid() {
-               return null !== key($this->entries);
-       }
-       
-       /**
-        * Implements \Iterator
-        * @return string
-        */
-       function key() {
-               return $this->path->getSubPath(current($this->entries));
-       }
-       
-       /**
-        * Implements \Iterator
-        */
-       function next() {
-               next($this->entries);
-       }
-
-       /**
-        * Implements \Iterator
-        * @return \mdref\RefEntry
-        */
-       function current() {
-               return new RefEntry($this->path, $this->key());
-       }
-       
-       /**
-        * Get the parent reference entry
-        * @return null|\mdref\RefEntry
-        */
-       function getParent() {
-               switch ($parent = dirname($this->path->getPathName())) {
-                       case ".":
-                       case "":
-                               return null;
-                       default:
-                               return new RefEntry($this->path, $parent);
-               }
-       }
-       
-       /**
-        * Get the reference entry this reflist is based of
-        * @return \mdref\RefEntry
-        */
-       function getSelf() {
-               return new RefEntry($this->path);
-       }
-}
diff --git a/mdref/Reference.php b/mdref/Reference.php
new file mode 100644 (file)
index 0000000..153004f
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+namespace mdref;
+
+/**
+ * The complete available reference
+ */
+class Reference implements \IteratorAggregate {
+       /**
+        * List of mdref repositories
+        * @var array
+        */
+       private $repos = array();
+       
+       /**
+        * @param array $refs list of mdref repository paths
+        */
+       public function __construct(array $refs) {
+               foreach ($refs as $path) {
+                       $repo = new Repo($path);
+                       $this->repos[$repo->getName()] = $repo;
+               }
+       }
+       
+       /**
+        * Lookup the repo containing a ref entry
+        * @param string $entry requested reference entry, e.g. "pq/Connection/exec"
+        * @param type $canonical
+        * @return \mdref\Repo|NULL
+        */
+       public function getRepoForEntry($entry, &$canonical = null) {
+               foreach ($this->repos as $repo) {
+                       if ($repo->hasEntry($entry, $canonical)) {
+                               return $repo;
+                       }
+               }
+       }
+       
+       /**
+        * Implements \IteratorAggregate
+        * @return \ArrayIterator repository list
+        */
+       public function getIterator() {
+               return new \ArrayIterator($this->repos);
+       }
+       
+}
diff --git a/mdref/Repo.php b/mdref/Repo.php
new file mode 100644 (file)
index 0000000..f59a82e
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+
+namespace mdref;
+
+
+/**
+ * A reference repo
+ */
+class Repo implements \IteratorAggregate {
+       /**
+        * The name of the repository
+        * @var string
+        */
+       private $name;
+       
+       /**
+        * The path to the repository
+        * @var string
+        */
+       private $path;
+       
+       /**
+        * The edit url template
+        * @var string
+        */
+       private $edit;
+       
+       /**
+        * Path to the repository containing the name.mdref file
+        * @param string $path
+        * @throws \InvalidArgumentException
+        */
+       public function __construct($path) {
+               if (!($mdref = current(glob("$path/*.mdref")))) {
+                       throw new \InvalidArgumentException(
+                               sprintf("Not a reference, could not find '*.mdref': '%s'",
+                                       $path));
+               }
+               
+               $this->path = realpath($path);
+               $this->name = basename($mdref, ".mdref");
+               $this->edit = trim(file_get_contents($mdref));
+       }
+       
+       /**
+        * Get the repository's name
+        * @return string
+        */
+       public function getName() {
+               return $this->name;
+       }
+       
+       /**
+        * Get the path of the repository or a file in it
+        * @param string $file
+        * @return string
+        */
+       public function getPath($file = "") {
+               return $this->path . "/$file";
+       }
+       
+       /**
+        * Get the edit url for a ref entry
+        * @param string $entry
+        * @return string
+        */
+       public function getEditUrl($entry) {
+               return sprintf($this->edit, $entry);
+       }
+       
+       /**
+        * Get the file path of an entry in this repo
+        * @param string $entry
+        * @return string file path
+        */
+       public function hasEntry($entry, &$canonical = null) {
+               $file = $this->getPath("$entry.md");
+               if (is_file($file)) { 
+                       return $file;
+               }
+               $file = $this->getPath($this->getName()."/$entry.md");
+               if (is_file($file)) {
+                       $canonical = $this->getName() . "/" . $entry;
+                       return $file;
+               }
+       }
+       
+       /**
+        * Get the canonical entry name of a file in this repo
+        * @param string $file
+        * @return string entry
+        */
+       public function hasFile($file) {
+               if (($file = realpath($file))) {
+                       $path = $this->getPath();
+                       $plen = strlen($path);
+                       if (!strncmp($file, $path, $plen)) {
+                               $dirname = dirname(substr($file, $plen));
+                               $basename = basename($file, ".md");
+                               
+                               if ($dirname === ".") {
+                                       return $basename;
+                               }
+                               
+                               return  $dirname . "/". $basename;
+                       }
+               }
+       }
+       
+       /**
+        * Get an Entry instance
+        * @param string $entry
+        * @return \mdref\Entry
+        * @throws \OutOfBoundsException
+        */
+       public function getEntry($entry) {
+               return new Entry($entry, $this);
+       }
+       
+       /**
+        * Get the root Entry instance
+        * @return \mdref\Entry
+        */
+       public function getRootEntry() {
+               return new Entry($this->name, $this);
+       }
+       
+       /**
+        * Implements \IteratorAggregate
+        * @return \mdref\Tree
+        */
+       public function getIterator() {
+               return new Tree($this->path, $this);
+       }
+}
diff --git a/mdref/Tree.php b/mdref/Tree.php
new file mode 100644 (file)
index 0000000..d9e2b0d
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+
+namespace mdref;
+
+class Tree implements \RecursiveIterator {
+       /**
+        * The repository
+        * @var \mdref\Repo
+        */
+       private $repo;
+       
+       /**
+        * List of first level entries
+        * @var array
+        */
+       private $list;
+       
+       /**
+        * The list iterator
+        * @var array
+        */
+       private $iter;
+       
+       /**
+        * @param string $path
+        * @param \mdref\Repo $repo
+        */
+       public function __construct($path, Repo $repo) {
+               if (!($list = glob("$path/*.md"))) {
+                       $list = glob("$path/*/*.md");
+               }
+               $this->list = array_filter($list, $this->generateFilter($list));
+               sort($this->list, SORT_STRING);
+               $this->repo = $repo;
+       }
+       
+       /**
+        * @param array $list
+        * @return callable
+        */
+       private function generateFilter(array $list) {
+               return function($v) use($list) {
+                       if ($v{0} === ".") {
+                               return false;
+                       }
+                       if (false !== array_search("$v.md", $list, true)) {
+                               return false;
+                       }
+                       
+                       $pi = pathinfo($v);
+                       if (isset($pi["extension"]) && "md" !== $pi["extension"]) {
+                               return false;
+                       }
+                       
+                       return true;
+               };
+       }
+
+       /**
+        * Implements \Iterator
+        * @return \mdref\Entry
+        */
+       public function current() {
+               return $this->repo->getEntry($this->repo->hasFile(current($this->iter)));
+       }
+       
+       /**
+        * Implements \Iterator
+        */
+       public function next() {
+               next($this->iter);
+       }
+       
+       /**
+        * Implements \Iterator
+        * @return int
+        */
+       public function key() {
+               return key($this->iter);
+       }
+       
+       /**
+        * Implements \Iterator
+        */
+       public function rewind() {
+               $this->iter = $this->list;
+               reset($this->iter);
+       }
+       
+       /**
+        * Implements \Iterator
+        * @return bool
+        */
+       public function valid() {
+               return null !== key($this->iter);
+       }
+       
+       /**
+        * Implements \RecursiveIterator
+        * @return bool
+        */
+       public function hasChildren() {
+               return $this->current()->hasIterator();
+       }
+       
+       /**
+        * Implements \RecursiveIterator
+        * @return \mdref\Tree
+        */
+       public function getChildren() {
+               return $this->current()->getIterator();
+       }
+}
index 0fcd08830234638c3c58a99be73560122d31c85a..6fc3a88ff536786d885165248f026665c53e1ccf 100644 (file)
@@ -16,7 +16,7 @@ body {
 body>* {
        margin-left: 1em;
 }
-body>ul {
+body>ul, body>div>ul {
        margin-left: 2em;
 }
 
@@ -77,7 +77,7 @@ pre>code, pre>code code {
        color: #eee;
 }
 
-p, pre {
+p, pre, table {
        margin: 1em 2em 2em 2em;
 }
 
@@ -104,6 +104,9 @@ a, h1 code>a {
 a:hover {
        text-decoration: none;
 }
+a[href^="http:"]:after, a[href^="https:"]:after {
+       content: " ⬈";
+}
 
 .var {
        color: #800000;
@@ -139,7 +142,7 @@ li h3 {
        margin: .5em 0 0 0;
 }
 
-body>h3 {
+body>h3, body>div>h3 {
        margin-left: 2em;
 }
 
index 23a1be2cf1a8595337669918a3438a80f4ef27fe..1e6d37f83bcf26f45822e31c68db7065f5d65b45 100644 (file)
@@ -20,7 +20,7 @@ new ExceptionHandler;
 
 $ctl = new Controller;
 $ctl->setDependency("baseUrl", new Url)
-       ->attach(new Action)
+       ->attach(new Action(["refpath" => REFS]))
        ->attach(new Layout)
        ->notify()
        ->getResponse()
index 417419d4d3c5ad0d247384445235dd2c6ec78291..6b4f2b1dd65fa4f153285ae53c5d88ba79347add 100644 (file)
@@ -1,25 +1,16 @@
 <h1>mdref</h1>
 
-<?php if (isset($exception)) : ?>
-       <?=\mdref\ExceptionHandler::html($exception, ["h2"], ["p"], ["pre", "style='overflow-x:scroll'"]); ?>
-<?php else : ?>
-       <?php if (isset($listing) && count($listing)) : ?>
-               <h2>Available References</h2>
-               <?php foreach ($listing as $entry) : ?>
-                       <h3><a href="<?=$entry->formatUrl()?>"><?=$entry->formatLink()?></a></h3>
-                       <?php $entry->recurse($refs, "/*.md", function($entry, $pattern, callable $recursor) { ?>
-                       <ul>
-                               <li><p><a href="<?=$entry->formatUrl()?>"><?=$entry->formatLink()?></a></p>
-                                       <?php
-                                               if (!isset($html)) {
-                                                       $html = new \mdref\Markdown;
-                                               }
-                                               echo $html->quick($entry->readDescription());
-                                               $recursor($entry, "/[A-Z]*.md");
-                                       ?>
-                               </li>
-                       </ul>
-                       <?php }); ?>
+<?php if (isset($html)) : ?>
+       <?= $html ?>
+<?php elseif (isset($text)) : ?>
+       <p style="white-space:pre-wrap"><?= $view->esc($text) ?></p>
+<?php elseif (isset($refs)) : ?>
+       <?php foreach ($refs as $repo) : /* @var \mdref\Repo $repo */ ?>
+               <?php foreach ($repo as $entry) : /* @var \mdref\Entry $entry */ ?>
+               <h2>
+                       <a href="<?= $view->esc($entry->getName()) ?>"
+                       ><?= $view->esc($entry->getTitle()) ?></a></h2>
+               <div><?= $quick($entry->getIntro()) ?></div>
                <?php endforeach; ?>
-       <?php endif; ?>
-<?php endif; ?>
+       <?php endforeach; ?>
+<?php endif; ?>
\ No newline at end of file
index 6ef8686ad87315b52694b7e11fb7d5c563830909..6b18f856574c1146d560f06a90e374dac1cbe214 100644 (file)
@@ -9,13 +9,15 @@
                        mdref
                </title>
                <base href="<?= $baseUrl ?>">
+               <meta http-equiv="Content-Location" content="<?= $baseUrl . $ref ?>">
                <link rel="stylesheet" href="index.css">
                <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        </head>
        <body>
                <?php include __DIR__."/sidebar.phtml" ?>
-               
-               <?php if (isset($html)) : ?>
+               <?php if (isset($exception)) : ?>
+                       <?= \mdref\ExceptionHandler::htmlException($exception) ?>
+               <?php elseif (isset($entry)) : ?>
                        <?php include __DIR__."/mdref.phtml" ?>
                <?php else: ?>
                        <?php include __DIR__."/index.phtml" ?>
@@ -25,7 +27,7 @@
                <div id="disqus_thread"></div>
                <script>
                        var disqus_shortname = 'mdref';
-                       var disqus_identifier = '<?=$permUrl?>';
+                       var disqus_identifier = '<?=$ref?>';
                        (function() {
                                var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
                                dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
index 08df2e11a33e4a0b5abaf2654a541c38d53362b0..e50a008227bee5d9e440f7c70ed96783dc2618da 100644 (file)
@@ -1,14 +1,27 @@
-<?= $html ?>
+<?= $file($entry->getPath()) ?>
 
-<?php if (isset($sublisting) && count($sublisting)) : ?>
-       <h2>Functions:</h2>
-       <ul>
-       <?php foreach($sublisting as $entry) : ?>
-               <li>
-                       <h3><a href="<?=$entry->formatUrl()?>"><?=$entry->formatLink(true)?></a></h3>
-                       <p><?=$html->quick($entry->readDescription())?></p>
-                       <p><?=$view->esc($entry->readTitle())?></p>
-               </li>
+<?php if ($entry->hasFunctions()) : ?>
+<h2>Functions:</h2>
+<ul>
+       <?php foreach($entry as $sub) : if (!$sub->isFunction()) continue; ?>
+       <li>
+               <h3><a href="<?= $view->esc($sub->getName()) ?>"><?= $view->esc($sub) ?></a></h3>
+               <p><?= $quick($sub->getDescription()) ?></p>
+               <p><?= $view->esc($sub->getTitle()) ?></p>
+       </li>
        <?php endforeach; ?>
-       </ul>
+</ul>
+<?php endif; ?>
+
+<?php if ($entry->hasNsClasses()) : ?>
+<h2>Namespaces, Interfaces and Classes:</h2>
+<ul>
+       <?php foreach ($entry as $sub) : if (!$sub->isNsClass()) continue; ?>
+       <li>
+               <h3><a href="<?= $view->esc($sub->getName()) ?>"><?= $view->esc($sub) ?></a></h3>
+               <p><?= $quick($sub->getDescription()) ?></p>
+               <p><?= $view->esc($sub->getTitle()) ?></p>
+       </li>
+       <?php endforeach; ?>
+</ul>
 <?php endif; ?>
index f6dccb455be7bc86d2bd7b769a2b2da5b2e41bc9..125ec0b87adc71fe156332077cf8bedc78be24ee 100644 (file)
@@ -1,24 +1,46 @@
-<?php if (isset($listing)) : ?>
 <div class="sidebar">
-       <?php include __DIR__."/edit.phtml" ?>
        <ul>
-               <li>&lsh; <a href="">Home</a></li>
-               <?php if (($entry = $listing->getParent())) : ?>
-                       <li>&uarr; <a href="<?=$entry->formatUrl()?>"><?=$entry->formatLink()?></a></li>
-               <?php endif; ?>
-               <?php if (($entry = $listing->getSelf()) && ($link = $entry->formatLink())) : ?>
-                       <ul><li>&circlearrowright; <?= $link ?>
-               <?php endif; ?>
-               <?php if (count($listing)) : ?>
+               <li>&lsh; <a href="">Home</a>
+                       <?php if (isset($entry)) : /* @var \mdref\Entry $entry */ ?>
                        <ul>
-                               <?php foreach ($listing as $entry) : ?>
-                               <li>&rdsh; <a href="<?=$entry->formatUrl()?>"><?=$entry->formatLink()?></a></li>
+                               <li>
+                                       <?php foreach ($entry->getParents() as $parent) if ($parent->isFile()) : ?>
+                                       &uarr; 
+                                               <a href="<?= $view->esc($parent->getName()) ?>">
+                                                       <?= $view->esc($entry->getRepo()->getEntry($parent)) ?>
+                                               </a>
+                                               <ul>
+                                                       <li>
+                                       <?php endif; ?>
+                                                       &circlearrowright; <a href="<?= $view->esc($entry->getName()) ?>"
+                                                       ><?= $view->esc($entry) ?></a>
+                                                       <ul>
+                                                               <?php foreach ($entry as $sub) : /* @var \mdref\Entry $sub */ ?>
+                                                               <li>
+                                                                       &rdsh; <a href="<?= $view->esc($sub->getName()) ?>"
+                                                                       ><?= $view->esc($sub) ?></a>
+                                                               </li>
+                                                               <?php endforeach; ?>
+                                                       </ul>
+                                       <?php foreach ($entry->getParents() as $parent) if ($parent->isFile()) : ?>
+                                               </li>
+                                       </ul>
+                                       <?php endif; ?>
+                               </li>
+                       </ul>
+                       <?php elseif (isset($refs)) : ?>
+                       <ul>
+                               <?php foreach ($refs as $repo) : /* @var \mdref\Repo $repo */ ?>
+                                       <?php foreach ($repo as $sub) : /* @var \mdref\Entry $entry */ ?>
+                                               <li>
+                                                       &rdsh; <a href="<?= $view->esc($sub->getName()) ?>"
+                                                       ><?= $view->esc($sub->getTitle()) ?></a>
+                                               </li>
+                                       <?php endforeach; ?>
                                <?php endforeach; ?>
                        </ul>
-               <?php endif; ?>
-               <?php if (isset($link) && strlen($link)) : ?>
-                       </li></ul>
-               <?php endif; ?>
+                       <?php endif; ?>
+               </li>
        </ul>
 </div>
-<?php endif; ?>
+