diff --git a/application/views/scripts/layout/menu.phtml b/application/views/scripts/layout/menu.phtml index 84fa6806a..4435ca72b 100644 --- a/application/views/scripts/layout/menu.phtml +++ b/application/views/scripts/layout/menu.phtml @@ -12,7 +12,7 @@ ?>
  • class=""> getUrl()): ?> - + getAttribs() as $attrib => $value): ?> =""> getIcon()) { diff --git a/library/Icinga/Web/MenuItem.php b/library/Icinga/Web/MenuItem.php index 91b063e6f..16fd30548 100644 --- a/library/Icinga/Web/MenuItem.php +++ b/library/Icinga/Web/MenuItem.php @@ -61,6 +61,8 @@ class MenuItem */ private $children = array(); + private $attribs = array(); + /** * Create a new MenuItem @@ -308,6 +310,28 @@ class MenuItem throw new ProgrammingError(sprintf('Trying to get invalid Menu child "%s"', $id)); } + /** + * Set HTML a tag attributes + * + * @param array $attribs + * + * @return self + */ + public function setAttribs(array $attribs) + { + $this->attribs = $attribs; + return $this; + } + + /** + * Get HTML a tag attributes + * + * @return array + */ + public function getAttribs() + { + return $this->attribs; + } /** * Compare children based on priority and title diff --git a/library/Icinga/Web/Url.php b/library/Icinga/Web/Url.php index b6d076f72..a5c56b742 100644 --- a/library/Icinga/Web/Url.php +++ b/library/Icinga/Web/Url.php @@ -358,11 +358,14 @@ class Url /** * Set the url anchor-part * - * @param $anchor The site's anchor string without the '#' + * @param $anchor The site's anchor string without the '#' + * + * @return self */ public function setAnchor($anchor) { $this->anchor = '#' . $anchor; + return $this; } /** diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php index 88834b0e1..9e7a63a95 100644 --- a/modules/doc/application/controllers/IndexController.php +++ b/modules/doc/application/controllers/IndexController.php @@ -3,7 +3,6 @@ // {{{ICINGA_LICENSE_HEADER}}} // {{{ICINGA_LICENSE_HEADER}}} -use Icinga\Application\Icinga; use Icinga\Module\Doc\Controller as DocController; class Doc_IndexController extends DocController @@ -13,7 +12,7 @@ class Doc_IndexController extends DocController */ public function indexAction() { - $this->populateViewFromDocDirectory(Icinga::app()->getApplicationDir('/../doc')); + $this->populateView(); } } // @codingStandardsIgnoreEnd diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php index dfb4ff7f9..5e3636f92 100644 --- a/modules/doc/application/controllers/ModuleController.php +++ b/modules/doc/application/controllers/ModuleController.php @@ -21,9 +21,7 @@ class Doc_ModuleController extends DocController */ public function viewAction() { - $this->populateViewFromDocDirectory( - Icinga::app()->getModuleManager()->getModuleDir($this->getParam('name'), '/doc') - ); + $this->populateView($this->getParam('name')); } /** diff --git a/modules/doc/application/views/scripts/module/view.phtml b/modules/doc/application/views/scripts/module/view.phtml index dc959497e..bcd894e65 100644 --- a/modules/doc/application/views/scripts/module/view.phtml +++ b/modules/doc/application/views/scripts/module/view.phtml @@ -1,5 +1,5 @@ -

    Module is not documented.

    +

    No documentation available.

    diff --git a/modules/doc/library/Doc/Controller.php b/modules/doc/library/Doc/Controller.php index 5a9fb9acb..3f7b1da4e 100644 --- a/modules/doc/library/Doc/Controller.php +++ b/modules/doc/library/Doc/Controller.php @@ -4,26 +4,20 @@ namespace Icinga\Module\Doc; -use Icinga\Module\Doc\DocParser; use Icinga\Web\Controller\ActionController; -use Icinga\Web\Menu; class Controller extends ActionController { /** * Set HTML and toc * - * @param string $dir + * @param string $module */ - protected function populateViewFromDocDirectory($dir) + protected function populateView($module = null) { - if (!@is_dir($dir)) { - $this->view->html = null; - } else { - $parser = new DocParser(); - list($html, $toc) = $parser->parseDirectory($dir); - $this->view->html = $html; - $this->view->toc = $toc; - } + $parser = new DocParser($module); + list($html, $toc) = $parser->getDocumentation(); + $this->view->html = $html; + $this->view->toc = $toc; } } \ No newline at end of file diff --git a/modules/doc/library/Doc/DocException.php b/modules/doc/library/Doc/DocException.php new file mode 100644 index 000000000..cb7134045 --- /dev/null +++ b/modules/doc/library/Doc/DocException.php @@ -0,0 +1,11 @@ +getApplicationDir('/../doc'); + } else { + $mm = Icinga::app()->getModuleManager(); + if (!$mm->hasInstalled($module)) { + throw new DocException('Module is not installed'); + } + if (!$mm->hasEnabled($module)) { + throw new DocException('Module is not enabled'); + } + $dir = $mm->getModuleDir($module, '/doc'); + } + if (!is_dir($dir)) { + throw new DocException('Doc directory does not exist'); + } + $this->dir = $dir; + $this->module = $module; + } + + /** + * Retrieve table of contents and HTML converted from markdown files sorted by filename * * @return array - * @throws Exception + * @throws DocException */ - public function parseDirectory($dir) + public function getDocumentation() { $iter = new RecursiveIteratorIterator( new MarkdownFileIterator( - new RecursiveDirectoryIterator($dir) + new RecursiveDirectoryIterator($this->dir) ) ); $fileInfos = iterator_to_array($iter); natcasesort($fileInfos); $cat = array(); - $toc = new Menu('doc'); - $stack = new SplStack(); + $toc = array((object) array( + 'level' => 0, + 'item' => new Menu('doc') + )); foreach ($fileInfos as $fileInfo) { try { $fileObject = $fileInfo->openFile(); } catch (RuntimeException $e) { - throw new Exception($e->getMessage()); + throw new DocException($e->getMessage()); } if ($fileObject->flock(LOCK_SH) === false) { - throw new Exception('Couldn\'t get the lock'); + throw new DocException('Couldn\'t get the lock'); } + $itemPriority = 1; + $line = null; while (!$fileObject->eof()) { - $line = $fileObject->fgets(); - if ($line && - $line[0] === '#' && - preg_match('/^#+/', $line, $match) === 1 - ) { - // Atx-style - $level = strlen($match[0]); - $heading = str_replace('.', '.', trim(strip_tags(substr($line, $level)))); - $fragment = urlencode($heading); - $line = '' . "\n" . $line; - $stack->rewind(); - while ($stack->valid() && $stack->current()->level >= $level) { - $stack->pop(); - $stack->next(); + // Save last line for setext-style headers + $lastLine = $line; + $line = $fileObject->fgets(); + $header = $this->extractHeader($line, $lastLine); + if ($header !== null) { + list($header, $level) = $header; + $id = $this->extractHeaderId($header); + $attribs = array(); + $this->reduceToc($toc, $level); + if ($id === null) { + $path = array(); + foreach (array_slice($toc, 1) as $entry) { + $path[] = $entry->item->getTitle(); + } + $path[] = $header; + $id = implode('-', $path); + $attribs['rel'] = 'nofollow'; } - $parent = $stack->current(); - if ($parent === null) { - $item = $toc->addChild($heading, array('url' => '#' . $fragment)); - } else { - $item = $parent->item->addChild($heading, array('url' => '#' . $fragment)); - } - $stack->push((object) array( - 'level' => $level, - 'item' => $item - )); - } elseif ( - $line && - ($line[0] === '=' || $line[0] === '-') && - preg_match('/^[=-]+\s*$/', $line, $match) === 1 - ) { - // Setext - if ($match[0][0] === '=') { - // H1 - $level = 1; - } else { - // H - $level = 2; - } - $heading = trim(strip_tags(end($cat))); - $fragment = urlencode($heading); - $line = '' . "\n" . $line; - $stack->rewind(); - while ($stack->valid() && $stack->current()->level >= $level) { - $stack->pop(); - $stack->next(); - } - $parent = $stack->current(); - if ($parent === null) { - $item = $toc->addChild($heading, array('url' => '#' . $fragment)); - } else { - $item = $parent->item->addChild($heading, array('url' => '#' . $fragment)); - } - $stack->push((object) array( + $id = str_replace('.', '.', strip_tags($id)); + $item = end($toc)->item->addChild( + $id, + array( + 'url' => Url::fromPath( + 'doc/module/view', + array( + 'name' => $this->module + ) + )->setAnchor(urlencode($id))->getRelativeUrl(), + 'title' => htmlspecialchars($header), + 'priority' => $itemPriority++, + 'attribs' => $attribs + ) + ); + $toc[] = ((object) array( 'level' => $level, 'item' => $item )); + $line = '' . PHP_EOL . $line; } $cat[] = $line; } $fileObject->flock(LOCK_UN); } $html = Parsedown::instance()->parse(implode('', $cat)); - return array($html, $toc); + return array($html, $toc[0]->item); + } + + /** + * Extract atx- or setext-style headers from the given lines + * + * @param string $line + * @param string $lastLine + * + * @return array|null An array containing the header and the header level or null if there's nothing to extract + */ + protected function extractHeader($line, $lastLine) + { + if (!$line) { + return null; + } + $header = null; + if ($line && + $line[0] === '#' && + preg_match('/^#+/', $line, $match) === 1 + ) { + // Atx-style + $level = strlen($match[0]); + $header = trim(substr($line, $level)); + if (!$header) { + return null; + } + } elseif ( + $line && + ($line[0] === '=' || $line[0] === '-') && + preg_match('/^[=-]+\s*$/', $line, $match) === 1 + ) { + // Setext + $header = trim($lastLine); + if (!$header) { + return null; + } + if ($match[0][0] === '=') { + $level = 1; + } else { + $level = 2; + } + } + if ($header === null) { + return null; + } + return array($header, $level); + } + + /** + * Extract header id in an a or a span tag + * + * @param string &$header + * + * @return id|null + */ + protected function extractHeaderId(&$header) + { + if ($header[0] === '<' && + preg_match('#(?:<(?Pa|span) id="(?P.+)">)#u', $header, $match) + ) { + $header = str_replace($match[0], '', $header); + return $match['id']; + } + return null; + } + + /** + * Reduce the toc to the given level + * + * @param array &$toc + * @param int $level + */ + protected function reduceToc(array &$toc, $level) { + while (end($toc)->level >= $level) { + array_pop($toc); + } } } diff --git a/modules/monitoring/doc/instances.md b/modules/monitoring/doc/instances.md index 2cb1eebc9..dd04ea30c 100644 --- a/modules/monitoring/doc/instances.md +++ b/modules/monitoring/doc/instances.md @@ -1,4 +1,4 @@ -# The instance.ini configuration file +# The instance.ini configuration file ## Abstract