diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php
new file mode 100644
index 000000000..a83cbdec5
--- /dev/null
+++ b/modules/doc/application/controllers/IndexController.php
@@ -0,0 +1,15 @@
+_forward('index', 'view');
+ }
+}
+// @codingStandardsIgnoreEnd
diff --git a/modules/doc/application/controllers/ViewController.php b/modules/doc/application/controllers/ViewController.php
new file mode 100644
index 000000000..b2174872d
--- /dev/null
+++ b/modules/doc/application/controllers/ViewController.php
@@ -0,0 +1,52 @@
+_helper->viewRenderer->setRender('view');
+ }
+
+ /**
+ * Populate view
+ *
+ * @param string $dir
+ */
+ private function populateView($dir)
+ {
+ $parser = new DocParser();
+ list($html, $toc) = $parser->parseDirectory($dir);
+ $this->view->html = $html;
+ $this->view->toc = $toc;
+ }
+
+ public function indexAction()
+ {
+ $this->populateView(Icinga::app()->getApplicationDir('/../doc'));
+ }
+
+ /**
+ * Provide run-time dispatching of module documentation
+ *
+ * @param string $methodName
+ * @param array $args
+ */
+ public function __call($methodName, $args)
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ $moduleName = substr($methodName, 0, -6); // Strip 'Action' suffix
+ if ($moduleManager->hasEnabled($moduleName)) {
+ $this->populateView($moduleManager->getModuleDir($moduleName, '/doc'));
+ } else {
+ parent::__call($methodName, $args);
+ }
+ }
+}
+// @codingStandardsIgnoreEnd
diff --git a/modules/doc/application/views/scripts/view/view.phtml b/modules/doc/application/views/scripts/view/view.phtml
new file mode 100644
index 000000000..c9b7cb46b
--- /dev/null
+++ b/modules/doc/application/views/scripts/view/view.phtml
@@ -0,0 +1,10 @@
+
+= $html ?>
diff --git a/modules/doc/library/Doc/MarkdownFileIterator.php b/modules/doc/library/Doc/MarkdownFileIterator.php
new file mode 100644
index 000000000..cbf23ac35
--- /dev/null
+++ b/modules/doc/library/Doc/MarkdownFileIterator.php
@@ -0,0 +1,27 @@
+getInnerIterator()->current();
+ if (!$current->isFile()) {
+ return false;
+ }
+ $filename = $current->getFilename();
+ $sfx = substr($filename, -3);
+ return $sfx === false ? false : strtolower($sfx) === '.md';
+ }
+}
diff --git a/modules/doc/library/Doc/Parser.php b/modules/doc/library/Doc/Parser.php
new file mode 100644
index 000000000..c96934f7c
--- /dev/null
+++ b/modules/doc/library/Doc/Parser.php
@@ -0,0 +1,93 @@
+openFile();
+ } catch (RuntimeException $e) {
+ throw new Exception($e->getMessage());
+ }
+ if ($fileObject->flock(LOCK_SH) === false) {
+ throw new Exception('Couldn\'t get the lock');
+ }
+ while (!$fileObject->eof()) {
+ $line = $fileObject->fgets();
+ if ($line &&
+ $line[0] === '#' &&
+ preg_match('/^#+/', $line, $match) === 1
+ ) {
+ // Atx-style
+ $level = strlen($match[0]);
+ $heading = trim(strip_tags(substr($line, $level)));
+ $fragment = urlencode($heading);
+ $toc[] = array(
+ 'heading' => $heading,
+ 'level' => $level,
+ 'fragment' => $fragment
+ );
+ $line = '' . "\n" . $line;
+ } elseif (
+ $line &&
+ ($line[0] === '=' || $line[0] === '-') &&
+ preg_match('/^[=-]+\s*$/', $line, $match) === 1
+ ) {
+ // Setext
+ if ($match[0][0] === '=') {
+ // H1
+ $level = 1;
+ } else {
+ // H2
+ $level = 2;
+ }
+ $heading = trim(strip_tags(end($cat)));
+ $fragment = urlencode($heading);
+ $toc[] = array(
+ 'heading' => $heading,
+ 'level' => $level,
+ 'fragment' => $fragment
+ );
+ $line = '' . "\n" . $line;
+ }
+ $cat[] = $line;
+ }
+ $fileObject->flock(LOCK_UN);
+ }
+ $html = MarkdownExtra::defaultTransform(implode('', $cat));
+ return array($html, $toc);
+ }
+}