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.+)">(?P=tag)>)#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