From 6478b415c59070f70ed860f3a592377eef9783b1 Mon Sep 17 00:00:00 2001 From: Michael Wallner Date: Wed, 8 Oct 2014 11:34:42 +0200 Subject: [PATCH] re-implement all the things --- .gitignore | 1 + VERSION | 2 +- bin/cli-server | 7 +- mdref/Action.php | 198 ++++++++++++++++++----------- mdref/Entry.php | 254 +++++++++++++++++++++++++++++++++++++ mdref/ExceptionHandler.php | 77 +++++++++-- mdref/File.php | 73 +++++++++++ mdref/Finder.php | 89 ------------- mdref/Markdown.php | 43 ------- mdref/Path.php | 108 ---------------- mdref/RefEntry.php | 164 ------------------------ mdref/RefListing.php | 98 -------------- mdref/Reference.php | 47 +++++++ mdref/Repo.php | 135 ++++++++++++++++++++ mdref/Tree.php | 113 +++++++++++++++++ public/index.css | 9 +- public/index.php | 2 +- views/index.phtml | 35 ++--- views/layout.phtml | 8 +- views/mdref.phtml | 35 +++-- views/sidebar.phtml | 56 +++++--- 21 files changed, 909 insertions(+), 645 deletions(-) create mode 100644 mdref/Entry.php create mode 100644 mdref/File.php delete mode 100644 mdref/Finder.php delete mode 100644 mdref/Markdown.php delete mode 100644 mdref/Path.php delete mode 100644 mdref/RefEntry.php delete mode 100644 mdref/RefListing.php create mode 100644 mdref/Reference.php create mode 100644 mdref/Repo.php create mode 100644 mdref/Tree.php diff --git a/.gitignore b/.gitignore index 030f4e0..bc34c0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /nbproject/ /composer.phar /vendor/ +/refs/ diff --git a/VERSION b/VERSION index a163131..3eefcb9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0alpha +1.0.0 diff --git a/bin/cli-server b/bin/cli-server index 15b4b55..05d700b 100755 --- a/bin/cli-server +++ b/bin/cli-server @@ -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 diff --git a/mdref/Action.php b/mdref/Action.php index d85e300..dafc8e5 100644 --- a/mdref/Action.php +++ b/mdref/Action.php @@ -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 index 0000000..587cd1e --- /dev/null +++ b/mdref/Entry.php @@ -0,0 +1,254 @@ +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()); + } +} diff --git a/mdref/ExceptionHandler.php b/mdref/ExceptionHandler.php index 1c9b7da..13e7f18 100644 --- a/mdref/ExceptionHandler.php +++ b/mdref/ExceptionHandler.php @@ -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\n<%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\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\n", + implode(" ", $trace_tag), $trace, $trace_tag[0]); + } } + return $html; } } diff --git a/mdref/File.php b/mdref/File.php new file mode 100644 index 0000000..0ac3da2 --- /dev/null +++ b/mdref/File.php @@ -0,0 +1,73 @@ +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 index e3734e3..0000000 --- a/mdref/Finder.php +++ /dev/null @@ -1,89 +0,0 @@ -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 index e8c5cb6..0000000 --- a/mdref/Markdown.php +++ /dev/null @@ -1,43 +0,0 @@ -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 index e0d00c7..0000000 --- a/mdref/Path.php +++ /dev/null @@ -1,108 +0,0 @@ -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("

%s

\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 . "
" . $html ."
"; - } -} diff --git a/mdref/RefEntry.php b/mdref/RefEntry.php deleted file mode 100644 index 6c54674..0000000 --- a/mdref/RefEntry.php +++ /dev/null @@ -1,164 +0,0 @@ -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 index 4c3b629..0000000 --- a/mdref/RefListing.php +++ /dev/null @@ -1,98 +0,0 @@ -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 index 0000000..153004f --- /dev/null +++ b/mdref/Reference.php @@ -0,0 +1,47 @@ +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 index 0000000..f59a82e --- /dev/null +++ b/mdref/Repo.php @@ -0,0 +1,135 @@ +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 index 0000000..d9e2b0d --- /dev/null +++ b/mdref/Tree.php @@ -0,0 +1,113 @@ +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(); + } +} diff --git a/public/index.css b/public/index.css index 0fcd088..6fc3a88 100644 --- a/public/index.css +++ b/public/index.css @@ -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; } diff --git a/public/index.php b/public/index.php index 23a1be2..1e6d37f 100644 --- a/public/index.php +++ b/public/index.php @@ -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() diff --git a/views/index.phtml b/views/index.phtml index 417419d..6b4f2b1 100644 --- a/views/index.phtml +++ b/views/index.phtml @@ -1,25 +1,16 @@

mdref

- - - - -

Available References

- -

formatLink()?>

- recurse($refs, "/*.md", function($entry, $pattern, callable $recursor) { ?> - - + + + +

esc($text) ?>

+ + + +

+ esc($entry->getTitle()) ?>

+
getIntro()) ?>
- - + + \ No newline at end of file diff --git a/views/layout.phtml b/views/layout.phtml index 6ef8686..6b18f85 100644 --- a/views/layout.phtml +++ b/views/layout.phtml @@ -9,13 +9,15 @@ mdref + - - + + + @@ -25,7 +27,7 @@