From 6ff5a986dd9818b6ecb81786abe01d29beb28ef3 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 26 May 2014 13:08:47 +0200
Subject: [PATCH 01/96] Modules: Don't call `FrontController::getRoute()' twice

refs #6303
---
 library/Icinga/Application/Modules/Module.php | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php
index c765ca380..e9a8ab583 100644
--- a/library/Icinga/Application/Modules/Module.php
+++ b/library/Icinga/Application/Modules/Module.php
@@ -600,18 +600,19 @@ class Module
      */
     protected function registerRoutes()
     {
-        $this->app->getFrontController()->getRouter()->addRoute(
+        $router = $this->app->getFrontController()->getRouter();
+        $router->addRoute(
             $this->name . '_jsprovider',
             new Route(
                 'js/' . $this->name . '/:file',
                 array(
                     'controller'    => 'static',
                     'action'        =>'javascript',
-                    'module_name'    => $this->name
+                    'module_name'   => $this->name
                 )
             )
         );
-        $this->app->getFrontController()->getRouter()->addRoute(
+        $router->addRoute(
             $this->name . '_img',
             new Route(
                 'img/' . $this->name . '/:file',

From a91961284dd580e283cd1dc7bfa4b0b7e17228fc Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 09:26:53 +0200
Subject: [PATCH 02/96] modules/doc: Show nothing on index yet

Before, doc's IndexController::indexAction() showed the documentation
of Icinga Web 2 but index should rather show an overview of available
docs.

refs #4820
---
 .../controllers/IndexController.php           | 26 +------------------
 .../views/scripts/index/index.phtml           |  5 ----
 2 files changed, 1 insertion(+), 30 deletions(-)

diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php
index 299ae5c65..1911f0534 100644
--- a/modules/doc/application/controllers/IndexController.php
+++ b/modules/doc/application/controllers/IndexController.php
@@ -1,37 +1,13 @@
 <?php
-// @codingStandardsIgnoreStart
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
 use Icinga\Module\Doc\Controller as DocController;
 
-use Icinga\Module\Doc\DocParser;
-
 class Doc_IndexController extends DocController
 {
-    protected $parser;
-
-
-    public function init()
-    {
-        $module = null;
-        $this->parser = new DocParser($module);
-    }
-
-
-    public function tocAction()
-    {
-        // Temporary workaround
-        list($html, $toc)   = $this->parser->getDocumentation();
-        $this->view->toc = $toc;
-    }
-
-    /**
-     * Display the application's documentation
-     */
     public function indexAction()
     {
-        $this->populateView();
+
     }
 }
-// @codingStandardsIgnoreEnd
diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml
index a178cc155..e69de29bb 100644
--- a/modules/doc/application/views/scripts/index/index.phtml
+++ b/modules/doc/application/views/scripts/index/index.phtml
@@ -1,5 +0,0 @@
-<h1>Icinga 2 Documentation</h1>
-<?= $this->partial('module/view.phtml', 'doc', array(
-    'toc'   => $toc,
-    'html'  => $html
-)); ?>
\ No newline at end of file

From 83c12cfb2ef5c29158c885d5f493dddb0394b2a8 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 09:35:02 +0200
Subject: [PATCH 03/96] modules/doc: Add partial for displaying doc and toc

refs #4820
---
 .../views/scripts/partials/docandtoc.phtml    | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 modules/doc/application/views/scripts/partials/docandtoc.phtml

diff --git a/modules/doc/application/views/scripts/partials/docandtoc.phtml b/modules/doc/application/views/scripts/partials/docandtoc.phtml
new file mode 100644
index 000000000..704e5ef88
--- /dev/null
+++ b/modules/doc/application/views/scripts/partials/docandtoc.phtml
@@ -0,0 +1,19 @@
+<?php if ($docHtml === null): ?>
+  <p>No documentation available.</p>
+<?php else: ?>
+  <div class="toc">
+    <?= $this->partial(
+      'layout/menu.phtml',
+        'default',
+        array(
+          'items'   => $docToc->getChildren(),
+          'sub'     => false,
+          'url'     => ''
+        )
+      );
+    ?>
+    </div>
+    <div class="doc">
+      <?= $docHtml ?>
+    </div>
+<?php endif ?>

From e6abe21fb8bb2ec444e9432a012abfbf3a9af457 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 09:36:14 +0200
Subject: [PATCH 04/96] modules/doc: Rename DocParser::getDocumentation() to
 getDocAndToc()

refs #4820
---
 modules/doc/library/Doc/Controller.php | 14 +++++++-------
 modules/doc/library/Doc/DocParser.php  |  8 ++++----
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/modules/doc/library/Doc/Controller.php b/modules/doc/library/Doc/Controller.php
index 3f7b1da4e..840011150 100644
--- a/modules/doc/library/Doc/Controller.php
+++ b/modules/doc/library/Doc/Controller.php
@@ -9,15 +9,15 @@ use Icinga\Web\Controller\ActionController;
 class Controller extends ActionController
 {
     /**
-     * Set HTML and toc
+     * Publish doc HTML and toc to the view
      *
-     * @param string $module
+     * @param string $module Name of the module for which to populate doc and toc. `null` for Icinga Web 2's doc
      */
     protected function populateView($module = null)
     {
-        $parser             = new DocParser($module);
-        list($html, $toc)   = $parser->getDocumentation();
-        $this->view->html   = $html;
-        $this->view->toc    = $toc;
+        $parser = new DocParser($module);
+        list($docHtml, $docToc) = $parser->getDocAndToc();
+        $this->view->docHtml = $docHtml;
+        $this->view->docToc = $docToc;
     }
-}
\ No newline at end of file
+}
diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 945a96eea..ac3c58ea6 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -4,6 +4,8 @@
 
 namespace Icinga\Module\Doc;
 
+require_once 'vendor/Parsedown/Parsedown.php';
+
 use RecursiveIteratorIterator;
 use RecursiveDirectoryIterator;
 use Parsedown;
@@ -11,8 +13,6 @@ use Icinga\Application\Icinga;
 use Icinga\Web\Menu;
 use Icinga\Web\Url;
 
-require_once 'vendor/Parsedown/Parsedown.php';
-
 /**
  * Parser for documentation written in Markdown
  */
@@ -51,12 +51,12 @@ class DocParser
     }
 
     /**
-     * Retrieve table of contents and HTML converted from markdown files sorted by filename
+     * Retrieve doc as HTML converted from markdown files sorted by filename and the table of contents
      *
      * @return  array
      * @throws  DocException
      */
-    public function getDocumentation()
+    public function getDocAndToc()
     {
         $iter = new RecursiveIteratorIterator(
             new MarkdownFileIterator(

From ce37c20736b10309353a284d62c5bfc09206213b Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 09:40:00 +0200
Subject: [PATCH 05/96] modules/doc: Add route 'modules/doc/icingaweb' to
 display Icinga Web 2's documentation

refs #4820
---
 .../application/controllers/IcingawebController.php | 13 +++++++++++++
 .../application/views/scripts/icingaweb/index.phtml |  9 +++++++++
 2 files changed, 22 insertions(+)
 create mode 100644 modules/doc/application/controllers/IcingawebController.php
 create mode 100644 modules/doc/application/views/scripts/icingaweb/index.phtml

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
new file mode 100644
index 000000000..209e751f4
--- /dev/null
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -0,0 +1,13 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+use Icinga\Module\Doc\Controller as DocController;
+
+class Doc_IcingawebController extends DocController
+{
+    public function indexAction()
+    {
+        $this->populateView();
+    }
+}
diff --git a/modules/doc/application/views/scripts/icingaweb/index.phtml b/modules/doc/application/views/scripts/icingaweb/index.phtml
new file mode 100644
index 000000000..ae6573b2b
--- /dev/null
+++ b/modules/doc/application/views/scripts/icingaweb/index.phtml
@@ -0,0 +1,9 @@
+<h1>Icinga Web 2 Documentation</h1>
+<?= $this->partial(
+  'partials/docandtoc.phtml',
+    'doc',
+    array(
+      'docToc'  => $docToc,
+      'docHtml' => $docHtml
+    )
+); ?>

From 99971e241c22f5a6c5d3a9665c5f310a2ad65422 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:03:18 +0200
Subject: [PATCH 06/96] modules/doc: Replace Parsedown's decprecated method
 parse() with text()

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index ac3c58ea6..f098d6907 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -125,7 +125,7 @@ class DocParser
             }
             $fileObject->flock(LOCK_UN);
         }
-        $html = Parsedown::instance()->parse(implode('', $cat));
+        $html = Parsedown::instance()->text(implode('', $cat));
         $html = preg_replace_callback(
             '#<pre><code class="language-php">(.*?)\</code></pre>#s',
             array($this, 'highlight'),

From 74ea4d19b829738f66f6778536532257b44069c8 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:04:36 +0200
Subject: [PATCH 07/96] modules/doc: Rename populateView() to renderDocAndToc()

Rename populateView() to renderDocAndToc() since the method now sets the viewHelper
to docandtoc.phtml

refs #4820
---
 .../views/scripts/partials/docandtoc.phtml    | 33 ++++++++++---------
 modules/doc/library/Doc/Controller.php        |  4 ++-
 2 files changed, 20 insertions(+), 17 deletions(-)

diff --git a/modules/doc/application/views/scripts/partials/docandtoc.phtml b/modules/doc/application/views/scripts/partials/docandtoc.phtml
index 704e5ef88..46f7b0bfe 100644
--- a/modules/doc/application/views/scripts/partials/docandtoc.phtml
+++ b/modules/doc/application/views/scripts/partials/docandtoc.phtml
@@ -1,19 +1,20 @@
+<h1><?= $docName ?> documentation</h1>
 <?php if ($docHtml === null): ?>
-  <p>No documentation available.</p>
+<p>Documentation not available.</p>
 <?php else: ?>
-  <div class="toc">
-    <?= $this->partial(
-      'layout/menu.phtml',
-        'default',
-        array(
-          'items'   => $docToc->getChildren(),
-          'sub'     => false,
-          'url'     => ''
-        )
-      );
-    ?>
-    </div>
-    <div class="doc">
-      <?= $docHtml ?>
-    </div>
+<div class="toc">
+<?= $this->partial(
+  'layout/menu.phtml',
+    'default',
+    array(
+      'items'   => $docToc->getChildren(),
+      'sub'     => false,
+      'url'     => ''
+    )
+  );
+?>
+</div>
+<div class="doc">
+<?= $docHtml ?>
+</div>
 <?php endif ?>
diff --git a/modules/doc/library/Doc/Controller.php b/modules/doc/library/Doc/Controller.php
index 840011150..ae5bc177f 100644
--- a/modules/doc/library/Doc/Controller.php
+++ b/modules/doc/library/Doc/Controller.php
@@ -13,11 +13,13 @@ class Controller extends ActionController
      *
      * @param string $module Name of the module for which to populate doc and toc. `null` for Icinga Web 2's doc
      */
-    protected function populateView($module = null)
+    protected function renderDocAndToc($module = null)
     {
         $parser = new DocParser($module);
         list($docHtml, $docToc) = $parser->getDocAndToc();
         $this->view->docHtml = $docHtml;
         $this->view->docToc = $docToc;
+        $this->view->docName = $module === null ? 'Icinga Web 2' : ucfirst($module);
+        $this->_helper->viewRenderer('partials/docandtoc', null, true);
     }
 }

From 40c2c64985be040976c27ca169ec0ca9174db63d Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:06:28 +0200
Subject: [PATCH 08/96] modules/doc: Implement doc/module/$moduleName URLs

refs #4820
---
 .../controllers/ModuleController.php          | 27 ++++++-------------
 1 file changed, 8 insertions(+), 19 deletions(-)

diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index 5e3636f92..5c7851542 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -1,5 +1,4 @@
 <?php
-// @codingStandardsIgnoreStart
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
@@ -9,39 +8,29 @@ use Icinga\Module\Doc\Controller as DocController;
 class Doc_ModuleController extends DocController
 {
     /**
-     * Display module documentations index
+     * List available modules
      */
     public function indexAction()
     {
         $this->view->enabledModules = Icinga::app()->getModuleManager()->listEnabledModules();
     }
 
-    /**
-     * Display a module's documentation
-     */
-    public function viewAction()
-    {
-        $this->populateView($this->getParam('name'));
-    }
-
     /**
      * Provide run-time dispatching of module documentation
      *
-     * @param   string    $methodName
-     * @param   array     $args
+     * @param   string  $methodName
+     * @param   array   $args
      *
      * @return  mixed
      */
     public function __call($methodName, $args)
     {
-        // TODO(el): Setup routing to retrieve module name as param and point route to moduleAction
-        $moduleManager  = Icinga::app()->getModuleManager();
-        $moduleName     = substr($methodName, 0, -6);  // Strip 'Action' suffix
-        if (!$moduleManager->hasEnabled($moduleName)) {
-            // TODO(el): Throw a not found exception once the code has been moved to the moduleAction (see TODO above)
+        $moduleManager = Icinga::app()->getModuleManager();
+        $moduleName = substr($methodName, 0, -6);  // Strip 'Action' suffix
+        if (! $moduleManager->hasEnabled($moduleName)) {
+            // TODO(el): Distinguish between not enabled and not installed
             return parent::__call($methodName, $args);
         }
-        $this->_helper->redirector->gotoSimpleAndExit('view', null, null, array('name' => $moduleName));
+        $this->renderDocAndToc($moduleName);
     }
 }
-// @codingStandardsIgnoreEnd

From ba860edb75f76b42bb16e5975783b20d17d8e86c Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:07:09 +0200
Subject: [PATCH 09/96] modules/doc: Remove icingaweb/index.phtml since it's
 replaced by renderDocAndToc()

refs #4820
---
 .../doc/application/controllers/IcingawebController.php  | 2 +-
 .../doc/application/views/scripts/icingaweb/index.phtml  | 9 ---------
 2 files changed, 1 insertion(+), 10 deletions(-)
 delete mode 100644 modules/doc/application/views/scripts/icingaweb/index.phtml

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
index 209e751f4..90b1e23f4 100644
--- a/modules/doc/application/controllers/IcingawebController.php
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -8,6 +8,6 @@ class Doc_IcingawebController extends DocController
 {
     public function indexAction()
     {
-        $this->populateView();
+        $this->renderDocAndToc();
     }
 }
diff --git a/modules/doc/application/views/scripts/icingaweb/index.phtml b/modules/doc/application/views/scripts/icingaweb/index.phtml
deleted file mode 100644
index ae6573b2b..000000000
--- a/modules/doc/application/views/scripts/icingaweb/index.phtml
+++ /dev/null
@@ -1,9 +0,0 @@
-<h1>Icinga Web 2 Documentation</h1>
-<?= $this->partial(
-  'partials/docandtoc.phtml',
-    'doc',
-    array(
-      'docToc'  => $docToc,
-      'docHtml' => $docHtml
-    )
-); ?>

From 4001f1de62fb6bf0dc8d1e5df03c83ea350dedac Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:09:50 +0200
Subject: [PATCH 10/96] modules/doc: Remove module/view.phtml as it has been
 replaced by renderDocAndToc()

refs #4820
---
 modules/doc/application/views/scripts/module/view.phtml | 7 -------
 modules/doc/library/Doc/DocException.php                | 2 +-
 2 files changed, 1 insertion(+), 8 deletions(-)
 delete mode 100644 modules/doc/application/views/scripts/module/view.phtml

diff --git a/modules/doc/application/views/scripts/module/view.phtml b/modules/doc/application/views/scripts/module/view.phtml
deleted file mode 100644
index 291947ad7..000000000
--- a/modules/doc/application/views/scripts/module/view.phtml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php if ($html === null): ?>
-    <p>No documentation available.</p>
-<?php else: ?>
-<div class="content">
-<?= $html ?>
-</div>
-<?php endif ?>
diff --git a/modules/doc/library/Doc/DocException.php b/modules/doc/library/Doc/DocException.php
index cb7134045..795c9a5f9 100644
--- a/modules/doc/library/Doc/DocException.php
+++ b/modules/doc/library/Doc/DocException.php
@@ -4,7 +4,7 @@
 
 namespace Icinga\Module\Doc;
 
-use \Exception;
+use Exception;
 
 class DocException extends Exception
 {

From a40f34394b7e17914937640f43653cf467633f5b Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:11:03 +0200
Subject: [PATCH 11/96] modules/doc: List available documentations on index

refs #4820
---
 modules/doc/application/views/scripts/index/index.phtml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml
index e69de29bb..9edb5910d 100644
--- a/modules/doc/application/views/scripts/index/index.phtml
+++ b/modules/doc/application/views/scripts/index/index.phtml
@@ -0,0 +1,5 @@
+<h1>Available documentations</h1>
+<ul>
+  <li><a href="<?= $this->href('doc/icingaweb'); ?>">Icinga Web 2</a></li>
+  <li><a href="<?= $this->href('doc/module/'); ?>">Module documentations</a></li>
+</ul>

From 5627c09b3771857bb3fe97cf4a8e07fec58de79b Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:11:28 +0200
Subject: [PATCH 12/96] modules/doc: Reduce tab size to two spaces in
 module/index.phtml

refs #4820
---
 modules/doc/application/views/scripts/module/index.phtml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml
index 36f11e15e..5e9eaae9d 100644
--- a/modules/doc/application/views/scripts/module/index.phtml
+++ b/modules/doc/application/views/scripts/module/index.phtml
@@ -1,6 +1,6 @@
 <h1>Module documentations</h1>
 <ul>
 <?php foreach ($enabledModules as $module): ?>
-    <li><a href="<?= $this->href('doc/module/view', array('name' => $module)); ?>"><?= $module ?></a></li>
+  <li><a href="<?= $this->href('doc/module/' . $module); ?>"><?= $module ?></a></li>
 <?php endforeach ?>
 </ul>

From 89bddb10e7cfe90fed4426228e9837d058cf8d16 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:16:58 +0200
Subject: [PATCH 13/96] modules/doc: Tell which doc directory does not exist,
 in case it does not exist :)

Thanks Michael

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index f098d6907..39f8c8718 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -35,16 +35,16 @@ class DocParser
             $dir = Icinga::app()->getApplicationDir('/../doc');
         } else {
             $mm = Icinga::app()->getModuleManager();
-            if (!$mm->hasInstalled($module)) {
+            if (! $mm->hasInstalled($module)) {
                 throw new DocException('Module is not installed');
             }
-            if (!$mm->hasEnabled($module)) {
+            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');
+        if (! is_dir($dir)) {
+            throw new DocException('Doc directory `' . $dir .'\' does not exist');
         }
         $this->dir      = $dir;
         $this->module   = $module;
@@ -81,7 +81,7 @@ class DocParser
                 throw new DocException('Couldn\'t get the lock');
             }
             $line = null;
-            while (!$fileObject->eof()) {
+            while (! $fileObject->eof()) {
                 // Save last line for setext-style headers
                 $lastLine   = $line;
                 $line       = $fileObject->fgets();
@@ -156,7 +156,7 @@ class DocParser
      */
     protected function extractHeader($line, $lastLine)
     {
-        if (!$line) {
+        if (! $line) {
             return null;
         }
         $header = null;
@@ -167,7 +167,7 @@ class DocParser
             // Atx-style
             $level  = strlen($match[0]);
             $header = trim(substr($line, $level));
-            if (!$header) {
+            if (! $header) {
                 return null;
             }
         } elseif (
@@ -177,7 +177,7 @@ class DocParser
         ) {
             // Setext
             $header = trim($lastLine);
-            if (!$header) {
+            if (! $header) {
                 return null;
             }
             if ($match[0][0] === '=') {

From 1d5c4c50554f24ac81ada45c20d1e61de6089730 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:17:37 +0200
Subject: [PATCH 14/96] modules/doc: Add styles

refs #4820
---
 modules/doc/public/css/module.less | 9 +++++++++
 1 file changed, 9 insertions(+)
 create mode 100644 modules/doc/public/css/module.less

diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less
new file mode 100644
index 000000000..45d97e5c6
--- /dev/null
+++ b/modules/doc/public/css/module.less
@@ -0,0 +1,9 @@
+.toc {
+  float: left;
+  width: 33.333%
+}
+
+.doc {
+  float: left;
+  width: 66.667%
+}

From 5923622fb8b80089fbc00189da4be84ebf8b4db4 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 23 May 2014 14:22:35 +0200
Subject: [PATCH 15/96] modules/doc: Fix toc links

The DocParser generates toc links with a hard-coded URL. This should
be changed to be generated from a route. But we did not decide how
to handle route overrides yet.

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 39f8c8718..04086469b 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -104,14 +104,13 @@ class DocParser
                     $item   = end($toc)->item->addChild(
                         $id,
                         array(
+                            // TODO(el): URL should be generated from a route else we always have to adapt the
+                            // URL here when we change URLs
                             'url' => Url::fromPath(
-                                'doc/module/view',
-                                array(
-                                    'name' => $this->module
-                                )
+                                $this->module === null ? 'doc/icingaweb' : 'doc/module/' . $this->module
                             )->setAnchor($id)->getRelativeUrl(),
                             'title'     => htmlspecialchars($header),
-                            'priority'  => $itemPriority++,
+                            'priority'  => $itemPriority++,  // Post-increment is on purpose
                             'attribs'   => $attribs
                         )
                     );

From b58ec5f445afd020c57a90c07634eb18bf9a2ec2 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 26 May 2014 13:33:32 +0200
Subject: [PATCH 16/96] Add Module::addRoute() to add a route to the route
 chain

refs #6303
---
 library/Icinga/Application/Modules/Module.php | 34 +++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php
index e9a8ab583..9d3fb3dee 100644
--- a/library/Icinga/Application/Modules/Module.php
+++ b/library/Icinga/Application/Modules/Module.php
@@ -30,6 +30,7 @@
 namespace Icinga\Application\Modules;
 
 use Exception;
+use Zend_Controller_Router_Route_Abstract;
 use Zend_Controller_Router_Route as Route;
 use Icinga\Application\ApplicationBootstrap;
 use Icinga\Application\Config;
@@ -150,6 +151,16 @@ class Module
      */
     private $app;
 
+
+    /**
+     * Routes to add to the route chain
+     *
+     * @var array Array of name-route pairs
+     *
+     * @see addRoute()
+     */
+    protected $routes = array();
+
     /**
      * Create a new module object
      *
@@ -594,13 +605,17 @@ class Module
     }
 
     /**
-     * Register routes for web access
+     * Add routes for static content and any route added via addRoute() to the route chain
      *
-     * @return self
+     * @return  self
+     * @see     addRoute()
      */
     protected function registerRoutes()
     {
         $router = $this->app->getFrontController()->getRouter();
+        foreach ($this->routes as $name => $route) {
+            $router->addRoute($name, $route);
+        }
         $router->addRoute(
             $this->name . '_jsprovider',
             new Route(
@@ -687,4 +702,19 @@ class Module
 
         return $this;
     }
+
+    /**
+     * Add a route which will be added to the route chain
+     *
+     * @param   string                                  $name   Name of the route
+     * @param   Zend_Controller_Router_Route_Abstract   $route  Instance of the route
+     *
+     * @return  self
+     * @see     registerRoutes()
+     */
+    protected function addRoute($name, Zend_Controller_Router_Route_Abstract $route)
+    {
+        $this->routes[$name] = $route;
+        return $this;
+    }
 }

From 6ce739e23d4e1c01249e803cf80357bf44cfc037 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 14:31:17 +0200
Subject: [PATCH 17/96] Doc: Construct parser with the path to the
 documentation

Before, the parser decided which path to used based on a given module name.
Now, the parser requires the path to the documentation.
Further the toc items no longer include a URL. The must URL must be generated
from a render function or view script.

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 54 +++++++++++----------------
 1 file changed, 22 insertions(+), 32 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 04086469b..fc87faf27 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -9,7 +9,7 @@ require_once 'vendor/Parsedown/Parsedown.php';
 use RecursiveIteratorIterator;
 use RecursiveDirectoryIterator;
 use Parsedown;
-use Icinga\Application\Icinga;
+use Icinga\Exception\NotReadableError;
 use Icinga\Web\Menu;
 use Icinga\Web\Url;
 
@@ -18,36 +18,30 @@ use Icinga\Web\Url;
  */
 class DocParser
 {
-    protected $dir;
-
-    protected $module;
+    /**
+     * Path to the documentation
+     *
+     * @var string
+     */
+    protected $path;
 
     /**
-     * Create a new documentation parser for the given module or the application
+     * Create a new documentation parser for the given path
      *
-     * @param   string $module
+     * @param   string $path Path to the documentation
      *
      * @throws  DocException
+     * @throws  NotReadableError
      */
-    public function __construct($module = null)
+    public function __construct($path)
     {
-        if ($module === null) {
-            $dir = Icinga::app()->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($path)) {
+            throw new DocException('Doc directory `' . $path .'\' does not exist');
         }
-        if (! is_dir($dir)) {
-            throw new DocException('Doc directory `' . $dir .'\' does not exist');
+        if (! is_readable($path)) {
+            throw new NotReadableError('Doc directory `' . $path .'\' is not readable');
         }
-        $this->dir      = $dir;
-        $this->module   = $module;
+        $this->path = $path;
     }
 
     /**
@@ -60,7 +54,7 @@ class DocParser
     {
         $iter = new RecursiveIteratorIterator(
             new MarkdownFileIterator(
-                new RecursiveDirectoryIterator($this->dir)
+                new RecursiveDirectoryIterator($this->path)
             )
         );
         $fileInfos = iterator_to_array($iter);
@@ -89,7 +83,7 @@ class DocParser
                 if ($header !== null) {
                     list($header, $level)   = $header;
                     $id                     = $this->extractHeaderId($header);
-                    $attribs                = array();
+                    $nofollow               = false;
                     $this->reduceToc($toc, $level);
                     if ($id === null) {
                         $path = array();
@@ -98,20 +92,16 @@ class DocParser
                         }
                         $path[]         = $header;
                         $id             = implode('-', $path);
-                        $attribs['rel'] = 'nofollow';
+                        $nofollow       = true;
                     }
                     $id     = urlencode(str_replace('.', '&#46;', strip_tags($id)));
                     $item   = end($toc)->item->addChild(
                         $id,
                         array(
-                            // TODO(el): URL should be generated from a route else we always have to adapt the
-                            // URL here when we change URLs
-                            'url' => Url::fromPath(
-                                $this->module === null ? 'doc/icingaweb' : 'doc/module/' . $this->module
-                            )->setAnchor($id)->getRelativeUrl(),
-                            'title'     => htmlspecialchars($header),
+                            'id'        => $id,
+                            'title'     => $header,
                             'priority'  => $itemPriority++,  // Post-increment is on purpose
-                            'attribs'   => $attribs
+                            'nofollow'  => $nofollow
                         )
                     );
                     $toc[]  = ((object) array(

From 71615151a4a20ba8098d857fe3ed5ebaf9fcd112 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 14:48:35 +0200
Subject: [PATCH 18/96] Doc: Rename `Controller' to `DocController'

refs #4820
---
 modules/doc/library/Doc/{Controller.php => DocController.php} | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
 rename modules/doc/library/Doc/{Controller.php => DocController.php} (94%)

diff --git a/modules/doc/library/Doc/Controller.php b/modules/doc/library/Doc/DocController.php
similarity index 94%
rename from modules/doc/library/Doc/Controller.php
rename to modules/doc/library/Doc/DocController.php
index ae5bc177f..7a6b4f854 100644
--- a/modules/doc/library/Doc/Controller.php
+++ b/modules/doc/library/Doc/DocController.php
@@ -6,7 +6,7 @@ namespace Icinga\Module\Doc;
 
 use Icinga\Web\Controller\ActionController;
 
-class Controller extends ActionController
+class DocController extends ActionController
 {
     /**
      * Publish doc HTML and toc to the view

From 884e4c8e946c40c5a3b0570ad64ab85e8101b790 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 14:49:32 +0200
Subject: [PATCH 19/96] Doc: Add `DocController::renderToc()' method

refs #4820
---
 modules/doc/library/Doc/DocController.php | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
index 7a6b4f854..967f7f070 100644
--- a/modules/doc/library/Doc/DocController.php
+++ b/modules/doc/library/Doc/DocController.php
@@ -22,4 +22,19 @@ class DocController extends ActionController
         $this->view->docName = $module === null ? 'Icinga Web 2' : ucfirst($module);
         $this->_helper->viewRenderer('partials/docandtoc', null, true);
     }
+
+    /**
+     * Render a toc
+     *
+     * @param string $path Path to the documentation
+     * @param string
+     */
+    protected function renderToc($path, $name)
+    {
+        $parser = new DocParser($path);
+        list($docHtml, $docToc) = $parser->getDocAndToc();
+        $this->view->docToc = $docToc;
+        $this->view->docName = $name;
+        $this->_helper->viewRenderer('partials/toc', null, true);
+    }
 }

From dd6427019a637f45cffc3dcf8e568e0c447d61e9 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 14:50:48 +0200
Subject: [PATCH 20/96] Doc: Add action to render the toc of Icinga Web 2's
 documentation

refs #4820
---
 .../application/controllers/IcingawebController.php    | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
index 90b1e23f4..f9e05cefd 100644
--- a/modules/doc/application/controllers/IcingawebController.php
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -2,12 +2,16 @@
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
-use Icinga\Module\Doc\Controller as DocController;
+use Icinga\Application\Icinga;
+use Icinga\Module\Doc\DocController;
 
 class Doc_IcingawebController extends DocController
 {
-    public function indexAction()
+    /**
+     * View toc of Icinga Web 2's documentation
+     */
+    public function tocAction()
     {
-        $this->renderDocAndToc();
+        $this->renderToc(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2');
     }
 }

From 0f5e0767713f2c9db96d2ad1e13d34c625bcad54 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 14:53:25 +0200
Subject: [PATCH 21/96] Doc: Add action to view the toc of a module's
 documentation

refs #4820
---
 .../controllers/ModuleController.php          | 38 +++++++++++++------
 1 file changed, 26 insertions(+), 12 deletions(-)

diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index 5c7851542..cdeca3e50 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -2,8 +2,9 @@
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
+use \Zend_Controller_Action_Exception;
 use Icinga\Application\Icinga;
-use Icinga\Module\Doc\Controller as DocController;
+use Icinga\Module\Doc\DocController;
 
 class Doc_ModuleController extends DocController
 {
@@ -16,21 +17,34 @@ class Doc_ModuleController extends DocController
     }
 
     /**
-     * Provide run-time dispatching of module documentation
+     * Assert that the given module is enabled
      *
-     * @param   string  $methodName
-     * @param   array   $args
+     * @param   $moduleName
      *
-     * @return  mixed
+     * @throws  Zend_Controller_Action_Exception
      */
-    public function __call($methodName, $args)
+    protected function assertModuleEnabled($moduleName)
     {
-        $moduleManager = Icinga::app()->getModuleManager();
-        $moduleName = substr($methodName, 0, -6);  // Strip 'Action' suffix
-        if (! $moduleManager->hasEnabled($moduleName)) {
-            // TODO(el): Distinguish between not enabled and not installed
-            return parent::__call($methodName, $args);
+        if ($moduleName === null) {
+            throw new Zend_Controller_Action_Exception('Missing parameter "moduleName"', 404);
         }
-        $this->renderDocAndToc($moduleName);
+        $moduleManager = Icinga::app()->getModuleManager();
+        if (! $moduleManager->hasInstalled($moduleName)) {
+            throw new Zend_Controller_Action_Exception('Module ' . $moduleName . ' is not installed', 404);
+        }
+        if (! $moduleManager->hasEnabled($moduleName)) {
+            throw new Zend_Controller_Action_Exception('Module ' . $moduleName. ' is not enabled', 404);
+        }
+    }
+
+    /**
+     * View toc of a module's documentation
+     */
+    public function tocAction()
+    {
+        $moduleName = $this->getParam('moduleName');
+        $this->assertModuleEnabled($moduleName);
+        $moduleManager = Icinga::app()->getModuleManager();
+        $this->renderToc($moduleManager->getModuleDir($moduleName, '/doc'), $moduleName);
     }
 }

From e320d8613c87e41617c4b7e183550f4ad16f5239 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 15:02:09 +0200
Subject: [PATCH 22/96] Doc: Rename `docandtoc' view script to `chapter'

Prepare that every chapter is displayed on a new page.

refs #4820
---
 .../views/scripts/partials/chapter.phtml      |  1 +
 .../views/scripts/partials/docandtoc.phtml    | 20 -------------------
 2 files changed, 1 insertion(+), 20 deletions(-)
 create mode 100644 modules/doc/application/views/scripts/partials/chapter.phtml
 delete mode 100644 modules/doc/application/views/scripts/partials/docandtoc.phtml

diff --git a/modules/doc/application/views/scripts/partials/chapter.phtml b/modules/doc/application/views/scripts/partials/chapter.phtml
new file mode 100644
index 000000000..b0e1fe0cb
--- /dev/null
+++ b/modules/doc/application/views/scripts/partials/chapter.phtml
@@ -0,0 +1 @@
+<?= $chapterHtml ?>
diff --git a/modules/doc/application/views/scripts/partials/docandtoc.phtml b/modules/doc/application/views/scripts/partials/docandtoc.phtml
deleted file mode 100644
index 46f7b0bfe..000000000
--- a/modules/doc/application/views/scripts/partials/docandtoc.phtml
+++ /dev/null
@@ -1,20 +0,0 @@
-<h1><?= $docName ?> documentation</h1>
-<?php if ($docHtml === null): ?>
-<p>Documentation not available.</p>
-<?php else: ?>
-<div class="toc">
-<?= $this->partial(
-  'layout/menu.phtml',
-    'default',
-    array(
-      'items'   => $docToc->getChildren(),
-      'sub'     => false,
-      'url'     => ''
-    )
-  );
-?>
-</div>
-<div class="doc">
-<?= $docHtml ?>
-</div>
-<?php endif ?>

From 282af5a79467ea4cc49b9c6da0424877ccc0b304 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 15:03:02 +0200
Subject: [PATCH 23/96] Doc: Add `toc' view script

refs #4820
---
 .../application/views/scripts/partials/toc.phtml   | 14 ++++++++++++++
 1 file changed, 14 insertions(+)
 create mode 100644 modules/doc/application/views/scripts/partials/toc.phtml

diff --git a/modules/doc/application/views/scripts/partials/toc.phtml b/modules/doc/application/views/scripts/partials/toc.phtml
new file mode 100644
index 000000000..c32285516
--- /dev/null
+++ b/modules/doc/application/views/scripts/partials/toc.phtml
@@ -0,0 +1,14 @@
+<div class="controls">
+  <h1><?= $docName ?> documentation</h1>
+</div>
+<div class="content" data-base-target="_next">
+<?= $this->partial(
+  'layout/menu.phtml',
+  'default',
+  array(
+    'items' => $docToc->getChildren(),
+    'sub'   => false,
+    'url'   => ''
+  )
+) ?>
+</div>

From dad7dc9e6ca0c9d2028a8b9d799a8ff8cf5fa0e4 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 15:06:48 +0200
Subject: [PATCH 24/96] Doc: Rename `DocController::renderDocAndToc()' to
 `renderChapter()'

Prepare that every chapter is displayed on a new page.

refs #4820
---
 modules/doc/library/Doc/DocController.php | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
index 967f7f070..0929eac4e 100644
--- a/modules/doc/library/Doc/DocController.php
+++ b/modules/doc/library/Doc/DocController.php
@@ -9,25 +9,24 @@ use Icinga\Web\Controller\ActionController;
 class DocController extends ActionController
 {
     /**
-     * Publish doc HTML and toc to the view
+     * Render a chapter
      *
-     * @param string $module Name of the module for which to populate doc and toc. `null` for Icinga Web 2's doc
+     * @param string $chapterName   Name of the chapter
+     * @param string $path          Path to the documentation
      */
-    protected function renderDocAndToc($module = null)
+    protected function renderChapter($chapterName, $path)
     {
-        $parser = new DocParser($module);
+        $parser = new DocParser($path);
         list($docHtml, $docToc) = $parser->getDocAndToc();
-        $this->view->docHtml = $docHtml;
-        $this->view->docToc = $docToc;
-        $this->view->docName = $module === null ? 'Icinga Web 2' : ucfirst($module);
-        $this->_helper->viewRenderer('partials/docandtoc', null, true);
+        $this->view->chapterHtml = $docHtml;
+        $this->_helper->viewRenderer('partials/chapter', null, true);
     }
 
     /**
      * Render a toc
      *
      * @param string $path Path to the documentation
-     * @param string
+     * @param string $name Name of the documentation
      */
     protected function renderToc($path, $name)
     {

From e78d98a60786b14e2cb7fbb498587013f1046704 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 15:07:49 +0200
Subject: [PATCH 25/96] Doc: Add action to display a chapter of Icinga Web 2's
 documentation

Note that the FULL documentation is displayed yet.

refs #4820
---
 .../controllers/IcingawebController.php           | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
index f9e05cefd..5ddc4d323 100644
--- a/modules/doc/application/controllers/IcingawebController.php
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -2,6 +2,7 @@
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
+use \Zend_Controller_Action_Exception;
 use Icinga\Application\Icinga;
 use Icinga\Module\Doc\DocController;
 
@@ -14,4 +15,18 @@ class Doc_IcingawebController extends DocController
     {
         $this->renderToc(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2');
     }
+
+    /**
+     * View a chapter of Icinga Web 2's documentation
+     *
+     * @throws Zend_Controller_Action_Exception
+     */
+    public function chapterAction()
+    {
+        $chapterName = $this->getParam('chapterName');
+        if ($chapterName === null) {
+            throw new Zend_Controller_Action_Exception('Missing parameter "chapterName"', 404);
+        }
+        $this->renderChapter($chapterName, Icinga::app()->getApplicationDir('/../doc'));
+    }
 }

From 178402b65c80e5029be43ddad74636d83c1f698f Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 15:09:01 +0200
Subject: [PATCH 26/96] Doc: Add action to view a chapter of a module's
 documentation

Note that the FULL documentation is displayed yet.

refs #4820
---
 .../controllers/ModuleController.php            | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index cdeca3e50..fd74050ec 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -47,4 +47,21 @@ class Doc_ModuleController extends DocController
         $moduleManager = Icinga::app()->getModuleManager();
         $this->renderToc($moduleManager->getModuleDir($moduleName, '/doc'), $moduleName);
     }
+
+    /**
+     * View a chapter of a module's documentation
+     *
+     * @throws Zend_Controller_Action_Exception
+     */
+    public function chapterAction()
+    {
+        $moduleName = $this->getParam('moduleName');
+        $this->assertModuleEnabled($moduleName);
+        $chapterName = $this->getParam('chapterName');
+        if ($chapterName === null) {
+            throw new Zend_Controller_Action_Exception('Missing parameter "chapterName"', 404);
+        }
+        $moduleManager = Icinga::app()->getModuleManager();
+        $this->renderChapter($chapterName, $moduleManager->getModuleDir($moduleName, '/doc'));
+    }
 }

From 6159c0545794b39d503ca2dfa8fda78b81189c81 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 27 May 2014 15:09:43 +0200
Subject: [PATCH 27/96] Doc: Fix IndexController not using `DocController'

refs #4820
---
 modules/doc/application/controllers/IndexController.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php
index 1911f0534..aa44c9c5d 100644
--- a/modules/doc/application/controllers/IndexController.php
+++ b/modules/doc/application/controllers/IndexController.php
@@ -2,7 +2,7 @@
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
-use Icinga\Module\Doc\Controller as DocController;
+use Icinga\Module\Doc\DocController;
 
 class Doc_IndexController extends DocController
 {

From 93ee8e0a3d6bfb4b6e98e58f1d5eec14898d5cf6 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 13:28:06 +0200
Subject: [PATCH 28/96] Doc: Remove index/toc view script

I see no reference to this file.

refs #4820
---
 .../doc/application/views/scripts/index/toc.phtml  | 14 --------------
 1 file changed, 14 deletions(-)
 delete mode 100644 modules/doc/application/views/scripts/index/toc.phtml

diff --git a/modules/doc/application/views/scripts/index/toc.phtml b/modules/doc/application/views/scripts/index/toc.phtml
deleted file mode 100644
index 9188e21ff..000000000
--- a/modules/doc/application/views/scripts/index/toc.phtml
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="controls">
-<h1>Module documentations</h1>
-</div>
-<div class="content" data-base-target="_next">
-<?= $this->partial(
-    'layout/menu.phtml',
-    'default',
-    array(
-        'items' => $toc->getChildren(),
-        'sub'   => false,
-        'url'   => ''
-    )
-) ?>
-</div>

From c12c4a9e4ca64543d981b3aa1486c12fd3ffedcf Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 13:29:02 +0200
Subject: [PATCH 29/96] Modules: Call `Module::registerWebIntegration()' after
 including the run script

Routes added via a module's run script were not respected since
`Module::registerRoutes()' is called from `Module::registerWebIntegration()'.

refs #6303
---
 library/Icinga/Application/Modules/Module.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php
index 9d3fb3dee..6959c6903 100644
--- a/library/Icinga/Application/Modules/Module.php
+++ b/library/Icinga/Application/Modules/Module.php
@@ -192,8 +192,7 @@ class Module
      */
     public function register()
     {
-        $this->registerAutoloader()
-             ->registerWebIntegration();
+        $this->registerAutoloader();
         try {
             $this->launchRunScript();
         } catch (Exception $e) {
@@ -205,6 +204,7 @@ class Module
             );
             return false;
         }
+        $this->registerWebIntegration();
         return true;
     }
 

From 0f4d2ad80c6a3b89f06232837a11bae6324b63f0 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 13:33:07 +0200
Subject: [PATCH 30/96] Doc: Support
 `doc/module/:moduleName/chapter/:chapterName' URLs

refs #4820
---
 modules/doc/run.php | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 modules/doc/run.php

diff --git a/modules/doc/run.php b/modules/doc/run.php
new file mode 100644
index 000000000..4db96cbc3
--- /dev/null
+++ b/modules/doc/run.php
@@ -0,0 +1,19 @@
+<?php
+
+use \Zend_Controller_Router_Route;
+use Icinga\Application\Icinga;
+
+if (Icinga::app()->isCli()) {
+    return;
+}
+
+$docModuleChapter = new Zend_Controller_Router_Route(
+    'doc/module/:moduleName/chapter/:chapterName',
+    array(
+        'controller'    => 'module',
+        'action'        => 'chapter',
+        'module'        => 'doc'
+    )
+);
+
+$this->addRoute('doc/module/chapter', $docModuleChapter);

From b11c0c36f9c82bb7967ab896eb4eb68624d8ce22 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 13:34:39 +0200
Subject: [PATCH 31/96] Doc: Support `doc/icingaweb/chapter/:chapterName' URLs

refs #4820
---
 modules/doc/run.php | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/modules/doc/run.php b/modules/doc/run.php
index 4db96cbc3..9d93ee865 100644
--- a/modules/doc/run.php
+++ b/modules/doc/run.php
@@ -16,4 +16,15 @@ $docModuleChapter = new Zend_Controller_Router_Route(
     )
 );
 
+$docIcingaWebChapter = new Zend_Controller_Router_Route(
+    'doc/icingaweb/chapter/:chapterName',
+    array(
+        'controller'    => 'icingaweb',
+        'action'        => 'chapter',
+        'module'        => 'doc'
+    )
+);
+
 $this->addRoute('doc/module/chapter', $docModuleChapter);
+$this->addRoute('doc/icingaweb/chapter', $docIcingaWebChapter);
+

From 040473f9860065b30a120f417662af9e8a2fdd1d Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:13:42 +0200
Subject: [PATCH 32/96] lib: Add NodeInterface

---
 library/Icinga/Data/Tree/NodeInterface.php | 38 ++++++++++++++++++++++
 1 file changed, 38 insertions(+)
 create mode 100644 library/Icinga/Data/Tree/NodeInterface.php

diff --git a/library/Icinga/Data/Tree/NodeInterface.php b/library/Icinga/Data/Tree/NodeInterface.php
new file mode 100644
index 000000000..8ecca86c6
--- /dev/null
+++ b/library/Icinga/Data/Tree/NodeInterface.php
@@ -0,0 +1,38 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Data\Tree;
+
+interface NodeInterface
+{
+    /**
+     * Append a child to the node
+     *
+     * @param   mixed $value
+     *
+     * @return  self
+     */
+    public function appendChild($value);
+
+    /**
+     * Get the node's value
+     *
+     * @return mixed
+     */
+    public function getValue();
+
+    /**
+     * Whether the node has children
+     *
+     * @return bool
+     */
+    public function hasChildren();
+
+    /**
+     * Get the node's children
+     *
+     * @return array
+     */
+    public function getChildren();
+}

From 9edaaa82e893b9cacf4db412b4e28d9a90f008f2 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:14:33 +0200
Subject: [PATCH 33/96] lib: Add TreeIterator

---
 library/Icinga/Data/Tree/TreeIterator.php | 90 +++++++++++++++++++++++
 1 file changed, 90 insertions(+)
 create mode 100644 library/Icinga/Data/Tree/TreeIterator.php

diff --git a/library/Icinga/Data/Tree/TreeIterator.php b/library/Icinga/Data/Tree/TreeIterator.php
new file mode 100644
index 000000000..6722f3b35
--- /dev/null
+++ b/library/Icinga/Data/Tree/TreeIterator.php
@@ -0,0 +1,90 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Data\Tree;
+
+//use RecursiveIterator;
+//
+//class TreeIterator implements RecursiveIterator
+//{
+//    protected $position = 0;
+//
+//    protected $nodes;
+//
+//    public function __construct(NodeInterface $node)
+//    {
+//        $this->nodes = $node->getChildren();
+//    }
+//
+//    public function hasChildren()
+//    {
+//        return $this->current()->hasChildren();
+//    }
+//
+//    public function getChildren()
+//    {
+//        return new self($this->current());
+//    }
+//
+//    public function current()
+//    {
+//        return $this->nodes[$this->position];
+//    }
+//
+//    public function next()
+//    {
+//        ++$this->position;
+//    }
+//
+//    public function valid()
+//    {
+//        return isset($this->nodes[$this->position]);
+//    }
+//
+//    public function rewind()
+//    {
+//        $this->position = 0;
+//    }
+//
+//    public function key()
+//    {
+//        return $this->position;
+//    }
+//}
+
+use ArrayIterator;
+use RecursiveIterator;
+
+class TreeIterator extends ArrayIterator implements RecursiveIterator
+{
+    /**
+     * Create a new TreeIterator
+     *
+     * @param NodeInterface $node
+     */
+    public function __construct(NodeInterface $node)
+    {
+        parent::__construct($node->getChildren());
+    }
+
+    /**
+     * Whether an iterator can be created for the current node
+     *
+     * @return bool
+     */
+    public function hasChildren()
+    {
+        return $this->current()->hasChildren();
+    }
+
+    /**
+     * Return an iterator for the current node
+     *
+     * @return self
+     */
+    public function getChildren()
+    {
+        return new self($this->current());
+    }
+}

From d2936d033808aaaf90dab0a8fe7456604e50ebb4 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:15:08 +0200
Subject: [PATCH 34/96] doc/lib: Add DocToc class representing a toc

refs #4820
---
 modules/doc/library/Doc/DocToc.php | 48 ++++++++++++++++++++++++++++++
 1 file changed, 48 insertions(+)
 create mode 100644 modules/doc/library/Doc/DocToc.php

diff --git a/modules/doc/library/Doc/DocToc.php b/modules/doc/library/Doc/DocToc.php
new file mode 100644
index 000000000..db7d9cb03
--- /dev/null
+++ b/modules/doc/library/Doc/DocToc.php
@@ -0,0 +1,48 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use IteratorAggregate;
+use Icinga\Data\Tree\NodeInterface;
+use Icinga\Data\Tree\TreeIterator;
+
+class DocToc implements NodeInterface, IteratorAggregate
+{
+    protected $children = array();
+
+    protected $value;
+
+    public function __construct($value = null)
+    {
+        $this->value = $value;
+    }
+
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    public function appendChild($value)
+    {
+        $child = new self($value);
+        $this->children[] = $child;
+        return $child;
+    }
+
+    public function hasChildren()
+    {
+        return ! empty($this->children);
+    }
+
+    public function getChildren()
+    {
+        return $this->children;
+    }
+
+    public function getIterator()
+    {
+        return new TreeIterator($this);
+    }
+}

From f0b6a3557e2fe622bd83c4e53fe6fd109028171e Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:15:43 +0200
Subject: [PATCH 35/96] doc/lib: Add DocTocHtmlRenderer class to render a toc
 to HTML

refs #4820
---
 .../doc/library/Doc/DocTocHtmlRenderer.php    | 45 +++++++++++++++++++
 1 file changed, 45 insertions(+)
 create mode 100644 modules/doc/library/Doc/DocTocHtmlRenderer.php

diff --git a/modules/doc/library/Doc/DocTocHtmlRenderer.php b/modules/doc/library/Doc/DocTocHtmlRenderer.php
new file mode 100644
index 000000000..e402f3eb4
--- /dev/null
+++ b/modules/doc/library/Doc/DocTocHtmlRenderer.php
@@ -0,0 +1,45 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Module\Doc;
+
+use RecursiveIteratorIterator;
+
+class DocTocHtmlRenderer extends RecursiveIteratorIterator
+{
+    protected $html = array();
+
+    public function __construct(DocToc $toc)
+    {
+        parent::__construct($toc, RecursiveIteratorIterator::SELF_FIRST);
+    }
+
+    public function beginIteration()
+    {
+        $this->html[] = '<ul>';
+    }
+
+    public function endIteration()
+    {
+        $this->html[] = '</ul>';
+    }
+
+    public function beginChildren()
+    {
+        $this->html[] = '<ul>';
+    }
+
+    public function endChildren()
+    {
+        $this->html[] = '</ul>';
+    }
+
+    public function render($callback)
+    {
+        foreach ($this as $node) {
+            $this->html[] = $callback($node->getValue());
+        }
+        return implode("\n", $this->html);
+    }
+}

From 2f1303a13b7471100958982c07d56b55b6d4f361 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:16:37 +0200
Subject: [PATCH 36/96] doc: No longer use a partial for viewing a toc

refs #4820
---
 .../application/views/scripts/partials/toc.phtml   | 14 --------------
 modules/doc/library/Doc/DocController.php          |  9 ++++-----
 2 files changed, 4 insertions(+), 19 deletions(-)
 delete mode 100644 modules/doc/application/views/scripts/partials/toc.phtml

diff --git a/modules/doc/application/views/scripts/partials/toc.phtml b/modules/doc/application/views/scripts/partials/toc.phtml
deleted file mode 100644
index c32285516..000000000
--- a/modules/doc/application/views/scripts/partials/toc.phtml
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="controls">
-  <h1><?= $docName ?> documentation</h1>
-</div>
-<div class="content" data-base-target="_next">
-<?= $this->partial(
-  'layout/menu.phtml',
-  'default',
-  array(
-    'items' => $docToc->getChildren(),
-    'sub'   => false,
-    'url'   => ''
-  )
-) ?>
-</div>
diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
index 0929eac4e..ef44de478 100644
--- a/modules/doc/library/Doc/DocController.php
+++ b/modules/doc/library/Doc/DocController.php
@@ -23,17 +23,16 @@ class DocController extends ActionController
     }
 
     /**
-     * Render a toc
+     * Populate toc
      *
      * @param string $path Path to the documentation
      * @param string $name Name of the documentation
      */
-    protected function renderToc($path, $name)
+    protected function populateToc($path, $name)
     {
         $parser = new DocParser($path);
-        list($docHtml, $docToc) = $parser->getDocAndToc();
-        $this->view->docToc = $docToc;
+        list($docHtml, $tocRenderer) = $parser->getDocAndToc();
+        $this->view->tocRenderer = $tocRenderer;
         $this->view->docName = $name;
-        $this->_helper->viewRenderer('partials/toc', null, true);
     }
 }

From d3a9f17fc2f8d0bcbacc4cc7bbbec3b7e21b773a Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:18:07 +0200
Subject: [PATCH 37/96] doc/parser: Return the DocTocHtmlRenderer instead of an
 array

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 37 ++++++++++++---------------
 1 file changed, 17 insertions(+), 20 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index fc87faf27..3bd6b3a1e 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -10,8 +10,6 @@ use RecursiveIteratorIterator;
 use RecursiveDirectoryIterator;
 use Parsedown;
 use Icinga\Exception\NotReadableError;
-use Icinga\Web\Menu;
-use Icinga\Web\Url;
 
 /**
  * Parser for documentation written in Markdown
@@ -59,10 +57,10 @@ class DocParser
         );
         $fileInfos = iterator_to_array($iter);
         natcasesort($fileInfos);
-        $cat    = array();
-        $toc    = array((object) array(
+        $cat = array();
+        $tocStack = array((object) array(
             'level' => 0,
-            'item'  => new Menu('doc')
+            'node'  => new DocToc()
         ));
         $itemPriority = 1;
         foreach ($fileInfos as $fileInfo) {
@@ -84,30 +82,29 @@ class DocParser
                     list($header, $level)   = $header;
                     $id                     = $this->extractHeaderId($header);
                     $nofollow               = false;
-                    $this->reduceToc($toc, $level);
+                    $this->reduceToc($tocStack, $level);
                     if ($id === null) {
                         $path = array();
-                        foreach (array_slice($toc, 1) as $entry) {
-                            $path[] = $entry->item->getTitle();
+                        foreach (array_slice($tocStack, 1) as $entity) {
+                            $path[] = $entity->node->getValue()->title;
                         }
                         $path[]         = $header;
                         $id             = implode('-', $path);
                         $nofollow       = true;
                     }
                     $id     = urlencode(str_replace('.', '&#46;', strip_tags($id)));
-                    $item   = end($toc)->item->addChild(
-                        $id,
-                        array(
+                    $node   = end($tocStack)->node->appendChild(
+                        (object) array(
                             'id'        => $id,
                             'title'     => $header,
                             'priority'  => $itemPriority++,  // Post-increment is on purpose
                             'nofollow'  => $nofollow
                         )
                     );
-                    $toc[]  = ((object) array(
+                    $tocStack[]  = (object) array(
                         'level' => $level,
-                        'item'  => $item
-                    ));
+                        'node'  => $node
+                    );
                     $line = '<a name="' . $id . '"></a>' . PHP_EOL . $line;
                 }
                 $cat[] = $line;
@@ -120,7 +117,7 @@ class DocParser
             array($this, 'highlight'),
             $html
         );
-        return array($html, $toc[0]->item);
+        return array($html, new DocTocHtmlRenderer($tocStack[0]->node));
     }
 
     /**
@@ -200,14 +197,14 @@ class DocParser
     }
 
     /**
-     * Reduce the toc to the given level
+     * Reduce the toc stack to the given level
      *
-     * @param array &$toc
+     * @param array &$tocStack
      * @param int   $level
      */
-    protected function reduceToc(array &$toc, $level) {
-        while (end($toc)->level >= $level) {
-            array_pop($toc);
+    protected function reduceToc(array &$tocStack, $level) {
+        while (end($tocStack)->level >= $level) {
+            array_pop($tocStack);
         }
     }
 }

From 72ab5027b5afc998b9aea89fdbcbed69a56cdfe8 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:18:57 +0200
Subject: [PATCH 38/96] doc: Add view script for the toc of Icinga Web 2's doc

refs #4820
---
 .../controllers/IcingawebController.php           |  2 +-
 .../application/views/scripts/icingaweb/toc.phtml | 15 +++++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)
 create mode 100644 modules/doc/application/views/scripts/icingaweb/toc.phtml

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
index 5ddc4d323..0820a82ca 100644
--- a/modules/doc/application/controllers/IcingawebController.php
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -13,7 +13,7 @@ class Doc_IcingawebController extends DocController
      */
     public function tocAction()
     {
-        $this->renderToc(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2');
+        $this->populateToc(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2');
     }
 
     /**
diff --git a/modules/doc/application/views/scripts/icingaweb/toc.phtml b/modules/doc/application/views/scripts/icingaweb/toc.phtml
new file mode 100644
index 000000000..065071f01
--- /dev/null
+++ b/modules/doc/application/views/scripts/icingaweb/toc.phtml
@@ -0,0 +1,15 @@
+<div class="controls">
+  <h1><?= $docName ?> documentation</h1>
+</div>
+<div class="content" data-base-target="_next">
+  <?php
+    $urlHelper = $this->getHelper('Url');
+    $view = $this;
+  ?>
+  <?= $tocRenderer->render(function ($section) use ($urlHelper, $view) {
+    // Chapter name is not yet defined
+    $path = $urlHelper->url(array('chapterName' => 'tbd'), 'doc/icingaweb/chapter', false, false);
+    $url = $view->url($path)->setAnchor($section->id);
+    return sprintf('<li><a href="%s">%s</a></li>', $url->getAbsoluteUrl(), $section->title);
+  }); ?>
+</div>

From dc1bada56f98abe33def0bb002c10b8bb04e7ce0 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:19:24 +0200
Subject: [PATCH 39/96] doc: Add a view script for module's doc toc

refs #4820
---
 .../controllers/ModuleController.php            |  3 ++-
 .../application/views/scripts/module/toc.phtml  | 17 +++++++++++++++++
 2 files changed, 19 insertions(+), 1 deletion(-)
 create mode 100644 modules/doc/application/views/scripts/module/toc.phtml

diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index fd74050ec..b864ff324 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -45,7 +45,8 @@ class Doc_ModuleController extends DocController
         $moduleName = $this->getParam('moduleName');
         $this->assertModuleEnabled($moduleName);
         $moduleManager = Icinga::app()->getModuleManager();
-        $this->renderToc($moduleManager->getModuleDir($moduleName, '/doc'), $moduleName);
+        $this->populateToc($moduleManager->getModuleDir($moduleName, '/doc'), $moduleName);
+        $this->view->moduleName = $moduleName;
     }
 
     /**
diff --git a/modules/doc/application/views/scripts/module/toc.phtml b/modules/doc/application/views/scripts/module/toc.phtml
new file mode 100644
index 000000000..b3be77d79
--- /dev/null
+++ b/modules/doc/application/views/scripts/module/toc.phtml
@@ -0,0 +1,17 @@
+<div class="controls">
+  <h1><?= $docName ?> documentation</h1>
+</div>
+<div class="content" data-base-target="_next">
+  <?php
+    $urlHelper = $this->getHelper('Url');
+    $view = $this;
+  ?>
+  <?= $tocRenderer->render(function ($section) use ($urlHelper, $view, $moduleName) {
+    // Chapter name is not yet defined
+    $path = $urlHelper->url(
+      array('moduleName' => $moduleName, 'chapterName' => 'tbd'), 'doc/module/chapter', false, false
+    );
+    $url = $view->url($path)->setAnchor($section->id);
+    return sprintf('<li><a href="%s">%s</a></li>', $url->getAbsoluteUrl(), $section->title);
+  }); ?>
+</div>

From 49e927c4bf3d72e8575889ada522d57cf929600a Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:19:48 +0200
Subject: [PATCH 40/96] doc: Support `doc/module/:moduleName/toc' URLs

refs #4820
---
 modules/doc/run.php | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/modules/doc/run.php b/modules/doc/run.php
index 9d93ee865..592d85ede 100644
--- a/modules/doc/run.php
+++ b/modules/doc/run.php
@@ -25,6 +25,16 @@ $docIcingaWebChapter = new Zend_Controller_Router_Route(
     )
 );
 
+$docModuleToc = new Zend_Controller_Router_Route(
+    'doc/module/:moduleName/toc',
+    array(
+        'controller'    => 'module',
+        'action'        => 'toc',
+        'module'        => 'doc'
+    )
+);
+
 $this->addRoute('doc/module/chapter', $docModuleChapter);
 $this->addRoute('doc/icingaweb/chapter', $docIcingaWebChapter);
+$this->addRoute('doc/module/toc', $docModuleToc);
 

From 0f1983fb25146e8b39bbfce3d5bf8300d5ba0571 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Wed, 28 May 2014 17:24:28 +0200
Subject: [PATCH 41/96] doc: Fix links in index view scripts

refs #4820
---
 modules/doc/application/views/scripts/index/index.phtml  | 2 +-
 modules/doc/application/views/scripts/module/index.phtml | 6 +++++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml
index 9edb5910d..4381bc2a0 100644
--- a/modules/doc/application/views/scripts/index/index.phtml
+++ b/modules/doc/application/views/scripts/index/index.phtml
@@ -1,5 +1,5 @@
 <h1>Available documentations</h1>
 <ul>
-  <li><a href="<?= $this->href('doc/icingaweb'); ?>">Icinga Web 2</a></li>
+  <li><a href="<?= $this->href('doc/icingaweb/toc'); ?>">Icinga Web 2</a></li>
   <li><a href="<?= $this->href('doc/module/'); ?>">Module documentations</a></li>
 </ul>
diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml
index 5e9eaae9d..bbd74345a 100644
--- a/modules/doc/application/views/scripts/module/index.phtml
+++ b/modules/doc/application/views/scripts/module/index.phtml
@@ -1,6 +1,10 @@
 <h1>Module documentations</h1>
 <ul>
 <?php foreach ($enabledModules as $module): ?>
-  <li><a href="<?= $this->href('doc/module/' . $module); ?>"><?= $module ?></a></li>
+  <li>
+    <a href="<?= $this->getHelper('Url')->url(array('moduleName' => $module), 'doc/module/toc', false, false); ?>">
+      <?= $module ?>
+    </a>
+  </li>
 <?php endforeach ?>
 </ul>

From 370bfca6e6e1f0897aa74b3b755711bb3a9ec501 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 3 Jun 2014 14:53:28 +0200
Subject: [PATCH 42/96] DocParser: Remove itemPriority since it's nowhere used

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 2 --
 1 file changed, 2 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 3bd6b3a1e..299295a03 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -62,7 +62,6 @@ class DocParser
             'level' => 0,
             'node'  => new DocToc()
         ));
-        $itemPriority = 1;
         foreach ($fileInfos as $fileInfo) {
             try {
                 $fileObject = $fileInfo->openFile();
@@ -97,7 +96,6 @@ class DocParser
                         (object) array(
                             'id'        => $id,
                             'title'     => $header,
-                            'priority'  => $itemPriority++,  // Post-increment is on purpose
                             'nofollow'  => $nofollow
                         )
                     );

From ed8de18ad5cfd3fa2d14c0ce740e1755e113de28 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 3 Jun 2014 14:56:44 +0200
Subject: [PATCH 43/96] DocParser: Use first header in a file as section title

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 299295a03..0957b2dd0 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -72,15 +72,20 @@ class DocParser
                 throw new DocException('Couldn\'t get the lock');
             }
             $line = null;
+            $sectionTitle = null;
             while (! $fileObject->eof()) {
                 // 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);
-                    $nofollow               = false;
+                    list($header, $level) = $header;
+                    if ($sectionTitle === null) {
+                        // The first header is the section's title
+                        $sectionTitle = $header;
+                    }
+                    $id         = $this->extractHeaderId($header);
+                    $nofollow   = false;
                     $this->reduceToc($tocStack, $level);
                     if ($id === null) {
                         $path = array();

From d5cf2f24726d68880f6fcc3e319317c862040563 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 3 Jun 2014 15:23:59 +0200
Subject: [PATCH 44/96] doc: Implement `DocParser::getToc()'

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 122 ++++++++++++++++++++++----
 1 file changed, 105 insertions(+), 17 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 0957b2dd0..3eb97794d 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -6,8 +6,62 @@ namespace Icinga\Module\Doc;
 
 require_once 'vendor/Parsedown/Parsedown.php';
 
+use ArrayIterator;
+use RunetimeException;
+
+class FileLockingIterator extends ArrayIterator
+{
+    public function next()
+    {
+        $this->current()->flock(LOCK_UN);
+        parent::next();
+    }
+
+    public function valid()
+    {
+        if (!parent::valid()) {
+            return false;
+        }
+        $fileInfo = $this->current();
+        try {
+            $fileObject = $fileInfo->openFile();
+        } catch (RuntimeException $e) {
+            throw new DocException($e->getMessage());
+        }
+        if ($fileObject->flock(LOCK_SH) === false) {
+            throw new DocException('Couldn\'t get the lock');
+        }
+        $this[$this->key()] = $fileObject;
+        return true;
+    }
+}
+
+use IteratorAggregate;
 use RecursiveIteratorIterator;
 use RecursiveDirectoryIterator;
+
+class DocIterator implements IteratorAggregate
+{
+    protected $fileInfos;
+
+    public function __construct($path)
+    {
+        $iter = new RecursiveIteratorIterator(
+            new MarkdownFileIterator(
+                new RecursiveDirectoryIterator($path)
+            )
+        );
+        $fileInfos = iterator_to_array($iter);
+        natcasesort($fileInfos);
+        $this->fileInfos = $fileInfos;
+    }
+
+    public function getIterator()
+    {
+        return new FileLockingIterator($this->fileInfos);
+    }
+}
+
 use Parsedown;
 use Icinga\Exception\NotReadableError;
 
@@ -42,6 +96,56 @@ class DocParser
         $this->path = $path;
     }
 
+    /**
+     * Retrieve the table of contents
+     *
+     * @return  DocTocHtmlRenderer
+     */
+    public function getToc()
+    {
+        $tocStack = array((object) array(
+            'level' => 0,
+            'node'  => new DocToc()
+        ));
+        foreach (new DocIterator($this->path) as $fileObject) {
+            $line = null;
+            while (! $fileObject->eof()) {
+                // 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);
+                    $nofollow = false;
+                    $this->reduceToc($tocStack, $level);
+                    if ($id === null) {
+                        $path = array();
+                        foreach (array_slice($tocStack, 1) as $entity) {
+                            $path[] = $entity->node->getValue()->title;
+                        }
+                        $path[] = $header;
+                        $id = implode('-', $path);
+                        $nofollow = true;
+                    }
+                    $id = urlencode(str_replace('.', '&#46;', strip_tags($id)));
+                    $node = end($tocStack)->node->appendChild(
+                        (object) array(
+                            'id'        => $id,
+                            'title'     => $header,
+                            'nofollow'  => $nofollow
+                        )
+                    );
+                    $tocStack[] = (object) array(
+                        'level' => $level,
+                        'node'  => $node
+                    );
+                }
+            }
+        }
+        return new DocTocHtmlRenderer($tocStack[0]->node);
+    }
+
     /**
      * Retrieve doc as HTML converted from markdown files sorted by filename and the table of contents
      *
@@ -50,27 +154,12 @@ class DocParser
      */
     public function getDocAndToc()
     {
-        $iter = new RecursiveIteratorIterator(
-            new MarkdownFileIterator(
-                new RecursiveDirectoryIterator($this->path)
-            )
-        );
-        $fileInfos = iterator_to_array($iter);
-        natcasesort($fileInfos);
         $cat = array();
         $tocStack = array((object) array(
             'level' => 0,
             'node'  => new DocToc()
         ));
-        foreach ($fileInfos as $fileInfo) {
-            try {
-                $fileObject = $fileInfo->openFile();
-            } catch (RuntimeException $e) {
-                throw new DocException($e->getMessage());
-            }
-            if ($fileObject->flock(LOCK_SH) === false) {
-                throw new DocException('Couldn\'t get the lock');
-            }
+        foreach (new DocIterator($this->path) as $fileObject) {
             $line = null;
             $sectionTitle = null;
             while (! $fileObject->eof()) {
@@ -112,7 +201,6 @@ class DocParser
                 }
                 $cat[] = $line;
             }
-            $fileObject->flock(LOCK_UN);
         }
         $html = Parsedown::instance()->text(implode('', $cat));
         $html = preg_replace_callback(

From 794ae141fae89371dfe5bd0e1fbc63d104776c38 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 13:55:58 +0200
Subject: [PATCH 45/96] lib: Let NodeInterface extend RecursiveIterator

Since a node may have children and they should be iterable, the Nodefinterface
now extends RecursiveIterator.
---
 library/Icinga/Data/Tree/NodeInterface.php | 18 +++---------------
 1 file changed, 3 insertions(+), 15 deletions(-)

diff --git a/library/Icinga/Data/Tree/NodeInterface.php b/library/Icinga/Data/Tree/NodeInterface.php
index 8ecca86c6..1d96973e0 100644
--- a/library/Icinga/Data/Tree/NodeInterface.php
+++ b/library/Icinga/Data/Tree/NodeInterface.php
@@ -4,7 +4,9 @@
 
 namespace Icinga\Data\Tree;
 
-interface NodeInterface
+use RecursiveIterator;
+
+interface NodeInterface extends RecursiveIterator
 {
     /**
      * Append a child to the node
@@ -21,18 +23,4 @@ interface NodeInterface
      * @return mixed
      */
     public function getValue();
-
-    /**
-     * Whether the node has children
-     *
-     * @return bool
-     */
-    public function hasChildren();
-
-    /**
-     * Get the node's children
-     *
-     * @return array
-     */
-    public function getChildren();
 }

From d84532d593e71a0024194cab8f05906431b2e044 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 13:57:18 +0200
Subject: [PATCH 46/96] lib: Remove TreeIterator

Nodes implementing the NodeInterface (which extends the RecursiveIterator interface)
are already iterable.
---
 library/Icinga/Data/Tree/TreeIterator.php | 90 -----------------------
 1 file changed, 90 deletions(-)
 delete mode 100644 library/Icinga/Data/Tree/TreeIterator.php

diff --git a/library/Icinga/Data/Tree/TreeIterator.php b/library/Icinga/Data/Tree/TreeIterator.php
deleted file mode 100644
index 6722f3b35..000000000
--- a/library/Icinga/Data/Tree/TreeIterator.php
+++ /dev/null
@@ -1,90 +0,0 @@
-<?php
-// {{{ICINGA_LICENSE_HEADER}}}
-// {{{ICINGA_LICENSE_HEADER}}}
-
-namespace Icinga\Data\Tree;
-
-//use RecursiveIterator;
-//
-//class TreeIterator implements RecursiveIterator
-//{
-//    protected $position = 0;
-//
-//    protected $nodes;
-//
-//    public function __construct(NodeInterface $node)
-//    {
-//        $this->nodes = $node->getChildren();
-//    }
-//
-//    public function hasChildren()
-//    {
-//        return $this->current()->hasChildren();
-//    }
-//
-//    public function getChildren()
-//    {
-//        return new self($this->current());
-//    }
-//
-//    public function current()
-//    {
-//        return $this->nodes[$this->position];
-//    }
-//
-//    public function next()
-//    {
-//        ++$this->position;
-//    }
-//
-//    public function valid()
-//    {
-//        return isset($this->nodes[$this->position]);
-//    }
-//
-//    public function rewind()
-//    {
-//        $this->position = 0;
-//    }
-//
-//    public function key()
-//    {
-//        return $this->position;
-//    }
-//}
-
-use ArrayIterator;
-use RecursiveIterator;
-
-class TreeIterator extends ArrayIterator implements RecursiveIterator
-{
-    /**
-     * Create a new TreeIterator
-     *
-     * @param NodeInterface $node
-     */
-    public function __construct(NodeInterface $node)
-    {
-        parent::__construct($node->getChildren());
-    }
-
-    /**
-     * Whether an iterator can be created for the current node
-     *
-     * @return bool
-     */
-    public function hasChildren()
-    {
-        return $this->current()->hasChildren();
-    }
-
-    /**
-     * Return an iterator for the current node
-     *
-     * @return self
-     */
-    public function getChildren()
-    {
-        return new self($this->current());
-    }
-}

From ce0aee5e4129dbd5d74e6476c5899dba4c5068ec Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 13:58:14 +0200
Subject: [PATCH 47/96] lib: Add Data/Tree/Node.php

---
 library/Icinga/Data/Tree/Node.php | 62 +++++++++++++++++++++++++++++++
 1 file changed, 62 insertions(+)
 create mode 100644 library/Icinga/Data/Tree/Node.php

diff --git a/library/Icinga/Data/Tree/Node.php b/library/Icinga/Data/Tree/Node.php
new file mode 100644
index 000000000..db3a49749
--- /dev/null
+++ b/library/Icinga/Data/Tree/Node.php
@@ -0,0 +1,62 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Data\Tree;
+
+use RecursiveIteratorIterator;
+use RuntimeException;
+use SplDoublyLinkedList;
+
+class Node extends SplDoublyLinkedList implements NodeInterface
+{
+    protected $value;
+
+    public function __construct($value = null)
+    {
+        $this->value = $value;
+    }
+
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    public function appendChild($value)
+    {
+        $child = new static($value);
+        $this->push($child);
+        return $child;
+    }
+
+    public function hasChildren()
+    {
+        $current = $this->current();
+        if ($current === null) {;
+            $current = $this;
+        }
+        return ! $current->isEmpty();
+    }
+
+    public function getChildren()
+    {
+        $current = $this->current();
+        if ($current === null) {;
+            $current = $this;
+        }
+        return $current;
+    }
+
+    public function findNodeBy($callback)
+    {
+        if (! is_callable($callback)) {
+            throw new RuntimeException();
+        }
+        foreach (new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST) as $node) {
+            if (call_user_func($callback, $node)) {
+                return $node;
+            }
+        }
+        return null;
+    }
+}

From 6c8d35c6671f5e5ea9217b9a5a77f4c5f9ff1097 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 13:58:40 +0200
Subject: [PATCH 48/96] lib: Add a not yet customizable node renderer

---
 library/Icinga/Data/Tree/NodeRenderer.php | 58 +++++++++++++++++++++++
 1 file changed, 58 insertions(+)
 create mode 100644 library/Icinga/Data/Tree/NodeRenderer.php

diff --git a/library/Icinga/Data/Tree/NodeRenderer.php b/library/Icinga/Data/Tree/NodeRenderer.php
new file mode 100644
index 000000000..4cebb7e01
--- /dev/null
+++ b/library/Icinga/Data/Tree/NodeRenderer.php
@@ -0,0 +1,58 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Data\Tree;
+
+use Exception;
+use RecursiveIteratorIterator;
+use RuntimeException;
+
+/**
+ * A not yet customizable node renderer
+ */
+class NodeRenderer extends RecursiveIteratorIterator
+{
+    protected $content = array();
+
+    public function __construct(NodeInterface $node)
+    {
+        parent::__construct($node, RecursiveIteratorIterator::SELF_FIRST);
+    }
+
+    public function beginIteration()
+    {
+        $this->content[] = '<ul>';
+    }
+
+    public function endIteration()
+    {
+        $this->content[] = '</ul>';
+    }
+
+    public function beginChildren()
+    {
+        $this->content[] = '<ul>';
+    }
+
+    public function endChildren()
+    {
+        $this->content[] = '</ul>';
+    }
+
+    public function render($callback)
+    {
+        if (! is_callable($callback)) {
+            throw new RuntimeException('Callable expected');
+        }
+        foreach ($this as $node) {
+            try {
+                $content = call_user_func($callback, $node);
+            } catch (Exception $e) {
+                throw new RuntimeException($e);
+            }
+            $this->content[] = $content;
+        }
+        return implode("\n", $this->content);
+    }
+}

From cf987544c5c5499abbbbad0fb13b4b6dc63a6b87 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 13:59:11 +0200
Subject: [PATCH 49/96] doc: Remove class DocToc as it's superseded by
 Data/Tree/Node

refs #4820
---
 modules/doc/library/Doc/DocToc.php | 48 ------------------------------
 1 file changed, 48 deletions(-)
 delete mode 100644 modules/doc/library/Doc/DocToc.php

diff --git a/modules/doc/library/Doc/DocToc.php b/modules/doc/library/Doc/DocToc.php
deleted file mode 100644
index db7d9cb03..000000000
--- a/modules/doc/library/Doc/DocToc.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-// {{{ICINGA_LICENSE_HEADER}}}
-// {{{ICINGA_LICENSE_HEADER}}}
-
-namespace Icinga\Module\Doc;
-
-use IteratorAggregate;
-use Icinga\Data\Tree\NodeInterface;
-use Icinga\Data\Tree\TreeIterator;
-
-class DocToc implements NodeInterface, IteratorAggregate
-{
-    protected $children = array();
-
-    protected $value;
-
-    public function __construct($value = null)
-    {
-        $this->value = $value;
-    }
-
-    public function getValue()
-    {
-        return $this->value;
-    }
-
-    public function appendChild($value)
-    {
-        $child = new self($value);
-        $this->children[] = $child;
-        return $child;
-    }
-
-    public function hasChildren()
-    {
-        return ! empty($this->children);
-    }
-
-    public function getChildren()
-    {
-        return $this->children;
-    }
-
-    public function getIterator()
-    {
-        return new TreeIterator($this);
-    }
-}

From 625a6dae404420998163416ca7240c4c1ba1f0d9 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 13:59:59 +0200
Subject: [PATCH 50/96] doc: Remove DocTocHtmlRenderer as it's superseded by
 Data/Tree/NodeRenderer

refs #4820
---
 .../doc/library/Doc/DocTocHtmlRenderer.php    | 45 -------------------
 1 file changed, 45 deletions(-)
 delete mode 100644 modules/doc/library/Doc/DocTocHtmlRenderer.php

diff --git a/modules/doc/library/Doc/DocTocHtmlRenderer.php b/modules/doc/library/Doc/DocTocHtmlRenderer.php
deleted file mode 100644
index e402f3eb4..000000000
--- a/modules/doc/library/Doc/DocTocHtmlRenderer.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-// {{{ICINGA_LICENSE_HEADER}}}
-// {{{ICINGA_LICENSE_HEADER}}
-
-namespace Icinga\Module\Doc;
-
-use RecursiveIteratorIterator;
-
-class DocTocHtmlRenderer extends RecursiveIteratorIterator
-{
-    protected $html = array();
-
-    public function __construct(DocToc $toc)
-    {
-        parent::__construct($toc, RecursiveIteratorIterator::SELF_FIRST);
-    }
-
-    public function beginIteration()
-    {
-        $this->html[] = '<ul>';
-    }
-
-    public function endIteration()
-    {
-        $this->html[] = '</ul>';
-    }
-
-    public function beginChildren()
-    {
-        $this->html[] = '<ul>';
-    }
-
-    public function endChildren()
-    {
-        $this->html[] = '</ul>';
-    }
-
-    public function render($callback)
-    {
-        foreach ($this as $node) {
-            $this->html[] = $callback($node->getValue());
-        }
-        return implode("\n", $this->html);
-    }
-}

From 07330c1ca90b7f0e380927c3866c5aff7443f0b2 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 14:09:12 +0200
Subject: [PATCH 51/96] doc: Don't use absolute 'use' in MarkdownFileIterator

refs #4820
---
 modules/doc/library/Doc/MarkdownFileIterator.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/doc/library/Doc/MarkdownFileIterator.php b/modules/doc/library/Doc/MarkdownFileIterator.php
index e8d982ba6..b68195a3e 100644
--- a/modules/doc/library/Doc/MarkdownFileIterator.php
+++ b/modules/doc/library/Doc/MarkdownFileIterator.php
@@ -4,7 +4,7 @@
 
 namespace Icinga\Module\Doc;
 
-use \RecursiveFilterIterator;
+use RecursiveFilterIterator;
 
 /**
  * Iterator over Markdown files recursively

From d446e0db2e860b5e9002164b3fad6ee4e1c5ab2b Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 14:10:13 +0200
Subject: [PATCH 52/96] doc: Implement DocParser::getChapter()

refs #4820
---
 modules/doc/library/Doc/DocIterator.php |  61 ++++++++++
 modules/doc/library/Doc/DocParser.php   | 143 +++++++++---------------
 2 files changed, 111 insertions(+), 93 deletions(-)
 create mode 100644 modules/doc/library/Doc/DocIterator.php

diff --git a/modules/doc/library/Doc/DocIterator.php b/modules/doc/library/Doc/DocIterator.php
new file mode 100644
index 000000000..07a49732f
--- /dev/null
+++ b/modules/doc/library/Doc/DocIterator.php
@@ -0,0 +1,61 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use ArrayIterator;
+use RunetimeException;
+
+class FileLockingIterator extends ArrayIterator
+{
+    public function next()
+    {
+        $this->current()->flock(LOCK_UN);
+        parent::next();
+    }
+
+    public function valid()
+    {
+        if (!parent::valid()) {
+            return false;
+        }
+        $fileInfo = $this->current();
+        try {
+            $fileObject = $fileInfo->openFile();
+        } catch (RuntimeException $e) {
+            throw new DocException($e->getMessage());
+        }
+        if ($fileObject->flock(LOCK_SH) === false) {
+            throw new DocException('Couldn\'t get the lock');
+        }
+        $this[$this->key()] = $fileObject;
+        return true;
+    }
+}
+
+use IteratorAggregate;
+use RecursiveIteratorIterator;
+use RecursiveDirectoryIterator;
+
+class DocIterator implements IteratorAggregate
+{
+    protected $fileInfos;
+
+    public function __construct($path)
+    {
+        $iter = new RecursiveIteratorIterator(
+            new MarkdownFileIterator(
+                new RecursiveDirectoryIterator($path)
+            )
+        );
+        $fileInfos = iterator_to_array($iter);
+        natcasesort($fileInfos);
+        $this->fileInfos = $fileInfos;
+    }
+
+    public function getIterator()
+    {
+        return new FileLockingIterator($this->fileInfos);
+    }
+}
diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 3eb97794d..1d77f47b1 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -1,68 +1,13 @@
 <?php
 // {{{ICINGA_LICENSE_HEADER}}}
-// {{{ICINGA_LICENSE_HEADER}}
+// {{{ICINGA_LICENSE_HEADER}}}
 
 namespace Icinga\Module\Doc;
 
 require_once 'vendor/Parsedown/Parsedown.php';
 
-use ArrayIterator;
-use RunetimeException;
-
-class FileLockingIterator extends ArrayIterator
-{
-    public function next()
-    {
-        $this->current()->flock(LOCK_UN);
-        parent::next();
-    }
-
-    public function valid()
-    {
-        if (!parent::valid()) {
-            return false;
-        }
-        $fileInfo = $this->current();
-        try {
-            $fileObject = $fileInfo->openFile();
-        } catch (RuntimeException $e) {
-            throw new DocException($e->getMessage());
-        }
-        if ($fileObject->flock(LOCK_SH) === false) {
-            throw new DocException('Couldn\'t get the lock');
-        }
-        $this[$this->key()] = $fileObject;
-        return true;
-    }
-}
-
-use IteratorAggregate;
-use RecursiveIteratorIterator;
-use RecursiveDirectoryIterator;
-
-class DocIterator implements IteratorAggregate
-{
-    protected $fileInfos;
-
-    public function __construct($path)
-    {
-        $iter = new RecursiveIteratorIterator(
-            new MarkdownFileIterator(
-                new RecursiveDirectoryIterator($path)
-            )
-        );
-        $fileInfos = iterator_to_array($iter);
-        natcasesort($fileInfos);
-        $this->fileInfos = $fileInfos;
-    }
-
-    public function getIterator()
-    {
-        return new FileLockingIterator($this->fileInfos);
-    }
-}
-
 use Parsedown;
+use Icinga\Data\Tree\Node;
 use Icinga\Exception\NotReadableError;
 
 /**
@@ -88,10 +33,10 @@ class DocParser
     public function __construct($path)
     {
         if (! is_dir($path)) {
-            throw new DocException('Doc directory `' . $path .'\' does not exist');
+            throw new DocException('Doc directory `' . $path . '\' does not exist');
         }
         if (! is_readable($path)) {
-            throw new NotReadableError('Doc directory `' . $path .'\' is not readable');
+            throw new NotReadableError('Doc directory `' . $path . '\' is not readable');
         }
         $this->path = $path;
     }
@@ -99,16 +44,17 @@ class DocParser
     /**
      * Retrieve the table of contents
      *
-     * @return  DocTocHtmlRenderer
+     * @return Node
      */
     public function getToc()
     {
         $tocStack = array((object) array(
             'level' => 0,
-            'node'  => new DocToc()
+            'node'  => new Node()
         ));
         foreach (new DocIterator($this->path) as $fileObject) {
             $line = null;
+            $currentChapterName = null;
             while (! $fileObject->eof()) {
                 // Save last line for setext-style headers
                 $lastLine = $line;
@@ -129,11 +75,17 @@ class DocParser
                         $nofollow = true;
                     }
                     $id = urlencode(str_replace('.', '&#46;', strip_tags($id)));
+                    if ($currentChapterName === null) {
+                        // The first header is the chapter's name
+                        $currentChapterName = $id;
+                        $id = null;
+                    }
                     $node = end($tocStack)->node->appendChild(
                         (object) array(
-                            'id'        => $id,
-                            'title'     => $header,
-                            'nofollow'  => $nofollow
+                            'id'            => $id,
+                            'title'         => $header,
+                            'nofollow'      => $nofollow,
+                            'chapterName'   => $currentChapterName
                         )
                     );
                     $tocStack[] = (object) array(
@@ -143,72 +95,77 @@ class DocParser
                 }
             }
         }
-        return new DocTocHtmlRenderer($tocStack[0]->node);
+        return $tocStack[0]->node;
     }
 
     /**
-     * Retrieve doc as HTML converted from markdown files sorted by filename and the table of contents
+     * Retrieve a chapter
      *
-     * @return  array
-     * @throws  DocException
+     * @param   string $chapterName
+     *
+     * @return  string
      */
-    public function getDocAndToc()
+    public function getChapter($chapterName)
     {
         $cat = array();
         $tocStack = array((object) array(
             'level' => 0,
-            'node'  => new DocToc()
+            'node'  => new Node()
         ));
+        $chapterFound = false;
         foreach (new DocIterator($this->path) as $fileObject) {
             $line = null;
-            $sectionTitle = null;
+            $currentChapterName = null;
+            $chapter = array();
             while (! $fileObject->eof()) {
                 // Save last line for setext-style headers
-                $lastLine   = $line;
-                $line       = $fileObject->fgets();
-                $header     = $this->extractHeader($line, $lastLine);
+                $lastLine = $line;
+                $line = $fileObject->fgets();
+                $header = $this->extractHeader($line, $lastLine);
                 if ($header !== null) {
                     list($header, $level) = $header;
-                    if ($sectionTitle === null) {
-                        // The first header is the section's title
-                        $sectionTitle = $header;
-                    }
-                    $id         = $this->extractHeaderId($header);
-                    $nofollow   = false;
+                    $id = $this->extractHeaderId($header);
                     $this->reduceToc($tocStack, $level);
                     if ($id === null) {
                         $path = array();
                         foreach (array_slice($tocStack, 1) as $entity) {
                             $path[] = $entity->node->getValue()->title;
                         }
-                        $path[]         = $header;
-                        $id             = implode('-', $path);
-                        $nofollow       = true;
+                        $path[] = $header;
+                        $id = implode('-', $path);
                     }
-                    $id     = urlencode(str_replace('.', '&#46;', strip_tags($id)));
-                    $node   = end($tocStack)->node->appendChild(
+                    $id = urlencode(str_replace('.', '&#46;', strip_tags($id)));
+                    if ($currentChapterName === null) {
+                        $currentChapterName = $id;
+                        $id = null;
+                    }
+                    $node = end($tocStack)->node->appendChild(
                         (object) array(
-                            'id'        => $id,
-                            'title'     => $header,
-                            'nofollow'  => $nofollow
+                            'title' => $header
                         )
                     );
-                    $tocStack[]  = (object) array(
+                    $tocStack[] = (object) array(
                         'level' => $level,
                         'node'  => $node
                     );
                     $line = '<a name="' . $id . '"></a>' . PHP_EOL . $line;
                 }
-                $cat[] = $line;
+                $chapter[] = $line;
+            }
+            if ($currentChapterName === $chapterName) {
+                $chapterFound = true;
+                $cat = $chapter;
+            }
+            if (! $chapterFound) {
+                $cat = array_merge($cat, $chapter);
             }
         }
-        $html = Parsedown::instance()->text(implode('', $cat));
         $html = preg_replace_callback(
             '#<pre><code class="language-php">(.*?)\</code></pre>#s',
             array($this, 'highlight'),
-            $html
+            Parsedown::instance()->text(implode('', $cat))
         );
-        return array($html, new DocTocHtmlRenderer($tocStack[0]->node));
+        return $html;
     }
 
     /**

From 16d5d65a05591e18ba721f67abe0a039bf0d7fcb Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 14:10:35 +0200
Subject: [PATCH 53/96] doc: Fix actions and view scripts DocParser usage

refs #4820
---
 .../controllers/IcingawebController.php       |  2 +-
 .../controllers/IndexController.php           |  1 -
 .../controllers/ModuleController.php          |  3 +-
 .../views/scripts/icingaweb/chapter.phtml     | 38 +++++++++++++++++++
 .../views/scripts/icingaweb/toc.phtml         | 26 ++++++++-----
 .../views/scripts/index/index.phtml           |  4 +-
 .../views/scripts/module/chapter.phtml        | 38 +++++++++++++++++++
 .../views/scripts/module/index.phtml          | 14 +++----
 .../views/scripts/module/toc.phtml            | 30 +++++++++------
 .../views/scripts/partials/chapter.phtml      |  1 -
 modules/doc/library/Doc/DocController.php     | 14 +++----
 11 files changed, 131 insertions(+), 40 deletions(-)
 create mode 100644 modules/doc/application/views/scripts/icingaweb/chapter.phtml
 create mode 100644 modules/doc/application/views/scripts/module/chapter.phtml
 delete mode 100644 modules/doc/application/views/scripts/partials/chapter.phtml

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
index 0820a82ca..092de4025 100644
--- a/modules/doc/application/controllers/IcingawebController.php
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -27,6 +27,6 @@ class Doc_IcingawebController extends DocController
         if ($chapterName === null) {
             throw new Zend_Controller_Action_Exception('Missing parameter "chapterName"', 404);
         }
-        $this->renderChapter($chapterName, Icinga::app()->getApplicationDir('/../doc'));
+        $this->populateChapter($chapterName, Icinga::app()->getApplicationDir('/../doc'));
     }
 }
diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php
index aa44c9c5d..63b5e8cdf 100644
--- a/modules/doc/application/controllers/IndexController.php
+++ b/modules/doc/application/controllers/IndexController.php
@@ -8,6 +8,5 @@ class Doc_IndexController extends DocController
 {
     public function indexAction()
     {
-
     }
 }
diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index b864ff324..07092c4ed 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -63,6 +63,7 @@ class Doc_ModuleController extends DocController
             throw new Zend_Controller_Action_Exception('Missing parameter "chapterName"', 404);
         }
         $moduleManager = Icinga::app()->getModuleManager();
-        $this->renderChapter($chapterName, $moduleManager->getModuleDir($moduleName, '/doc'));
+        $this->populateChapter($chapterName, $moduleManager->getModuleDir($moduleName, '/doc'));
+        $this->view->moduleName = $moduleName;
     }
 }
diff --git a/modules/doc/application/views/scripts/icingaweb/chapter.phtml b/modules/doc/application/views/scripts/icingaweb/chapter.phtml
new file mode 100644
index 000000000..328d0bebe
--- /dev/null
+++ b/modules/doc/application/views/scripts/icingaweb/chapter.phtml
@@ -0,0 +1,38 @@
+<?php
+$urlHelper = $this->getHelper('Url');
+$view = $this;
+?>
+<?= preg_replace_callback(
+    '/<a\s+(?P<attribs>[^>]*?\s+)?href="#(?P<fragment>[^"]+)"/im',
+    function($match) use ($toc, $urlHelper, $view) {
+        if (($node = $toc->findNodeBy(function ($node) use ($match) {
+            $section = $node->getValue();
+            if (($section->id === null && $section->chapterName === $match['fragment'])
+                || $section->id === $match['fragment']
+            ) {
+                return true;
+            }
+            return false;
+        }))) {
+            $section = $node->getValue();
+            $path = $urlHelper->url(
+                array('chapterName' => $section->chapterName),
+                'doc/icingaweb/chapter',
+                false,
+                false
+            );
+            $url = $view->url($path);
+            if ($section->id) {
+                $url->setAnchor($section->id);
+            }
+            return sprintf(
+                '<a %s%shref="%s"',
+                strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
+                $section->nofollow ? 'rel="nofollow" ' : '',
+                $url->getAbsoluteUrl()
+            );
+        }
+        return $match[0];
+    },
+    $chapterHtml
+); ?>
diff --git a/modules/doc/application/views/scripts/icingaweb/toc.phtml b/modules/doc/application/views/scripts/icingaweb/toc.phtml
index 065071f01..bf91ffc77 100644
--- a/modules/doc/application/views/scripts/icingaweb/toc.phtml
+++ b/modules/doc/application/views/scripts/icingaweb/toc.phtml
@@ -1,15 +1,23 @@
 <div class="controls">
-  <h1><?= $docName ?> documentation</h1>
+    <h1><?= $docName ?> documentation</h1>
 </div>
 <div class="content" data-base-target="_next">
-  <?php
+    <?php
     $urlHelper = $this->getHelper('Url');
     $view = $this;
-  ?>
-  <?= $tocRenderer->render(function ($section) use ($urlHelper, $view) {
-    // Chapter name is not yet defined
-    $path = $urlHelper->url(array('chapterName' => 'tbd'), 'doc/icingaweb/chapter', false, false);
-    $url = $view->url($path)->setAnchor($section->id);
-    return sprintf('<li><a href="%s">%s</a></li>', $url->getAbsoluteUrl(), $section->title);
-  }); ?>
+    ?>
+    <?= $tocRenderer->render(function ($node) use ($urlHelper, $view) {
+        $section = $node->getValue();
+        $path = $urlHelper->url(array('chapterName' => $section->chapterName), 'doc/icingaweb/chapter', false, false);
+        $url = $view->url($path);
+        if ($section->id) {
+            $url->setAnchor($section->id);
+        }
+        return sprintf(
+            '<li><a %shref="%s">%s</a></li>',
+            $section->nofollow ? 'rel="nofollow" ' : '',
+            $url->getAbsoluteUrl(),
+            $section->title
+        );
+    }); ?>
 </div>
diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml
index 4381bc2a0..3cebdca69 100644
--- a/modules/doc/application/views/scripts/index/index.phtml
+++ b/modules/doc/application/views/scripts/index/index.phtml
@@ -1,5 +1,5 @@
 <h1>Available documentations</h1>
 <ul>
-  <li><a href="<?= $this->href('doc/icingaweb/toc'); ?>">Icinga Web 2</a></li>
-  <li><a href="<?= $this->href('doc/module/'); ?>">Module documentations</a></li>
+    <li><a href="<?= $this->href('doc/icingaweb/toc'); ?>">Icinga Web 2</a></li>
+    <li><a href="<?= $this->href('doc/module/'); ?>">Module documentations</a></li>
 </ul>
diff --git a/modules/doc/application/views/scripts/module/chapter.phtml b/modules/doc/application/views/scripts/module/chapter.phtml
new file mode 100644
index 000000000..59a6ecb33
--- /dev/null
+++ b/modules/doc/application/views/scripts/module/chapter.phtml
@@ -0,0 +1,38 @@
+<?php
+$urlHelper = $this->getHelper('Url');
+$view = $this;
+?>
+<?= preg_replace_callback(
+    '/<a\s+(?P<attribs>[^>]*?\s+)?href="#(?P<fragment>[^"]+)"/im',
+    function($match) use ($toc, $moduleName, $urlHelper, $view) {
+        if (($node = $toc->findNodeBy(function ($node) use ($match) {
+            $section = $node->getValue();
+            if (($section->id === null && $section->chapterName === $match['fragment'])
+                || $section->id === $match['fragment']
+            ) {
+                return true;
+            }
+            return false;
+        }))) {
+            $section = $node->getValue();
+            $path = $urlHelper->url(
+                array('moduleName' => $moduleName, 'chapterName' => $section->chapterName),
+                'doc/module/chapter',
+                false,
+                false
+            );
+            $url = $view->url($path);
+            if ($section->id) {
+                $url->setAnchor($section->id);
+            }
+            return sprintf(
+                '<a %s%shref="%s"',
+                strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
+                $section->nofollow ? 'rel="nofollow" ' : '',
+                $url->getAbsoluteUrl()
+            );
+        }
+        return $match[0];
+    },
+    $chapterHtml
+); ?>
diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml
index bbd74345a..c3522c4c2 100644
--- a/modules/doc/application/views/scripts/module/index.phtml
+++ b/modules/doc/application/views/scripts/module/index.phtml
@@ -1,10 +1,10 @@
 <h1>Module documentations</h1>
 <ul>
-<?php foreach ($enabledModules as $module): ?>
-  <li>
-    <a href="<?= $this->getHelper('Url')->url(array('moduleName' => $module), 'doc/module/toc', false, false); ?>">
-      <?= $module ?>
-    </a>
-  </li>
-<?php endforeach ?>
+    <?php foreach ($enabledModules as $module): ?>
+        <li>
+            <a href="<?= $this->getHelper('Url')->url(array('moduleName' => $module), 'doc/module/toc', false, false); ?>">
+                <?= $module ?>
+            </a>
+        </li>
+    <?php endforeach ?>
 </ul>
diff --git a/modules/doc/application/views/scripts/module/toc.phtml b/modules/doc/application/views/scripts/module/toc.phtml
index b3be77d79..ec33c507c 100644
--- a/modules/doc/application/views/scripts/module/toc.phtml
+++ b/modules/doc/application/views/scripts/module/toc.phtml
@@ -1,17 +1,25 @@
 <div class="controls">
-  <h1><?= $docName ?> documentation</h1>
+    <h1><?= $docName ?> documentation</h1>
 </div>
 <div class="content" data-base-target="_next">
-  <?php
+    <?php
     $urlHelper = $this->getHelper('Url');
     $view = $this;
-  ?>
-  <?= $tocRenderer->render(function ($section) use ($urlHelper, $view, $moduleName) {
-    // Chapter name is not yet defined
-    $path = $urlHelper->url(
-      array('moduleName' => $moduleName, 'chapterName' => 'tbd'), 'doc/module/chapter', false, false
-    );
-    $url = $view->url($path)->setAnchor($section->id);
-    return sprintf('<li><a href="%s">%s</a></li>', $url->getAbsoluteUrl(), $section->title);
-  }); ?>
+    ?>
+    <?= $tocRenderer->render(function ($node) use ($urlHelper, $view, $moduleName) {
+        $section = $node->getValue();
+        $path = $urlHelper->url(
+            array('moduleName' => $moduleName, 'chapterName' => $section->chapterName), 'doc/module/chapter', false, false
+        );
+        $url = $view->url($path);
+        if ($section->id) {
+            $url->setAnchor($section->id);
+        }
+        return sprintf(
+            '<li><a %shref="%s">%s</a></li>',
+            $section->nofollow ? 'rel="nofollow" ' : '',
+            $url->getAbsoluteUrl(),
+            $section->title
+        );
+    }); ?>
 </div>
diff --git a/modules/doc/application/views/scripts/partials/chapter.phtml b/modules/doc/application/views/scripts/partials/chapter.phtml
deleted file mode 100644
index b0e1fe0cb..000000000
--- a/modules/doc/application/views/scripts/partials/chapter.phtml
+++ /dev/null
@@ -1 +0,0 @@
-<?= $chapterHtml ?>
diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
index ef44de478..bc335f381 100644
--- a/modules/doc/library/Doc/DocController.php
+++ b/modules/doc/library/Doc/DocController.php
@@ -4,22 +4,22 @@
 
 namespace Icinga\Module\Doc;
 
+use Icinga\Data\Tree\NodeRenderer;
 use Icinga\Web\Controller\ActionController;
 
 class DocController extends ActionController
 {
     /**
-     * Render a chapter
+     * Populate a chapter
      *
      * @param string $chapterName   Name of the chapter
      * @param string $path          Path to the documentation
      */
-    protected function renderChapter($chapterName, $path)
+    protected function populateChapter($chapterName, $path)
     {
         $parser = new DocParser($path);
-        list($docHtml, $docToc) = $parser->getDocAndToc();
-        $this->view->chapterHtml = $docHtml;
-        $this->_helper->viewRenderer('partials/chapter', null, true);
+        $this->view->chapterHtml = $parser->getChapter($chapterName);
+        $this->view->toc = $parser->getToc();
     }
 
     /**
@@ -31,8 +31,8 @@ class DocController extends ActionController
     protected function populateToc($path, $name)
     {
         $parser = new DocParser($path);
-        list($docHtml, $tocRenderer) = $parser->getDocAndToc();
-        $this->view->tocRenderer = $tocRenderer;
+        $toc = $parser->getToc();
+        $this->view->tocRenderer = new NodeRenderer($toc);
         $this->view->docName = $name;
     }
 }

From 61ac3b0168cd2979bd20012f35de9c95ee6637b7 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 6 Jun 2014 14:45:13 +0200
Subject: [PATCH 54/96] doc: Fix translation exceptions

refs #6432
refs #4820
---
 modules/doc/application/locale/.gitkeep | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 modules/doc/application/locale/.gitkeep

diff --git a/modules/doc/application/locale/.gitkeep b/modules/doc/application/locale/.gitkeep
new file mode 100644
index 000000000..9fc47527c
--- /dev/null
+++ b/modules/doc/application/locale/.gitkeep
@@ -0,0 +1 @@
+Remove this file and the locale directory once #6432 has been fixed.

From 7f6010e1f8b1cca800627088428c581b36f9ea38 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 13 Jun 2014 17:22:43 +0200
Subject: [PATCH 55/96] lib/tree: Add PHPDoc to Node's methods

---
 library/Icinga/Data/Tree/Node.php          | 55 ++++++++++++++++++++--
 library/Icinga/Data/Tree/NodeInterface.php |  6 +--
 2 files changed, 54 insertions(+), 7 deletions(-)

diff --git a/library/Icinga/Data/Tree/Node.php b/library/Icinga/Data/Tree/Node.php
index db3a49749..b8f9e8dad 100644
--- a/library/Icinga/Data/Tree/Node.php
+++ b/library/Icinga/Data/Tree/Node.php
@@ -4,24 +4,47 @@
 
 namespace Icinga\Data\Tree;
 
+use Exception;
 use RecursiveIteratorIterator;
 use RuntimeException;
 use SplDoublyLinkedList;
 
 class Node extends SplDoublyLinkedList implements NodeInterface
 {
+    /**
+     * The node's value
+     *
+     * @var mixed
+     */
     protected $value;
 
+    /**
+     * Create a new node
+     *
+     * @param mixed $value The node's value
+     */
     public function __construct($value = null)
     {
         $this->value = $value;
     }
 
+    /**
+     * Get the node's value
+     *
+     * @return mixed
+     */
     public function getValue()
     {
         return $this->value;
     }
 
+    /**
+     * Create a new node from the given value and insert the node as the last child of this node
+     *
+     * @param   mixed           $value  The node's value
+     *
+     * @return  NodeInterface           The appended node
+     */
     public function appendChild($value)
     {
         $child = new static($value);
@@ -29,31 +52,55 @@ class Node extends SplDoublyLinkedList implements NodeInterface
         return $child;
     }
 
+    /**
+     * Whether this node has child nodes
+     *
+     * @return bool
+     */
     public function hasChildren()
     {
         $current = $this->current();
-        if ($current === null) {;
+        if ($current === null) {
             $current = $this;
         }
         return ! $current->isEmpty();
     }
 
+    /**
+     * Get the node's child nodes
+     *
+     * @return NodeInterface
+     */
     public function getChildren()
     {
         $current = $this->current();
-        if ($current === null) {;
+        if ($current === null) {
             $current = $this;
         }
         return $current;
     }
 
+    /**
+     * Find the first child node by searching through nodes deeper than the immediate children using a custom function
+     *
+     * @param   $callback
+     *
+     * @return  NodeInterface|null
+     * @throws  Exception
+     */
     public function findNodeBy($callback)
     {
         if (! is_callable($callback)) {
-            throw new RuntimeException();
+            throw new RuntimeException('Callable expected');
         }
         foreach (new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST) as $node) {
-            if (call_user_func($callback, $node)) {
+            try {
+                $found = call_user_func($callback, $node);
+            } catch (Exception $e) {
+                // TODO(el): Log exception and return false instead?
+                throw $e;
+            }
+            if ($found) {
                 return $node;
             }
         }
diff --git a/library/Icinga/Data/Tree/NodeInterface.php b/library/Icinga/Data/Tree/NodeInterface.php
index 1d96973e0..6953214dc 100644
--- a/library/Icinga/Data/Tree/NodeInterface.php
+++ b/library/Icinga/Data/Tree/NodeInterface.php
@@ -9,11 +9,11 @@ use RecursiveIterator;
 interface NodeInterface extends RecursiveIterator
 {
     /**
-     * Append a child to the node
+     * Create a new node from the given value and insert the node as the last child of this node
      *
-     * @param   mixed $value
+     * @param   mixed           $value  The node's value
      *
-     * @return  self
+     * @return  NodeInterface           The appended node
      */
     public function appendChild($value);
 

From 8fe1d49ce94161d37ce643a05b4ddaff6dd2dc2c Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Fri, 13 Jun 2014 17:23:20 +0200
Subject: [PATCH 56/96] doc/cs: Begin additional if conditions on a new line

---
 modules/doc/library/Doc/DocParser.php         | 19 +++++++++----------
 .../doc/library/Doc/MarkdownFileIterator.php  |  2 +-
 2 files changed, 10 insertions(+), 11 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 40a6c298e..728dd1c08 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -160,12 +160,11 @@ class DocParser
                 $cat = array_merge($cat, $chapter);
             }
         }
-        $html = preg_replace_callback(
+        return preg_replace_callback(
             '#<pre><code class="language-php">(.*?)\</code></pre>#s',
             array($this, 'highlight'),
             Parsedown::instance()->text(implode('', $cat))
         );
-        return $html;
     }
 
     /**
@@ -194,9 +193,9 @@ class DocParser
             return null;
         }
         $header = null;
-        if ($line &&
-            $line[0] === '#' &&
-            preg_match('/^#+/', $line, $match) === 1
+        if ($line
+            && $line[0] === '#'
+            && preg_match('/^#+/', $line, $match) === 1
         ) {
             // Atx-style
             $level  = strlen($match[0]);
@@ -205,9 +204,9 @@ class DocParser
                 return null;
             }
         } elseif (
-            $line &&
-            ($line[0] === '=' || $line[0] === '-') &&
-            preg_match('/^[=-]+\s*$/', $line, $match) === 1
+            $line
+            && ($line[0] === '=' || $line[0] === '-')
+            && preg_match('/^[=-]+\s*$/', $line, $match) === 1
         ) {
             // Setext
             $header = trim($lastLine);
@@ -235,8 +234,8 @@ class DocParser
      */
     protected function extractHeaderId(&$header)
     {
-        if ($header[0] === '<' &&
-            preg_match('#(?:<(?P<tag>a|span) id="(?P<id>.+)"></(?P=tag)>)#u', $header, $match)
+        if ($header[0] === '<'
+            && preg_match('#(?:<(?P<tag>a|span) id="(?P<id>.+)"></(?P=tag)>)#u', $header, $match)
         ) {
             $header = str_replace($match[0], '', $header);
             return $match['id'];
diff --git a/modules/doc/library/Doc/MarkdownFileIterator.php b/modules/doc/library/Doc/MarkdownFileIterator.php
index b68195a3e..2fc8cef1b 100644
--- a/modules/doc/library/Doc/MarkdownFileIterator.php
+++ b/modules/doc/library/Doc/MarkdownFileIterator.php
@@ -20,7 +20,7 @@ class MarkdownFileIterator extends RecursiveFilterIterator
     public function accept()
     {
         $current = $this->getInnerIterator()->current();
-        if (!$current->isFile()) {
+        if (! $current->isFile()) {
             return false;
         }
         $filename = $current->getFilename();

From e73471030c946229f62eb5bbc4fef3ec7de72b0d Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 30 Jun 2014 11:43:25 +0200
Subject: [PATCH 57/96] doc/modules/index: Only list modules which have the
 'doc' directory

refs #4820
---
 .../application/controllers/ModuleController.php   | 14 +++++++++++---
 .../application/views/scripts/module/index.phtml   |  2 +-
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index 07092c4ed..e8b7c2fdd 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -2,18 +2,26 @@
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
-use \Zend_Controller_Action_Exception;
+use Zend_Controller_Action_Exception;
 use Icinga\Application\Icinga;
 use Icinga\Module\Doc\DocController;
 
 class Doc_ModuleController extends DocController
 {
     /**
-     * List available modules
+     * List modules which are enabled and having the 'doc' directory
      */
     public function indexAction()
     {
-        $this->view->enabledModules = Icinga::app()->getModuleManager()->listEnabledModules();
+        $moduleManager = Icinga::app()->getModuleManager();
+        $modules = array();
+        foreach (Icinga::app()->getModuleManager()->listEnabledModules() as $enabledModule) {
+            $docDir = $moduleManager->getModuleDir($enabledModule, '/doc');
+            if (is_dir($docDir)) {
+                $modules[] = $enabledModule;
+            }
+        }
+        $this->view->modules = $modules;
     }
 
     /**
diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml
index c3522c4c2..0755748c7 100644
--- a/modules/doc/application/views/scripts/module/index.phtml
+++ b/modules/doc/application/views/scripts/module/index.phtml
@@ -1,6 +1,6 @@
 <h1>Module documentations</h1>
 <ul>
-    <?php foreach ($enabledModules as $module): ?>
+    <?php foreach ($modules as $module): ?>
         <li>
             <a href="<?= $this->getHelper('Url')->url(array('moduleName' => $module), 'doc/module/toc', false, false); ?>">
                 <?= $module ?>

From 93cc24a93a87d5a2548486021e816884f1db6ff7 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 30 Jun 2014 15:18:22 +0200
Subject: [PATCH 58/96] doc module: Move `DocException' beneath `Exception'
 namespace

There are more exceptions to come which also will be beneath the `Exception' namespace.

refs #4820
---
 modules/doc/library/Doc/{ => Exception}/DocException.php | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
 rename modules/doc/library/Doc/{ => Exception}/DocException.php (52%)

diff --git a/modules/doc/library/Doc/DocException.php b/modules/doc/library/Doc/Exception/DocException.php
similarity index 52%
rename from modules/doc/library/Doc/DocException.php
rename to modules/doc/library/Doc/Exception/DocException.php
index 795c9a5f9..f684f1faa 100644
--- a/modules/doc/library/Doc/DocException.php
+++ b/modules/doc/library/Doc/Exception/DocException.php
@@ -2,10 +2,13 @@
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
-namespace Icinga\Module\Doc;
+namespace Icinga\Module\Doc\Exception;
 
 use Exception;
 
+/**
+ * Exception thrown if an error in the documentation module occurs
+ */
 class DocException extends Exception
 {
 }

From 0382e2265c6c14594fe40321d91dc518407655bb Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 30 Jun 2014 15:21:16 +0200
Subject: [PATCH 59/96] doc module: Add `ChapterNotFoundException'

refs #4820
---
 .../Doc/Exception/ChapterNotFoundException.php       | 12 ++++++++++++
 1 file changed, 12 insertions(+)
 create mode 100644 modules/doc/library/Doc/Exception/ChapterNotFoundException.php

diff --git a/modules/doc/library/Doc/Exception/ChapterNotFoundException.php b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php
new file mode 100644
index 000000000..1e9bcf7e9
--- /dev/null
+++ b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php
@@ -0,0 +1,12 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc\Exception;
+
+/**
+ * Exception thrown if a chapter was not found
+ */
+class ChapterNotFoundException extends DocException
+{
+}

From a8e6dda78349257a303a4429bbba97523c969a3b Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 30 Jun 2014 15:21:35 +0200
Subject: [PATCH 60/96] doc module: Add `DocEmptyException'

refs #4820
---
 .../doc/library/Doc/Exception/DocEmptyException.php  | 12 ++++++++++++
 1 file changed, 12 insertions(+)
 create mode 100644 modules/doc/library/Doc/Exception/DocEmptyException.php

diff --git a/modules/doc/library/Doc/Exception/DocEmptyException.php b/modules/doc/library/Doc/Exception/DocEmptyException.php
new file mode 100644
index 000000000..032a01744
--- /dev/null
+++ b/modules/doc/library/Doc/Exception/DocEmptyException.php
@@ -0,0 +1,12 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc\Exception;
+
+/**
+ * Exception thrown if a documentation directory is empty
+ */
+class DocEmptyException extends DocException
+{
+}

From c48f7f9fba6a07f869009d03b5d6f33f2ceab0fb Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 30 Jun 2014 15:22:32 +0200
Subject: [PATCH 61/96] doc module: Add `DocIterator::count()'

Method used to count available markdown documentation files.

refs #4820
---
 modules/doc/library/Doc/DocIterator.php | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/modules/doc/library/Doc/DocIterator.php b/modules/doc/library/Doc/DocIterator.php
index 07a49732f..1a229c918 100644
--- a/modules/doc/library/Doc/DocIterator.php
+++ b/modules/doc/library/Doc/DocIterator.php
@@ -34,11 +34,12 @@ class FileLockingIterator extends ArrayIterator
     }
 }
 
+use Countable;
 use IteratorAggregate;
 use RecursiveIteratorIterator;
 use RecursiveDirectoryIterator;
 
-class DocIterator implements IteratorAggregate
+class DocIterator implements Countable, IteratorAggregate
 {
     protected $fileInfos;
 
@@ -54,6 +55,11 @@ class DocIterator implements IteratorAggregate
         $this->fileInfos = $fileInfos;
     }
 
+    public function count()
+    {
+        return count($this->fileInfos);
+    }
+
     public function getIterator()
     {
         return new FileLockingIterator($this->fileInfos);

From 1bbfa9b9cadfe8ac0179375a714565f0ed1c36fe Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 30 Jun 2014 15:24:40 +0200
Subject: [PATCH 62/96] doc module: Let `DocParser' throw `DocEmptyException'
 and `ChapterNotFound' exception

`DocEmptyException' is thrown during construction when a documentation directory is empty.
`ChapterNotFound' is thrown when a chapter was not found when calling `getChapter()'.

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 45 +++++++++++++++++----------
 1 file changed, 28 insertions(+), 17 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 728dd1c08..c213c764a 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -9,6 +9,9 @@ require_once 'IcingaVendor/Parsedown/Parsedown.php';
 use Parsedown;
 use Icinga\Data\Tree\Node;
 use Icinga\Exception\NotReadableError;
+use Icinga\Module\Doc\Exception\ChapterNotFoundException;
+use Icinga\Module\Doc\Exception\DocEmptyException;
+use Icinga\Module\Doc\Exception\DocException;
 
 /**
  * Parser for documentation written in Markdown
@@ -22,13 +25,21 @@ class DocParser
      */
     protected $path;
 
+    /**
+     * Iterator over documentation files
+     *
+     * @var DocIterator
+     */
+    protected $docIterator;
+
     /**
      * Create a new documentation parser for the given path
      *
      * @param   string $path Path to the documentation
      *
-     * @throws  DocException
-     * @throws  NotReadableError
+     * @throws  DocException        If the documentation directory does not exist
+     * @throws  NotReadableError    If the documentation directory is not readable
+     * @throws  DocEmptyException   If the documentation directory is empty
      */
     public function __construct($path)
     {
@@ -38,13 +49,18 @@ class DocParser
         if (! is_readable($path)) {
             throw new NotReadableError('Doc directory `' . $path . '\' is not readable');
         }
+        $docIterator = new DocIterator($path);
+        if ($docIterator->count() === 0) {
+            throw new DocEmptyException('Doc directory `' . $path . '\' is empty');
+        }
         $this->path = $path;
+        $this->docIterator = $docIterator;
     }
 
     /**
      * Retrieve the table of contents
      *
-     * @return Node
+     * @return  Node
      */
     public function getToc()
     {
@@ -52,7 +68,7 @@ class DocParser
             'level' => 0,
             'node'  => new Node()
         ));
-        foreach (new DocIterator($this->path) as $fileObject) {
+        foreach ($this->docIterator as $fileObject) {
             $line = null;
             $currentChapterName = null;
             while (! $fileObject->eof()) {
@@ -104,16 +120,15 @@ class DocParser
      * @param   string $chapterName
      *
      * @return  string
+     * @throws  ChapterNotFoundException If the chapter was not found
      */
     public function getChapter($chapterName)
     {
-        $cat = array();
         $tocStack = array((object) array(
             'level' => 0,
             'node'  => new Node()
         ));
-        $chapterFound = false;
-        foreach (new DocIterator($this->path) as $fileObject) {
+        foreach ($this->docIterator as $fileObject) {
             $line = null;
             $currentChapterName = null;
             $chapter = array();
@@ -153,18 +168,14 @@ class DocParser
                 $chapter[] = $line;
             }
             if ($currentChapterName === $chapterName) {
-                $chapterFound = true;
-                $cat = $chapter;
-            }
-            if (! $chapterFound) {
-                $cat = array_merge($cat, $chapter);
+                return preg_replace_callback(
+                    '#<pre><code class="language-php">(.*?)\</code></pre>#s',
+                    array($this, 'highlight'),
+                    Parsedown::instance()->text(implode('', $chapter))
+                );
             }
         }
-        return preg_replace_callback(
-            '#<pre><code class="language-php">(.*?)\</code></pre>#s',
-            array($this, 'highlight'),
-            Parsedown::instance()->text(implode('', $cat))
-        );
+        throw new ChapterNotFoundException('Chapter \'' . $chapterName . '\' not found');
     }
 
     /**

From 2ea418cbe60ebca1b89e7110b7ef444bcf54e1f7 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 30 Jun 2014 15:48:43 +0200
Subject: [PATCH 63/96] doc module: Respond with 404 if a chapter was not found
 or the doc directory is empty

refs #4820
---
 .../application/controllers/ModuleController.php  | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index e8b7c2fdd..2ce1b1e73 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -2,9 +2,10 @@
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
-use Zend_Controller_Action_Exception;
+use \Zend_Controller_Action_Exception;
 use Icinga\Application\Icinga;
 use Icinga\Module\Doc\DocController;
+use Icinga\Module\Doc\Exception\DocException;
 
 class Doc_ModuleController extends DocController
 {
@@ -53,7 +54,11 @@ class Doc_ModuleController extends DocController
         $moduleName = $this->getParam('moduleName');
         $this->assertModuleEnabled($moduleName);
         $moduleManager = Icinga::app()->getModuleManager();
-        $this->populateToc($moduleManager->getModuleDir($moduleName, '/doc'), $moduleName);
+        try {
+            $this->populateToc($moduleManager->getModuleDir($moduleName, '/doc'), $moduleName);
+        } catch (DocException $e) {
+            throw new Zend_Controller_Action_Exception($e->getMessage(), 404);
+        }
         $this->view->moduleName = $moduleName;
     }
 
@@ -71,7 +76,11 @@ class Doc_ModuleController extends DocController
             throw new Zend_Controller_Action_Exception('Missing parameter "chapterName"', 404);
         }
         $moduleManager = Icinga::app()->getModuleManager();
-        $this->populateChapter($chapterName, $moduleManager->getModuleDir($moduleName, '/doc'));
+        try {
+            $this->populateChapter($chapterName, $moduleManager->getModuleDir($moduleName, '/doc'));
+        } catch (DocException $e) {
+            throw new Zend_Controller_Action_Exception($e->getMessage(), 404);
+        }
         $this->view->moduleName = $moduleName;
     }
 }

From 9af89e6ccfc32b54d643dc308f0c3a9e32eb1129 Mon Sep 17 00:00:00 2001
From: Alexander Klimov <Alexander.Klimov@netways.de>
Date: Wed, 23 Jul 2014 11:28:45 +0200
Subject: [PATCH 64/96] doc module: style h[1-6], table and code

Headers have different sizes
Tables have borders
Table headers are left-aligned
Codes are displayed inline (except inside pre) and have scrollbars if necessary

refs #6484
fixes #6632
---
 .../views/scripts/icingaweb/chapter.phtml     |  2 ++
 .../views/scripts/module/chapter.phtml        |  2 ++
 modules/doc/public/css/module.less            | 36 +++++++++++++++----
 3 files changed, 34 insertions(+), 6 deletions(-)

diff --git a/modules/doc/application/views/scripts/icingaweb/chapter.phtml b/modules/doc/application/views/scripts/icingaweb/chapter.phtml
index 328d0bebe..2ab744ebd 100644
--- a/modules/doc/application/views/scripts/icingaweb/chapter.phtml
+++ b/modules/doc/application/views/scripts/icingaweb/chapter.phtml
@@ -2,6 +2,7 @@
 $urlHelper = $this->getHelper('Url');
 $view = $this;
 ?>
+<div class="chapter">
 <?= preg_replace_callback(
     '/<a\s+(?P<attribs>[^>]*?\s+)?href="#(?P<fragment>[^"]+)"/im',
     function($match) use ($toc, $urlHelper, $view) {
@@ -36,3 +37,4 @@ $view = $this;
     },
     $chapterHtml
 ); ?>
+</div>
diff --git a/modules/doc/application/views/scripts/module/chapter.phtml b/modules/doc/application/views/scripts/module/chapter.phtml
index 59a6ecb33..3a809209f 100644
--- a/modules/doc/application/views/scripts/module/chapter.phtml
+++ b/modules/doc/application/views/scripts/module/chapter.phtml
@@ -2,6 +2,7 @@
 $urlHelper = $this->getHelper('Url');
 $view = $this;
 ?>
+<div class="chapter">
 <?= preg_replace_callback(
     '/<a\s+(?P<attribs>[^>]*?\s+)?href="#(?P<fragment>[^"]+)"/im',
     function($match) use ($toc, $moduleName, $urlHelper, $view) {
@@ -36,3 +37,4 @@ $view = $this;
     },
     $chapterHtml
 ); ?>
+</div>
diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less
index 45d97e5c6..52fff5c32 100644
--- a/modules/doc/public/css/module.less
+++ b/modules/doc/public/css/module.less
@@ -1,9 +1,33 @@
-.toc {
-  float: left;
-  width: 33.333%
+// W3C Recommendation <http://www.w3.org/TR/CSS21/sample.html> (except h4)
+h1 { font-size:    2em !important; }
+h2 { font-size:  1.5em !important; }
+h3 { font-size: 1.17em !important; }
+h4 { font-size:    1em !important; }
+h5 { font-size:  .83em !important; }
+h6 { font-size:  .75em !important; }
+
+div.chapter {
+  padding-left: 5px;
 }
 
-.doc {
-  float: left;
-  width: 66.667%
+table th {
+  text-align: left;
+}
+
+table th,
+table td {
+  border: solid 1px lightgray;
+  padding-left: 5px;
+  padding-right: 5px;
+}
+
+code {
+  width: 100%;
+  overflow-x: auto;
+  padding: 0.2em;
+  display: inline;
+}
+
+pre > code {
+  display: inline-block;
 }

From 507ac1c4d7ae1b04eeed1e806f95224ba2600dad Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 18:52:25 +0200
Subject: [PATCH 65/96] doc/MarkdownFileIterator: Nail down PHPDoc

refs #4820
---
 modules/doc/library/Doc/MarkdownFileIterator.php | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/modules/doc/library/Doc/MarkdownFileIterator.php b/modules/doc/library/Doc/MarkdownFileIterator.php
index 857d078ed..6f317ce6a 100644
--- a/modules/doc/library/Doc/MarkdownFileIterator.php
+++ b/modules/doc/library/Doc/MarkdownFileIterator.php
@@ -7,12 +7,12 @@ namespace Icinga\Module\Doc;
 use RecursiveFilterIterator;
 
 /**
- * Iterator over Markdown files recursively
+ * Recursive iterator over Markdown files
  */
 class MarkdownFileIterator extends RecursiveFilterIterator
 {
     /**
-     * Accept files with .md suffix
+     * Accept files with '.md' suffix
      *
      * @return bool Whether the current element of the iterator is acceptable
      *              through this filter
@@ -20,6 +20,7 @@ class MarkdownFileIterator extends RecursiveFilterIterator
     public function accept()
     {
         $current = $this->getInnerIterator()->current();
+        /* @var $current \SplFileInfo */
         if (! $current->isFile()) {
             return false;
         }

From a0c331020ba4b9be51208091d93d81d21ba7c99f Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 18:53:41 +0200
Subject: [PATCH 66/96] doc/DocException: Extend `RuntimeException' instead of
 `Exception'

Errors in the documentation module can only be found on runtime.

refs #4820
---
 modules/doc/library/Doc/Exception/DocException.php | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/modules/doc/library/Doc/Exception/DocException.php b/modules/doc/library/Doc/Exception/DocException.php
index f684f1faa..374ea3e01 100644
--- a/modules/doc/library/Doc/Exception/DocException.php
+++ b/modules/doc/library/Doc/Exception/DocException.php
@@ -4,11 +4,9 @@
 
 namespace Icinga\Module\Doc\Exception;
 
-use Exception;
+use RuntimeException;
 
 /**
- * Exception thrown if an error in the documentation module occurs
+ * Exception thrown if an error in the documentation module's library occurs
  */
-class DocException extends Exception
-{
-}
+class DocException extends RuntimeException {}

From 77f8a3d67dc0d49c213d3572c68c9cb0e98a80f5 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 18:55:54 +0200
Subject: [PATCH 67/96] doc/DocEmptyException: Remove unnecessary line breaks

refs #4820
---
 modules/doc/library/Doc/Exception/DocEmptyException.php | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/modules/doc/library/Doc/Exception/DocEmptyException.php b/modules/doc/library/Doc/Exception/DocEmptyException.php
index 032a01744..2869e2678 100644
--- a/modules/doc/library/Doc/Exception/DocEmptyException.php
+++ b/modules/doc/library/Doc/Exception/DocEmptyException.php
@@ -7,6 +7,4 @@ namespace Icinga\Module\Doc\Exception;
 /**
  * Exception thrown if a documentation directory is empty
  */
-class DocEmptyException extends DocException
-{
-}
+class DocEmptyException extends DocException {}

From 71e81087b3c0f922b90c0c88e8f9af6a7af605f3 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 18:57:46 +0200
Subject: [PATCH 68/96] doc/ChapterNotFoundException: Remove unnecessary line
 breaks

refs #4820
---
 .../doc/library/Doc/Exception/ChapterNotFoundException.php    | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/modules/doc/library/Doc/Exception/ChapterNotFoundException.php b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php
index 1e9bcf7e9..cd048a162 100644
--- a/modules/doc/library/Doc/Exception/ChapterNotFoundException.php
+++ b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php
@@ -7,6 +7,4 @@ namespace Icinga\Module\Doc\Exception;
 /**
  * Exception thrown if a chapter was not found
  */
-class ChapterNotFoundException extends DocException
-{
-}
+class ChapterNotFoundException extends DocException {}

From 4f8cbb99dd6b1fde47d8f28dd3b380409811f933 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 18:58:46 +0200
Subject: [PATCH 69/96] doc/lib: Add `NonEmptyFileIterator' for iterating over
 non-empty files

refs #4820
---
 .../doc/library/Doc/NonEmptyFileIterator.php  | 31 +++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100644 modules/doc/library/Doc/NonEmptyFileIterator.php

diff --git a/modules/doc/library/Doc/NonEmptyFileIterator.php b/modules/doc/library/Doc/NonEmptyFileIterator.php
new file mode 100644
index 000000000..71bf5acfa
--- /dev/null
+++ b/modules/doc/library/Doc/NonEmptyFileIterator.php
@@ -0,0 +1,31 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Module\Doc;
+
+use RecursiveFilterIterator;
+
+/**
+ * Recursive iterator over non-empty files
+ */
+class NonEmptyFileIterator extends RecursiveFilterIterator
+{
+    /**
+     * Accept non-empty files
+     *
+     * @return bool Whether the current element of the iterator is acceptable
+     *              through this filter
+     */
+    public function accept()
+    {
+        $current = $this->getInnerIterator()->current();
+        /* @var $current \SplFileInfo */
+        if (! $current->isFile()
+            || $current->getSize() === 0
+        ) {
+            return false;
+        }
+        return true;
+    }
+}

From c325c09293959cdf8789ddad31b5bc3c917c61bc Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 18:59:42 +0200
Subject: [PATCH 70/96] doc/DocIterator: Use `NonEmptyFileIterator'

Considering empty files makes no sense.

refs #4820
---
 modules/doc/library/Doc/DocIterator.php | 71 ++++++++++++-------------
 1 file changed, 33 insertions(+), 38 deletions(-)

diff --git a/modules/doc/library/Doc/DocIterator.php b/modules/doc/library/Doc/DocIterator.php
index 1a229c918..43a9c7727 100644
--- a/modules/doc/library/Doc/DocIterator.php
+++ b/modules/doc/library/Doc/DocIterator.php
@@ -5,63 +5,58 @@
 namespace Icinga\Module\Doc;
 
 use ArrayIterator;
-use RunetimeException;
-
-class FileLockingIterator extends ArrayIterator
-{
-    public function next()
-    {
-        $this->current()->flock(LOCK_UN);
-        parent::next();
-    }
-
-    public function valid()
-    {
-        if (!parent::valid()) {
-            return false;
-        }
-        $fileInfo = $this->current();
-        try {
-            $fileObject = $fileInfo->openFile();
-        } catch (RuntimeException $e) {
-            throw new DocException($e->getMessage());
-        }
-        if ($fileObject->flock(LOCK_SH) === false) {
-            throw new DocException('Couldn\'t get the lock');
-        }
-        $this[$this->key()] = $fileObject;
-        return true;
-    }
-}
-
 use Countable;
 use IteratorAggregate;
 use RecursiveIteratorIterator;
 use RecursiveDirectoryIterator;
 
+/**
+ * Iterator over non-empty Markdown files ordered by the case insensitive "natural order" of file names
+ */
 class DocIterator implements Countable, IteratorAggregate
 {
-    protected $fileInfos;
+    /**
+     * Ordered files
+     *
+     * @var array
+     */
+    protected $fileInfo;
 
+    /**
+     * Create a new DocIterator
+     *
+     * @param string $path Path to the documentation
+     */
     public function __construct($path)
     {
-        $iter = new RecursiveIteratorIterator(
-            new MarkdownFileIterator(
-                new RecursiveDirectoryIterator($path)
+        $it = new RecursiveIteratorIterator(
+            new NonEmptyFileIterator(
+                new MarkdownFileIterator(
+                    new RecursiveDirectoryIterator($path)
+                )
             )
         );
-        $fileInfos = iterator_to_array($iter);
-        natcasesort($fileInfos);
-        $this->fileInfos = $fileInfos;
+        // Unfortunately we have no chance to sort the iterator
+        $fileInfo = iterator_to_array($it);
+        natcasesort($fileInfo);
+        $this->fileInfo = $fileInfo;
     }
 
+    /**
+     * (non-PHPDoc)
+     * @see Countable::count()
+     */
     public function count()
     {
-        return count($this->fileInfos);
+        return count($this->fileInfo);
     }
 
+    /**
+     * (non-PHPDoc)
+     * @see IteratorAggregate::getIterator()
+     */
     public function getIterator()
     {
-        return new FileLockingIterator($this->fileInfos);
+        return new ArrayIterator($this->fileInfo);
     }
 }

From c71086c748d1153c26a86570778fc4695be2f3af Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:00:50 +0200
Subject: [PATCH 71/96] doc/lib: Add `Section'

In the future a documentation will be represented as tree of sections.

refs #4820
---
 modules/doc/library/Doc/Section.php | 143 ++++++++++++++++++++++++++++
 1 file changed, 143 insertions(+)
 create mode 100644 modules/doc/library/Doc/Section.php

diff --git a/modules/doc/library/Doc/Section.php b/modules/doc/library/Doc/Section.php
new file mode 100644
index 000000000..57419dc0a
--- /dev/null
+++ b/modules/doc/library/Doc/Section.php
@@ -0,0 +1,143 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use Icinga\Data\Identifiable;
+
+/**
+ * A section of a documentation
+ */
+class Section implements Identifiable
+{
+    /**
+     * The ID of the section
+     *
+     * @var string
+     */
+    protected $id;
+
+    /**
+     * The title of the section
+     *
+     * @var string
+     */
+    protected $title;
+
+    /**
+     * The header level
+     *
+     * @var int
+     */
+    protected $level;
+
+    /**
+     * Whether to instruct search engines to not index the link to the section
+     *
+     * @var bool
+     */
+    protected $nofollow;
+
+    /**
+     * The title of the chapter the section is part of
+     *
+     * @var string
+     */
+    protected $chapterTitle;
+
+    /**
+     * The content of the section
+     *
+     * @var array
+     */
+    protected $content = array();
+
+    /**
+     * Create a new section
+     *
+     * @param string    $id             The ID of the section
+     * @param string    $title          The title of the section
+     * @param int       $level          The header level
+     * @param bool      $nofollow       Whether to instruct search engines to not index the link to the section
+     * @param string    $chapterTitle   The title of the chapter the section is part of
+     */
+    public function __construct($id, $title, $level, $nofollow, $chapterTitle)
+    {
+        $this->id = $id;
+        $this->title = $title;
+        $this->level = $level;
+        $this->nofollow = $nofollow;
+        $this->chapterTitle= $chapterTitle;
+    }
+
+    /**
+     * Get the ID of the section
+     *
+     * @return string
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Get the title of the section
+     *
+     * @return string
+     */
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    /**
+     * Get the header level
+     *
+     * @return int
+     */
+    public function getLevel()
+    {
+        return $this->level;
+    }
+
+    /**
+     * Whether to instruct search engines to not index the link to the section
+     *
+     * @return bool
+     */
+    public function isNofollow()
+    {
+        return $this->nofollow;
+    }
+
+    /**
+     * The title of the chapter the section is part of
+     *
+     * @return string
+     */
+    public function getChapterTitle()
+    {
+        return $this->chapterTitle;
+    }
+
+    /**
+     * Append content
+     *
+     * @param string $content
+     */
+    public function appendContent($content)
+    {
+        $this->content[] = $content;
+    }
+
+    /**
+     * Get the content of the section
+     *
+     * @return array
+     */
+    public function getContent()
+    {
+        return $this->content;
+    }
+}

From bbcdcb4609cf90b80f1a53c6b51c0585524bfd9f Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:05:37 +0200
Subject: [PATCH 72/96] lib: Add `Identifiable' interface for objects that are
 identifiable by an ID of any type

---
 library/Icinga/Data/Identifiable.php | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
 create mode 100644 library/Icinga/Data/Identifiable.php

diff --git a/library/Icinga/Data/Identifiable.php b/library/Icinga/Data/Identifiable.php
new file mode 100644
index 000000000..cfa727a1d
--- /dev/null
+++ b/library/Icinga/Data/Identifiable.php
@@ -0,0 +1,18 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Data;
+
+/**
+ * Interface for objects that are identifiable by an ID of any type
+ */
+interface Identifiable
+{
+    /**
+     * Get the ID associated with this Identifiable object
+     *
+     * @return mixed
+     */
+    public function getId();
+}

From 51bc0274f3e47b6541ae9491373b6ce6cb0e8330 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:06:12 +0200
Subject: [PATCH 73/96] doc/lib: Add `DocTree' for representing a documentation
 as tree of sections

refs #4820
---
 modules/doc/library/Doc/DocTree.php | 80 +++++++++++++++++++++++++++++
 1 file changed, 80 insertions(+)
 create mode 100644 modules/doc/library/Doc/DocTree.php

diff --git a/modules/doc/library/Doc/DocTree.php b/modules/doc/library/Doc/DocTree.php
new file mode 100644
index 000000000..07a0c5279
--- /dev/null
+++ b/modules/doc/library/Doc/DocTree.php
@@ -0,0 +1,80 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use LogicException;
+use Icinga\Data\Identifiable;
+use Icinga\Data\Tree\Node;
+
+/**
+ * Documentation tree
+ */
+class DocTree extends Node
+{
+    /**
+     * All nodes of the tree
+     *
+     * @var array
+     */
+    protected $nodes = array();
+
+    /**
+     * Append a root node to the tree
+     *
+     * @param Identifiable $root
+     */
+    public function addRoot(Identifiable $root)
+    {
+        $rootId = $root->getId();
+        if (isset($this->nodes[$rootId])) {
+            $rootId = uniqid($rootId);
+//            throw new LogicException(
+//                sprintf('Can\'t add root node: a root node with the id \'%s\' already exists', $rootId)
+//            );
+        }
+        $this->nodes[$rootId] = $this->appendChild($root);
+    }
+
+    /**
+     * Append a child node to a parent node
+     *
+     * @param   Identifiable $child
+     * @param   Identifiable $parent
+     *
+     * @throws  LogicException If the the tree does not contain the parent node
+     */
+    public function addChild(Identifiable $child, Identifiable $parent)
+    {
+        $childId = $child->getId();
+        $parentId = $parent->getId();
+        if (isset($this->nodes[$childId])) {
+            $childId = uniqid($childId);
+//            throw new LogicException(
+//                sprintf('Can\'t add child node: a child node with the id \'%s\' already exists', $childId)
+//            );
+        }
+        if (! isset($this->nodes[$parentId])) {
+            throw new LogicException(
+                sprintf('Can\'t add child node: there\'s no parent node having the id \'%s\'', $parentId)
+            );
+        }
+        $this->nodes[$childId] = $this->nodes[$parentId]->appendChild($child);
+    }
+
+    /**
+     * Get a node
+     *
+     * @param   mixed $id
+     *
+     * @return  Node|null
+     */
+    public function getNode($id)
+    {
+        if (! isset($this->nodes[$id])) {
+            return null;
+        }
+        return $this->nodes[$id];
+    }
+}

From e26d360561b7e4f0edc5bc619d5803306ebb2937 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:07:13 +0200
Subject: [PATCH 74/96] lib/Node: Remove method `findNodeBy()'

The new `DocTree' class provides `getNode()'.
---
 library/Icinga/Data/Tree/Node.php | 30 ------------------------------
 1 file changed, 30 deletions(-)

diff --git a/library/Icinga/Data/Tree/Node.php b/library/Icinga/Data/Tree/Node.php
index b8f9e8dad..b57b0fad4 100644
--- a/library/Icinga/Data/Tree/Node.php
+++ b/library/Icinga/Data/Tree/Node.php
@@ -4,9 +4,6 @@
 
 namespace Icinga\Data\Tree;
 
-use Exception;
-use RecursiveIteratorIterator;
-use RuntimeException;
 use SplDoublyLinkedList;
 
 class Node extends SplDoublyLinkedList implements NodeInterface
@@ -79,31 +76,4 @@ class Node extends SplDoublyLinkedList implements NodeInterface
         }
         return $current;
     }
-
-    /**
-     * Find the first child node by searching through nodes deeper than the immediate children using a custom function
-     *
-     * @param   $callback
-     *
-     * @return  NodeInterface|null
-     * @throws  Exception
-     */
-    public function findNodeBy($callback)
-    {
-        if (! is_callable($callback)) {
-            throw new RuntimeException('Callable expected');
-        }
-        foreach (new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST) as $node) {
-            try {
-                $found = call_user_func($callback, $node);
-            } catch (Exception $e) {
-                // TODO(el): Log exception and return false instead?
-                throw $e;
-            }
-            if ($found) {
-                return $node;
-            }
-        }
-        return null;
-    }
 }

From 134db3fc666690bd815f2c3f677ed40454afa639 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:09:04 +0200
Subject: [PATCH 75/96] doc/DocParser: Replace `getDoc()' and `getToc()' with
 `getDocTree()'

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 229 ++++++++------------------
 1 file changed, 66 insertions(+), 163 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index c213c764a..b74a0b377 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -4,12 +4,8 @@
 
 namespace Icinga\Module\Doc;
 
-require_once 'IcingaVendor/Parsedown/Parsedown.php';
-
-use Parsedown;
-use Icinga\Data\Tree\Node;
+use SplDoublyLinkedList;
 use Icinga\Exception\NotReadableError;
-use Icinga\Module\Doc\Exception\ChapterNotFoundException;
 use Icinga\Module\Doc\Exception\DocEmptyException;
 use Icinga\Module\Doc\Exception\DocException;
 
@@ -44,152 +40,26 @@ class DocParser
     public function __construct($path)
     {
         if (! is_dir($path)) {
-            throw new DocException('Doc directory `' . $path . '\' does not exist');
+            throw new DocException(
+                mt('doc', 'Documentation directory') . ' \'' . $path . '\' ' . mt('doc', 'does not exist')
+            );
         }
         if (! is_readable($path)) {
-            throw new NotReadableError('Doc directory `' . $path . '\' is not readable');
+            throw new DocException(
+                mt('doc', 'Documentation directory') . ' \'' . $path . '\' ' . mt('doc', 'is not readable')
+            );
         }
         $docIterator = new DocIterator($path);
         if ($docIterator->count() === 0) {
-            throw new DocEmptyException('Doc directory `' . $path . '\' is empty');
+            throw new DocEmptyException(
+                mt('doc', 'Documentation directory') . ' \'' . $path . '\' '
+                    . mt('doc', 'does not contain any non-empty Markdown file (\'.md\' suffix')
+            );
         }
         $this->path = $path;
         $this->docIterator = $docIterator;
     }
 
-    /**
-     * Retrieve the table of contents
-     *
-     * @return  Node
-     */
-    public function getToc()
-    {
-        $tocStack = array((object) array(
-            'level' => 0,
-            'node'  => new Node()
-        ));
-        foreach ($this->docIterator as $fileObject) {
-            $line = null;
-            $currentChapterName = null;
-            while (! $fileObject->eof()) {
-                // 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);
-                    $nofollow = false;
-                    $this->reduceToc($tocStack, $level);
-                    if ($id === null) {
-                        $path = array();
-                        foreach (array_slice($tocStack, 1) as $entity) {
-                            $path[] = $entity->node->getValue()->title;
-                        }
-                        $path[] = $header;
-                        $id = implode('-', $path);
-                        $nofollow = true;
-                    }
-                    $id = urlencode(str_replace('.', '&#46;', strip_tags($id)));
-                    if ($currentChapterName === null) {
-                        // The first header is the chapter's name
-                        $currentChapterName = $id;
-                        $id = null;
-                    }
-                    $node = end($tocStack)->node->appendChild(
-                        (object) array(
-                            'id'            => $id,
-                            'title'         => $header,
-                            'nofollow'      => $nofollow,
-                            'chapterName'   => $currentChapterName
-                        )
-                    );
-                    $tocStack[] = (object) array(
-                        'level' => $level,
-                        'node'  => $node
-                    );
-                }
-            }
-        }
-        return $tocStack[0]->node;
-    }
-
-    /**
-     * Retrieve a chapter
-     *
-     * @param   string $chapterName
-     *
-     * @return  string
-     * @throws  ChapterNotFoundException If the chapter was not found
-     */
-    public function getChapter($chapterName)
-    {
-        $tocStack = array((object) array(
-            'level' => 0,
-            'node'  => new Node()
-        ));
-        foreach ($this->docIterator as $fileObject) {
-            $line = null;
-            $currentChapterName = null;
-            $chapter = array();
-            while (! $fileObject->eof()) {
-                // 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);
-                    $this->reduceToc($tocStack, $level);
-                    if ($id === null) {
-                        $path = array();
-                        foreach (array_slice($tocStack, 1) as $entity) {
-                            $path[] = $entity->node->getValue()->title;
-                        }
-                        $path[] = $header;
-                        $id = implode('-', $path);
-                    }
-                    $id = urlencode(str_replace('.', '&#46;', strip_tags($id)));
-                    if ($currentChapterName === null) {
-                        $currentChapterName = $id;
-                        $id = null;
-                    }
-                    $node = end($tocStack)->node->appendChild(
-                        (object) array(
-                            'title' => $header
-                        )
-                    );
-                    $tocStack[] = (object) array(
-                        'level' => $level,
-                        'node'  => $node
-                    );
-                    $line = '<a name="' . $id . '"></a>' . PHP_EOL . $line;
-                }
-                $chapter[] = $line;
-            }
-            if ($currentChapterName === $chapterName) {
-                return preg_replace_callback(
-                    '#<pre><code class="language-php">(.*?)\</code></pre>#s',
-                    array($this, 'highlight'),
-                    Parsedown::instance()->text(implode('', $chapter))
-                );
-            }
-        }
-        throw new ChapterNotFoundException('Chapter \'' . $chapterName . '\' not found');
-    }
-
-    /**
-     * Syntax highlighting for PHP code
-     *
-     * @param   $match
-     *
-     * @return  string
-     */
-    protected function highlight($match)
-    {
-        return highlight_string(htmlspecialchars_decode($match[1]), true);
-    }
-
     /**
      * Extract atx- or setext-style headers from the given lines
      *
@@ -208,7 +78,7 @@ class DocParser
             && $line[0] === '#'
             && preg_match('/^#+/', $line, $match) === 1
         ) {
-            // Atx-style
+            // Atx
             $level  = strlen($match[0]);
             $header = trim(substr($line, $level));
             if (! $header) {
@@ -233,36 +103,69 @@ class DocParser
         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('#(?:<(?P<tag>a|span) id="(?P<id>.+)"></(?P=tag)>)#u', $header, $match)
+            && preg_match('#(?:<(?P<tag>a|span) (?:id|name)="(?P<id>.+)"></(?P=tag)>)\s*#u', $header, $match)
         ) {
             $header = str_replace($match[0], '', $header);
-            return $match['id'];
+            $id = $match['id'];
+        } else {
+            $id = null;
         }
-        return null;
+        return array($header, $id, $level);
     }
 
     /**
-     * Reduce the toc stack to the given level
+     * Get the documentation tree
      *
-     * @param array &$tocStack
-     * @param int   $level
+     * @return DocTree
      */
-    protected function reduceToc(array &$tocStack, $level) {
-        while (end($tocStack)->level >= $level) {
-            array_pop($tocStack);
+    public function getDocTree()
+    {
+        $tree = new DocTree();
+        $stack = new SplDoublyLinkedList();
+        foreach ($this->docIterator as $fileInfo) {
+            /* @var $file \SplFileInfo */
+            $file = $fileInfo->openFile();
+            /* @var $file \SplFileObject */
+            $lastLine = null;
+            foreach ($file as $line) {
+                $header = $this->extractHeader($line, $lastLine);
+                if ($header !== null) {
+                    list($header, $id, $level) = $header;  // When overwriting the variable to extract, it has to be
+                                                           // list()'s first parameter since list() assigns the values
+                                                           // starting with the right-most parameter
+                    while (! $stack->isEmpty() && $stack->top()->getLevel() >= $level) {
+                        $stack->pop();
+                    }
+                    if ($id === null) {
+                        $path = array();
+                        foreach ($stack as $section) {
+                            /* @var $section Section */
+                            $path[] = $section->getTitle();
+                        }
+                        $path[] = $header;
+                        $id = implode('-', $path);
+                        $nofollow = true;
+                    } else {
+                        $nofollow = false;
+                    }
+                    if ($stack->isEmpty()) {
+                        $chapterName = $header;
+                        $section = new Section($id, $header, $level, $nofollow, $chapterName);
+                        $tree->addRoot($section);
+                    } else {
+                        $chapterName = $stack->bottom()->getTitle();
+                        $section = new Section($id, $header, $level, $nofollow, $chapterName);
+                        $tree->addChild($section, $stack->top());
+                    }
+                    $stack->push($section);
+                } else {
+                    $stack->top()->appendContent($line);
+                }
+                // Save last line for setext-style headers
+                $lastLine = $line;
+            }
         }
+        return $tree;
     }
 }

From 00337330628593c47bc3d8c68cbf07a70c8990bb Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:09:55 +0200
Subject: [PATCH 76/96] doc/lib: Add `Renderer' as base class for toc and
 section renderer

refs #4820
---
 modules/doc/library/Doc/Renderer.php | 75 ++++++++++++++++++++++++++++
 1 file changed, 75 insertions(+)
 create mode 100644 modules/doc/library/Doc/Renderer.php

diff --git a/modules/doc/library/Doc/Renderer.php b/modules/doc/library/Doc/Renderer.php
new file mode 100644
index 000000000..0aebb89b9
--- /dev/null
+++ b/modules/doc/library/Doc/Renderer.php
@@ -0,0 +1,75 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use RecursiveIteratorIterator;
+use Zend_View_Helper_Url;
+use Icinga\Web\View;
+
+/**
+ * Base class for toc and section renderer
+ */
+abstract class Renderer extends RecursiveIteratorIterator
+{
+    /**
+     * Encode an anchor identifier
+     *
+     * @param   string $anchor
+     *
+     * @return  string
+     */
+    public static function encodeAnchor($anchor)
+    {
+        return rawurlencode($anchor);
+    }
+
+    /**
+     * Decode an anchor identifier
+     *
+     * @param   string $anchor
+     *
+     * @return  string
+     */
+    public static function decodeAnchor($anchor)
+    {
+        return rawurldecode($anchor);
+    }
+
+    /**
+     * Encode a URL parameter
+     *
+     * @param   string $param
+     *
+     * @return  string
+     */
+    public static function encodeUrlParam($param)
+    {
+        return str_replace(array('%2F','%5C'), array('%252F','%255C'), rawurlencode($param));
+    }
+
+    /**
+     * Decode a URL parameter
+     *
+     * @param   string $param
+     *
+     * @return  string
+     */
+    public static function decodeUrlParam($param)
+    {
+        return str_replace(array('%2F', '%5C'), array('/', '\\'), $param);
+    }
+
+    /**
+     * Render to HTML
+     *
+     * Meant to be overwritten by concrete classes.
+     *
+     * @param   View                    $view
+     * @param   Zend_View_Helper_Url    $zendUrlHelper
+     *
+     * @return  string
+     */
+    abstract public function render(View $view, Zend_View_Helper_Url $zendUrlHelper);
+}

From 8a229e15abceff7cb9974b9cfadef2738830543c Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:10:40 +0200
Subject: [PATCH 77/96] doc/lib: Add `TocRenderer' for rendering instances of
 `DocTree' as toc

refs #4820
---
 modules/doc/library/Doc/TocRenderer.php | 109 ++++++++++++++++++++++++
 1 file changed, 109 insertions(+)
 create mode 100644 modules/doc/library/Doc/TocRenderer.php

diff --git a/modules/doc/library/Doc/TocRenderer.php b/modules/doc/library/Doc/TocRenderer.php
new file mode 100644
index 000000000..8264c2d8e
--- /dev/null
+++ b/modules/doc/library/Doc/TocRenderer.php
@@ -0,0 +1,109 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Module\Doc;
+
+use RecursiveIteratorIterator;
+use Zend_View_Helper_Url;
+use Icinga\Web\View;
+
+/**
+ * toc renderer
+ */
+class TocRenderer extends Renderer
+{
+    /**
+     * The URL to replace links with
+     *
+     * @var string
+     */
+    protected $url;
+
+    /**
+     * Additional URL parameters
+     *
+     * @var array
+     */
+    protected $urlParams;
+
+    /**
+     * Content
+     *
+     * @var array
+     */
+    protected $content = array();
+
+    /**
+     * Create a new toc renderer
+     *
+     * @param DocTree   $docTree    The documentation tree
+     * @param string    $url        The URL to replace links with
+     * @param array     $urlParams  Additional URL parameters
+     */
+    public function __construct(DocTree $docTree, $url, array $urlParams)
+    {
+        parent::__construct($docTree, RecursiveIteratorIterator::SELF_FIRST);
+        $this->url = $url;
+        $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams);
+    }
+
+    public function beginIteration()
+    {
+        $this->content[] = '<nav><ul>';
+    }
+
+    public function endIteration()
+    {
+        $this->content[] = '</ul></nav>';
+    }
+
+    public function beginChildren()
+    {
+        $this->content[] = '<ul>';
+    }
+
+    public function endChildren()
+    {
+        $this->content[] = '</ul></li>';
+    }
+
+    /**
+     * Render the toc
+     *
+     * @param   View                    $view
+     * @param   Zend_View_Helper_Url    $zendUrlHelper
+     *
+     * @return  string
+     */
+    public function render(View $view, Zend_View_Helper_Url $zendUrlHelper)
+    {
+        foreach ($this as $node) {
+            $section = $node->getValue();
+            /* @var $section \Icinga\Module\Doc\Section */
+            $path = $zendUrlHelper->url(
+                array_merge(
+                    $this->urlParams,
+                    array(
+                        'chapterName' => $this->encodeUrlParam($section->getChapterTitle())
+                    )
+                ),
+                $this->url,
+                false,
+                false
+            );
+            $url = $view->url($path);
+            $url->setAnchor($this->encodeAnchor($section->getId()));
+            $this->content[] = sprintf(
+                '<li><a %shref="%s">%s</a>',
+                $section->isNofollow() ? 'rel="nofollow" ' : '',
+                $url->getAbsoluteUrl(),
+                $view->escape($section->getTitle())
+            );
+            if (! $this->getInnerIterator()->current()->hasChildren()) {
+                $this->content[] = '</li>';
+            }
+        }
+        return implode("\n", $this->content);
+    }
+}

From 79f6130e3c510c96e827b5b3b51d3fc44c2e5e3b Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:11:15 +0200
Subject: [PATCH 78/96] doc/lib: Add `SectionRenderer' for rendering instances
 of `DocTree' as sections

refs #4820
---
 modules/doc/library/Doc/SectionRenderer.php | 192 ++++++++++++++++++++
 1 file changed, 192 insertions(+)
 create mode 100644 modules/doc/library/Doc/SectionRenderer.php

diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php
new file mode 100644
index 000000000..012d50678
--- /dev/null
+++ b/modules/doc/library/Doc/SectionRenderer.php
@@ -0,0 +1,192 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Module\Doc;
+
+require_once 'IcingaVendor/Parsedown/Parsedown.php';
+
+use Icinga\Module\Doc\Exception\ChapterNotFoundException;
+use RecursiveIteratorIterator;
+use Parsedown;
+use Zend_View_Helper_Url;
+use Icinga\Web\View;
+
+/**
+ * preg_replace_callback helper to replace links
+ */
+class Callback
+{
+    protected $docTree;
+
+    protected $view;
+
+    protected $zendUrlHelper;
+
+    protected $url;
+
+    protected $urlParams;
+
+    public function __construct(
+        DocTree $docTree,
+        View $view,
+        Zend_View_Helper_Url $zendUrlHelper,
+        $url,
+        array $urlParams)
+    {
+        $this->docTree = $docTree;
+        $this->view = $view;
+        $this->zendUrlHelper = $zendUrlHelper;
+        $this->url = $url;
+        $this->urlParams = $urlParams;
+    }
+
+    public function render($match)
+    {
+        $node = $this->docTree->getNode(Renderer::decodeAnchor($match['fragment']));
+        /* @var $node \Icinga\Data\Tree\Node */
+        if ($node === null) {
+            return $match[0];
+        }
+        $section = $node->getValue();
+        /* @var $section \Icinga\Module\Doc\Section */
+        $path = $this->zendUrlHelper->url(
+            array_merge(
+                $this->urlParams,
+                array(
+                    'chapterName' => SectionRenderer::encodeUrlParam($section->getChapterTitle())
+                )
+            ),
+            $this->url,
+            false,
+            false
+        );
+        $url = $this->view->url($path);
+        $url->setAnchor(SectionRenderer::encodeAnchor($section->getId()));
+        return sprintf(
+            '<a %s%shref="%s"',
+            strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
+            $section->isNofollow() ? 'rel="nofollow" ' : '',
+            $url->getAbsoluteUrl()
+        );
+    }
+}
+
+/**
+ * Section renderer
+ */
+class SectionRenderer extends Renderer
+{
+    /**
+     * The documentation tree
+     *
+     * @var DocTree
+     */
+    protected $docTree;
+
+    /**
+     * The URL to replace links with
+     *
+     * @var string
+     */
+    protected $url;
+
+    /**
+     * Additional URL parameters
+     *
+     * @var array
+     */
+    protected $urlParams;
+
+    /**
+     * Parsedown instance
+     *
+     * @var Parsedown
+     */
+    protected $parsedown;
+
+    /**
+     * Content
+     *
+     * @var array
+     */
+    protected $content = array();
+
+    /**
+     * Create a new section renderer
+     *
+     * @param   DocTree     $docTree        The documentation tree
+     * @param   string|null $chapterTitle   If not null, the chapter title to filter for
+     * @param   string      $url            The URL to replace links with
+     * @param   array       $urlParams      Additional URL parameters
+     *
+     * @throws  ChapterNotFoundException    If the chapter to filter for was not found
+     */
+    public function __construct(DocTree $docTree, $chapterTitle, $url, array $urlParams)
+    {
+        if ($chapterTitle !== null) {
+            $filter = new SectionFilterIterator($docTree, $chapterTitle);
+            if ($filter->count() === 0) {
+                throw new ChapterNotFoundException(
+                    mt('doc', 'Chapter') . ' \'' . $chapterTitle . '\' ' . mt('doc', 'not found')
+                );
+            }
+            parent::__construct(
+                $filter,
+                RecursiveIteratorIterator::SELF_FIRST
+            );
+        } else {
+            parent::__construct($docTree, RecursiveIteratorIterator::SELF_FIRST);
+        }
+        $this->docTree = $docTree;
+        $this->url = $url;
+        $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams);
+        $this->parsedown = Parsedown::instance();
+    }
+
+    /**
+     * Syntax highlighting for PHP code
+     *
+     * @param   $match
+     *
+     * @return  string
+     */
+    protected function highlightPhp($match)
+    {
+        return '<pre>' . highlight_string(htmlspecialchars_decode($match[1]), true) . '</pre>';
+    }
+
+    /**
+     * Render the section
+     *
+     * @param   View                    $view
+     * @param   Zend_View_Helper_Url    $zendUrlHelper
+     * @return  string
+     */
+    public function render(View $view, Zend_View_Helper_Url $zendUrlHelper)
+    {
+        $callback = new Callback($this->docTree, $view, $zendUrlHelper, $this->url, $this->urlParams);
+        $content = array();
+        foreach ($this as $node) {
+            $section = $node->getValue();
+            /* @var $section \Icinga\Module\Doc\Section */
+            $content[] = sprintf(
+                '<a name="%1$s"></a> <h%2$d>%3$s</h%2$d>',
+                Renderer::encodeAnchor($section->getId()),
+                $section->getLevel(),
+                $view->escape($section->getTitle())
+            );
+            $html = preg_replace_callback(
+                '#<pre><code class="language-php">(.*?)</code></pre>#s',
+                array($this, 'highlightPhp'),
+                $this->parsedown->text(implode('', $section->getContent()))
+            );
+            $content[] = preg_replace_callback(
+                '/<a\s+(?P<attribs>[^>]*?\s+)?href="#(?P<fragment>[^"]+)"/',
+                array($callback, 'render'),
+                $html
+            );
+        }
+        return implode("\n", $content);
+    }
+}

From 88312e6284b56941daf19f4cc64f43fbbe9dd134 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:11:59 +0200
Subject: [PATCH 79/96] doc/lib: Add `SectionFilterIterator' for iterating over
 sections that are part of a particular chapter

refs #4820
---
 .../doc/library/Doc/SectionFilterIterator.php | 68 +++++++++++++++++++
 1 file changed, 68 insertions(+)
 create mode 100644 modules/doc/library/Doc/SectionFilterIterator.php

diff --git a/modules/doc/library/Doc/SectionFilterIterator.php b/modules/doc/library/Doc/SectionFilterIterator.php
new file mode 100644
index 000000000..9b5d2c013
--- /dev/null
+++ b/modules/doc/library/Doc/SectionFilterIterator.php
@@ -0,0 +1,68 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Module\Doc;
+
+use Countable;
+use RecursiveFilterIterator;
+use Icinga\Data\Tree\NodeInterface;
+
+/**
+ * Recursive iterator over sections that are part of a particular chapter
+ */
+class SectionFilterIterator extends RecursiveFilterIterator implements Countable
+{
+    /**
+     * The chapter title to filter for
+     *
+     * @var string
+     */
+    protected $chapterTitle;
+
+    /**
+     * Create a new SectionFilterIterator
+     *
+     * @param NodeInterface $node           Node
+     * @param string        $chapterTitle   The chapter title to filter for
+     */
+    public function __construct(NodeInterface $node, $chapterTitle)
+    {
+        parent::__construct($node);
+        $this->chapterTitle = $chapterTitle;
+    }
+
+    /**
+     * Accept sections that are part of the given chapter
+     *
+     * @return bool Whether the current element of the iterator is acceptable
+     *              through this filter
+     */
+    public function accept()
+    {
+        $section = $this->getInnerIterator()->current()->getValue();
+        /* @var $section \Icinga\Module\Doc\Section */
+        if ($section->getChapterTitle() === $this->chapterTitle) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * (non-PHPDoc)
+     * @see RecursiveFilterIterator::getChildren()
+     */
+    public function getChildren()
+    {
+        return new static($this->getInnerIterator()->getChildren(), $this->chapterTitle);
+    }
+
+    /**
+     * (non-PHPDoc)
+     * @see Countable::count()
+     */
+    public function count()
+    {
+        return iterator_count($this);
+    }
+}

From edc89d6ad67709a9e8d5f55373d87b133e3a0cbf Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:12:35 +0200
Subject: [PATCH 80/96] lib: Remove `NodeRenderer'

`NodeRenderer' is superseded by `TocRenderer'.
---
 library/Icinga/Data/Tree/NodeRenderer.php | 58 -----------------------
 1 file changed, 58 deletions(-)
 delete mode 100644 library/Icinga/Data/Tree/NodeRenderer.php

diff --git a/library/Icinga/Data/Tree/NodeRenderer.php b/library/Icinga/Data/Tree/NodeRenderer.php
deleted file mode 100644
index 4cebb7e01..000000000
--- a/library/Icinga/Data/Tree/NodeRenderer.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-// {{{ICINGA_LICENSE_HEADER}}}
-// {{{ICINGA_LICENSE_HEADER}}
-
-namespace Icinga\Data\Tree;
-
-use Exception;
-use RecursiveIteratorIterator;
-use RuntimeException;
-
-/**
- * A not yet customizable node renderer
- */
-class NodeRenderer extends RecursiveIteratorIterator
-{
-    protected $content = array();
-
-    public function __construct(NodeInterface $node)
-    {
-        parent::__construct($node, RecursiveIteratorIterator::SELF_FIRST);
-    }
-
-    public function beginIteration()
-    {
-        $this->content[] = '<ul>';
-    }
-
-    public function endIteration()
-    {
-        $this->content[] = '</ul>';
-    }
-
-    public function beginChildren()
-    {
-        $this->content[] = '<ul>';
-    }
-
-    public function endChildren()
-    {
-        $this->content[] = '</ul>';
-    }
-
-    public function render($callback)
-    {
-        if (! is_callable($callback)) {
-            throw new RuntimeException('Callable expected');
-        }
-        foreach ($this as $node) {
-            try {
-                $content = call_user_func($callback, $node);
-            } catch (Exception $e) {
-                throw new RuntimeException($e);
-            }
-            $this->content[] = $content;
-        }
-        return implode("\n", $this->content);
-    }
-}

From ce4fa1fd3d0531888905cfc5a6a9292e8037c547 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:14:50 +0200
Subject: [PATCH 81/96] Vagrant/doc module: Add menu link to the documentation
 module

refs #4820
---
 .vagrant-puppet/manifests/default.pp | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/.vagrant-puppet/manifests/default.pp b/.vagrant-puppet/manifests/default.pp
index 68affd347..121b7813f 100644
--- a/.vagrant-puppet/manifests/default.pp
+++ b/.vagrant-puppet/manifests/default.pp
@@ -792,3 +792,15 @@ file { '/etc/bash_completion.d/icingacli':
    require   => Exec['install bash-completion']
 }
 
+file { '/etc/icingaweb/modules/doc/':
+  ensure    => 'directory',
+  owner     => 'apache',
+  group     => 'apache'
+}
+
+file { '/etc/icingaweb/modules/doc/menu.ini':
+  source    => 'puppet:////vagrant/.vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini',
+  owner     => 'apache',
+  group     => 'apache',
+}
+

From 9eb4b62cd0c9b712f2ea910d2cfad94030d3845b Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:15:44 +0200
Subject: [PATCH 82/96] doc: Register PDF route

refs #4820
---
 modules/doc/run.php | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/modules/doc/run.php b/modules/doc/run.php
index 592d85ede..eaab1cb05 100644
--- a/modules/doc/run.php
+++ b/modules/doc/run.php
@@ -34,7 +34,17 @@ $docModuleToc = new Zend_Controller_Router_Route(
     )
 );
 
+$docModulePdf = new Zend_Controller_Router_Route(
+    'doc/module/:moduleName/pdf',
+    array(
+        'controller'    => 'module',
+        'action'        => 'pdf',
+        'module'        => 'doc'
+    )
+);
+
 $this->addRoute('doc/module/chapter', $docModuleChapter);
 $this->addRoute('doc/icingaweb/chapter', $docIcingaWebChapter);
 $this->addRoute('doc/module/toc', $docModuleToc);
+$this->addRoute('doc/module/pdf', $docModulePdf);
 

From 788e3eb6fc09e26b2dd814ad1108da50ec0182ea Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:17:03 +0200
Subject: [PATCH 83/96] doc: No longer use separate view scripts

refs #4820
---
 .../controllers/IcingawebController.php       | 19 +++--
 .../controllers/IndexController.php           |  4 +-
 .../controllers/ModuleController.php          | 69 +++++++++++++++----
 .../application/views/scripts/chapter.phtml   |  3 +
 .../views/scripts/icingaweb/chapter.phtml     | 40 -----------
 .../views/scripts/icingaweb/toc.phtml         | 23 -------
 .../views/scripts/index/index.phtml           |  5 +-
 .../views/scripts/module/chapter.phtml        | 40 -----------
 .../views/scripts/module/index.phtml          |  2 +-
 .../views/scripts/module/toc.phtml            | 25 -------
 .../doc/application/views/scripts/pdf.phtml   |  7 ++
 .../doc/application/views/scripts/toc.phtml   |  6 ++
 modules/doc/library/Doc/DocController.php     | 58 ++++++++++++----
 13 files changed, 138 insertions(+), 163 deletions(-)
 create mode 100644 modules/doc/application/views/scripts/chapter.phtml
 delete mode 100644 modules/doc/application/views/scripts/icingaweb/chapter.phtml
 delete mode 100644 modules/doc/application/views/scripts/icingaweb/toc.phtml
 delete mode 100644 modules/doc/application/views/scripts/module/chapter.phtml
 delete mode 100644 modules/doc/application/views/scripts/module/toc.phtml
 create mode 100644 modules/doc/application/views/scripts/pdf.phtml
 create mode 100644 modules/doc/application/views/scripts/toc.phtml

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
index 092de4025..813439169 100644
--- a/modules/doc/application/controllers/IcingawebController.php
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -9,11 +9,11 @@ use Icinga\Module\Doc\DocController;
 class Doc_IcingawebController extends DocController
 {
     /**
-     * View toc of Icinga Web 2's documentation
+     * View the toc of Icinga Web 2's documentation
      */
     public function tocAction()
     {
-        $this->populateToc(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2');
+        $this->renderToc(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2', 'doc/icingaweb/chapter');
     }
 
     /**
@@ -25,8 +25,19 @@ class Doc_IcingawebController extends DocController
     {
         $chapterName = $this->getParam('chapterName');
         if ($chapterName === null) {
-            throw new Zend_Controller_Action_Exception('Missing parameter "chapterName"', 404);
+            throw new Zend_Controller_Action_Exception(
+                $this->translate('Missing parameter \'chapterName\''),
+                404
+            );
         }
-        $this->populateChapter($chapterName, Icinga::app()->getApplicationDir('/../doc'));
+        $this->renderChapter(Icinga::app()->getApplicationDir('/../doc'), $chapterName, 'doc/icingaweb/chapter');
+    }
+
+    /**
+     * View Icinga Web 2's documentation as PDF
+     */
+    public function pdfAction()
+    {
+        $this->renderPdf(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2', 'doc/icingaweb/chapter');
     }
 }
diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php
index 63b5e8cdf..c83cfabab 100644
--- a/modules/doc/application/controllers/IndexController.php
+++ b/modules/doc/application/controllers/IndexController.php
@@ -6,7 +6,5 @@ use Icinga\Module\Doc\DocController;
 
 class Doc_IndexController extends DocController
 {
-    public function indexAction()
-    {
-    }
+    public function indexAction() {}
 }
diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index 2ce1b1e73..b04a56400 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -30,24 +30,36 @@ class Doc_ModuleController extends DocController
      *
      * @param   $moduleName
      *
-     * @throws  Zend_Controller_Action_Exception
+     * @throws  Zend_Controller_Action_Exception    If the required parameter 'moduleName' is empty or either if the
+     *                                              given module is neither installed nor enabled
      */
     protected function assertModuleEnabled($moduleName)
     {
-        if ($moduleName === null) {
-            throw new Zend_Controller_Action_Exception('Missing parameter "moduleName"', 404);
+        if (empty($moduleName)) {
+            throw new Zend_Controller_Action_Exception(
+                $this->translate('Missing parameter \'moduleName\''),
+                404
+            );
         }
         $moduleManager = Icinga::app()->getModuleManager();
         if (! $moduleManager->hasInstalled($moduleName)) {
-            throw new Zend_Controller_Action_Exception('Module ' . $moduleName . ' is not installed', 404);
+            throw new Zend_Controller_Action_Exception(
+                $this->translate('Module') . ' \'' . $moduleName . '\' ' . $this->translate('is not installed'),
+                404
+            );
         }
         if (! $moduleManager->hasEnabled($moduleName)) {
-            throw new Zend_Controller_Action_Exception('Module ' . $moduleName. ' is not enabled', 404);
+            throw new Zend_Controller_Action_Exception(
+                $this->translate('Module') . ' \'' . $moduleName . '\' ' . $this->translate('is not enabled'),
+                404
+            );
         }
     }
 
     /**
-     * View toc of a module's documentation
+     * View the toc of a module's documentation
+     *
+     * @see assertModuleEnabled()
      */
     public function tocAction()
     {
@@ -55,7 +67,12 @@ class Doc_ModuleController extends DocController
         $this->assertModuleEnabled($moduleName);
         $moduleManager = Icinga::app()->getModuleManager();
         try {
-            $this->populateToc($moduleManager->getModuleDir($moduleName, '/doc'), $moduleName);
+            $this->renderToc(
+                $moduleManager->getModuleDir($moduleName, '/doc'),
+                $moduleName,
+                'doc/module/chapter',
+                array('moduleName' => $moduleName)
+            );
         } catch (DocException $e) {
             throw new Zend_Controller_Action_Exception($e->getMessage(), 404);
         }
@@ -65,22 +82,50 @@ class Doc_ModuleController extends DocController
     /**
      * View a chapter of a module's documentation
      *
-     * @throws Zend_Controller_Action_Exception
+     * @throws  Zend_Controller_Action_Exception    If the required parameter 'chapterName' is missing or if an error in
+     *                                              the documentation module's library occurs
+     * @see     assertModuleEnabled()
      */
     public function chapterAction()
     {
         $moduleName = $this->getParam('moduleName');
         $this->assertModuleEnabled($moduleName);
-        $chapterName = $this->getParam('chapterName');
-        if ($chapterName === null) {
-            throw new Zend_Controller_Action_Exception('Missing parameter "chapterName"', 404);
+        $chapterTitle = $this->getParam('chapterName');
+        if ($chapterTitle === null) {
+            throw new Zend_Controller_Action_Exception(
+                $this->translate('Missing parameter \'chapterName\''),
+                404
+            );
         }
         $moduleManager = Icinga::app()->getModuleManager();
         try {
-            $this->populateChapter($chapterName, $moduleManager->getModuleDir($moduleName, '/doc'));
+            $this->renderChapter(
+                $moduleManager->getModuleDir($moduleName, '/doc'),
+                $chapterTitle,
+                'doc/module/chapter',
+                array('moduleName' => $moduleName)
+            );
         } catch (DocException $e) {
             throw new Zend_Controller_Action_Exception($e->getMessage(), 404);
         }
         $this->view->moduleName = $moduleName;
     }
+
+    /**
+     * View a module's documentation as PDF
+     *
+     * @see assertModuleEnabled()
+     */
+    public function pdfAction()
+    {
+        $moduleName = $this->getParam('moduleName');
+        $this->assertModuleEnabled($moduleName);
+        $moduleManager = Icinga::app()->getModuleManager();
+        $this->renderPdf(
+            $moduleManager->getModuleDir($moduleName, '/doc'),
+            $moduleName,
+            'doc/module/chapter',
+            array('moduleName' => $moduleName)
+        );
+    }
 }
diff --git a/modules/doc/application/views/scripts/chapter.phtml b/modules/doc/application/views/scripts/chapter.phtml
new file mode 100644
index 000000000..7657d69fb
--- /dev/null
+++ b/modules/doc/application/views/scripts/chapter.phtml
@@ -0,0 +1,3 @@
+<div class="chapter">
+    <?= $sectionRenderer->render($this, $this->getHelper('Url')); ?>
+</div>
diff --git a/modules/doc/application/views/scripts/icingaweb/chapter.phtml b/modules/doc/application/views/scripts/icingaweb/chapter.phtml
deleted file mode 100644
index 2ab744ebd..000000000
--- a/modules/doc/application/views/scripts/icingaweb/chapter.phtml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-$urlHelper = $this->getHelper('Url');
-$view = $this;
-?>
-<div class="chapter">
-<?= preg_replace_callback(
-    '/<a\s+(?P<attribs>[^>]*?\s+)?href="#(?P<fragment>[^"]+)"/im',
-    function($match) use ($toc, $urlHelper, $view) {
-        if (($node = $toc->findNodeBy(function ($node) use ($match) {
-            $section = $node->getValue();
-            if (($section->id === null && $section->chapterName === $match['fragment'])
-                || $section->id === $match['fragment']
-            ) {
-                return true;
-            }
-            return false;
-        }))) {
-            $section = $node->getValue();
-            $path = $urlHelper->url(
-                array('chapterName' => $section->chapterName),
-                'doc/icingaweb/chapter',
-                false,
-                false
-            );
-            $url = $view->url($path);
-            if ($section->id) {
-                $url->setAnchor($section->id);
-            }
-            return sprintf(
-                '<a %s%shref="%s"',
-                strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
-                $section->nofollow ? 'rel="nofollow" ' : '',
-                $url->getAbsoluteUrl()
-            );
-        }
-        return $match[0];
-    },
-    $chapterHtml
-); ?>
-</div>
diff --git a/modules/doc/application/views/scripts/icingaweb/toc.phtml b/modules/doc/application/views/scripts/icingaweb/toc.phtml
deleted file mode 100644
index bf91ffc77..000000000
--- a/modules/doc/application/views/scripts/icingaweb/toc.phtml
+++ /dev/null
@@ -1,23 +0,0 @@
-<div class="controls">
-    <h1><?= $docName ?> documentation</h1>
-</div>
-<div class="content" data-base-target="_next">
-    <?php
-    $urlHelper = $this->getHelper('Url');
-    $view = $this;
-    ?>
-    <?= $tocRenderer->render(function ($node) use ($urlHelper, $view) {
-        $section = $node->getValue();
-        $path = $urlHelper->url(array('chapterName' => $section->chapterName), 'doc/icingaweb/chapter', false, false);
-        $url = $view->url($path);
-        if ($section->id) {
-            $url->setAnchor($section->id);
-        }
-        return sprintf(
-            '<li><a %shref="%s">%s</a></li>',
-            $section->nofollow ? 'rel="nofollow" ' : '',
-            $url->getAbsoluteUrl(),
-            $section->title
-        );
-    }); ?>
-</div>
diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml
index 3cebdca69..e4218bee2 100644
--- a/modules/doc/application/views/scripts/index/index.phtml
+++ b/modules/doc/application/views/scripts/index/index.phtml
@@ -1,5 +1,6 @@
-<h1>Available documentations</h1>
+<div class="controls"></div>
+<h1><?= $this->translate('Available documentations'); ?></h1>
 <ul>
     <li><a href="<?= $this->href('doc/icingaweb/toc'); ?>">Icinga Web 2</a></li>
-    <li><a href="<?= $this->href('doc/module/'); ?>">Module documentations</a></li>
+    <li><a href="<?= $this->href('doc/module/'); ?>"><?= $this->translate('Module documentations'); ?></a></li>
 </ul>
diff --git a/modules/doc/application/views/scripts/module/chapter.phtml b/modules/doc/application/views/scripts/module/chapter.phtml
deleted file mode 100644
index 3a809209f..000000000
--- a/modules/doc/application/views/scripts/module/chapter.phtml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-$urlHelper = $this->getHelper('Url');
-$view = $this;
-?>
-<div class="chapter">
-<?= preg_replace_callback(
-    '/<a\s+(?P<attribs>[^>]*?\s+)?href="#(?P<fragment>[^"]+)"/im',
-    function($match) use ($toc, $moduleName, $urlHelper, $view) {
-        if (($node = $toc->findNodeBy(function ($node) use ($match) {
-            $section = $node->getValue();
-            if (($section->id === null && $section->chapterName === $match['fragment'])
-                || $section->id === $match['fragment']
-            ) {
-                return true;
-            }
-            return false;
-        }))) {
-            $section = $node->getValue();
-            $path = $urlHelper->url(
-                array('moduleName' => $moduleName, 'chapterName' => $section->chapterName),
-                'doc/module/chapter',
-                false,
-                false
-            );
-            $url = $view->url($path);
-            if ($section->id) {
-                $url->setAnchor($section->id);
-            }
-            return sprintf(
-                '<a %s%shref="%s"',
-                strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
-                $section->nofollow ? 'rel="nofollow" ' : '',
-                $url->getAbsoluteUrl()
-            );
-        }
-        return $match[0];
-    },
-    $chapterHtml
-); ?>
-</div>
diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml
index 0755748c7..cc184016f 100644
--- a/modules/doc/application/views/scripts/module/index.phtml
+++ b/modules/doc/application/views/scripts/module/index.phtml
@@ -1,4 +1,4 @@
-<h1>Module documentations</h1>
+<h1><?= $this->translate('Module documentations'); ?></h1>
 <ul>
     <?php foreach ($modules as $module): ?>
         <li>
diff --git a/modules/doc/application/views/scripts/module/toc.phtml b/modules/doc/application/views/scripts/module/toc.phtml
deleted file mode 100644
index ec33c507c..000000000
--- a/modules/doc/application/views/scripts/module/toc.phtml
+++ /dev/null
@@ -1,25 +0,0 @@
-<div class="controls">
-    <h1><?= $docName ?> documentation</h1>
-</div>
-<div class="content" data-base-target="_next">
-    <?php
-    $urlHelper = $this->getHelper('Url');
-    $view = $this;
-    ?>
-    <?= $tocRenderer->render(function ($node) use ($urlHelper, $view, $moduleName) {
-        $section = $node->getValue();
-        $path = $urlHelper->url(
-            array('moduleName' => $moduleName, 'chapterName' => $section->chapterName), 'doc/module/chapter', false, false
-        );
-        $url = $view->url($path);
-        if ($section->id) {
-            $url->setAnchor($section->id);
-        }
-        return sprintf(
-            '<li><a %shref="%s">%s</a></li>',
-            $section->nofollow ? 'rel="nofollow" ' : '',
-            $url->getAbsoluteUrl(),
-            $section->title
-        );
-    }); ?>
-</div>
diff --git a/modules/doc/application/views/scripts/pdf.phtml b/modules/doc/application/views/scripts/pdf.phtml
new file mode 100644
index 000000000..72d77f3c0
--- /dev/null
+++ b/modules/doc/application/views/scripts/pdf.phtml
@@ -0,0 +1,7 @@
+<h1><?= $docName ?> <?= $this->translate('Documentation'); ?></h1>
+<div class="toc">
+    <?= $tocRenderer->render($this, $this->getHelper('Url')); ?>
+</div>
+<div class="chapter">
+    <?= $sectionRenderer->render($this, $this->getHelper('Url')); ?>
+</div>
diff --git a/modules/doc/application/views/scripts/toc.phtml b/modules/doc/application/views/scripts/toc.phtml
new file mode 100644
index 000000000..8ffe2820d
--- /dev/null
+++ b/modules/doc/application/views/scripts/toc.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+    <h1><?= $docName ?> <?= $this->translate('Documentation'); ?></h1>
+</div>
+<div class="content toc" data-base-target="_next">
+    <?= $tocRenderer->render($this, $this->getHelper('Url')); ?>
+</div>
diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
index 88fd7476e..de2a7bac9 100644
--- a/modules/doc/library/Doc/DocController.php
+++ b/modules/doc/library/Doc/DocController.php
@@ -4,35 +4,67 @@
 
 namespace Icinga\Module\Doc;
 
-use Icinga\Data\Tree\NodeRenderer;
 use Icinga\Web\Controller\ModuleActionController;
 
 class DocController extends ModuleActionController
 {
     /**
-     * Populate a chapter
+     * Render a chapter
      *
-     * @param string $chapterName   Name of the chapter
-     * @param string $path          Path to the documentation
+     * @param string    $path           Path to the documentation
+     * @param string    $chapterTitle   Title of the chapter
+     * @param string    $url
+     * @param array     $urlParams
      */
-    protected function populateChapter($chapterName, $path)
+    protected function renderChapter($path, $chapterTitle, $url, array $urlParams = array())
     {
         $parser = new DocParser($path);
-        $this->view->chapterHtml = $parser->getChapter($chapterName);
-        $this->view->toc = $parser->getToc();
+        $this->view->sectionRenderer = new SectionRenderer(
+            $parser->getDocTree(),
+            SectionRenderer::decodeUrlParam($chapterTitle),
+            $url,
+            $urlParams
+        );
+        $this->_helper->viewRenderer('chapter', null, true);
     }
 
     /**
-     * Populate toc
+     * Render a toc
      *
-     * @param string $path Path to the documentation
-     * @param string $name Name of the documentation
+     * @param string    $path           Path to the documentation
+     * @param string    $name           Name of the documentation
+     * @param string    $url
+     * @param array     $urlParams
      */
-    protected function populateToc($path, $name)
+    protected function renderToc($path, $name, $url, array $urlParams = array())
     {
         $parser = new DocParser($path);
-        $toc = $parser->getToc();
-        $this->view->tocRenderer = new NodeRenderer($toc);
+        $this->view->tocRenderer = new TocRenderer($parser->getDocTree(), $url, $urlParams);
         $this->view->docName = $name;
+        $this->_helper->viewRenderer('toc', null, true);
+    }
+
+    /**
+     * Render a pdf
+     *
+     * @param string    $path           Path to the documentation
+     * @param string    $name           Name of the documentation
+     * @param string    $url
+     * @param array     $urlParams
+     */
+    protected function renderPdf($path, $name, $url, array $urlParams = array())
+    {
+        $parser = new DocParser($path);
+        $docTree = $parser->getDocTree();
+        $this->view->tocRenderer = new TocRenderer($docTree, $url, $urlParams);
+        $this->view->sectionRenderer = new SectionRenderer(
+            $docTree,
+            null,
+            $url,
+            $urlParams
+        );
+        $this->view->docName = $name;
+        $this->_helper->viewRenderer('pdf', null, true);
+        $this->_request->setParam('format', 'pdf');
     }
 }

From a0867ce33afbcba1233e78918cd6473022b5164d Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Mon, 28 Jul 2014 19:26:39 +0200
Subject: [PATCH 84/96] doc module/Vagrant: Add menu.ini

refs #4820
---
 .vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 .vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini

diff --git a/.vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini b/.vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini
new file mode 100644
index 000000000..86889b239
--- /dev/null
+++ b/.vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini
@@ -0,0 +1,5 @@
+[Documentation]
+title       = "Documentation"
+icon        = "img/icons/comment.png"
+url         = "doc"
+priority    = 80

From 55d3818ebb19379c586d5045a2a0ec39e95f50e9 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 29 Jul 2014 11:10:06 +0200
Subject: [PATCH 85/96] doc/SectionRenderer: Support images

refs #4820
---
 modules/doc/library/Doc/SectionRenderer.php | 28 ++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php
index 012d50678..c7e6c6ffb 100644
--- a/modules/doc/library/Doc/SectionRenderer.php
+++ b/modules/doc/library/Doc/SectionRenderer.php
@@ -6,10 +6,13 @@ namespace Icinga\Module\Doc;
 
 require_once 'IcingaVendor/Parsedown/Parsedown.php';
 
-use Icinga\Module\Doc\Exception\ChapterNotFoundException;
+use DOMDocument;
+use DOMXPath;
 use RecursiveIteratorIterator;
 use Parsedown;
 use Zend_View_Helper_Url;
+use Icinga\Module\Doc\Exception\ChapterNotFoundException;
+use Icinga\Web\Url;
 use Icinga\Web\View;
 
 /**
@@ -156,6 +159,24 @@ class SectionRenderer extends Renderer
         return '<pre>' . highlight_string(htmlspecialchars_decode($match[1]), true) . '</pre>';
     }
 
+    /**
+     * Replace img src tags
+     *
+     * @param   $match
+     *
+     * @return  string
+     */
+    protected function replaceImg($match)
+    {
+        $doc = new DOMDocument();
+        $doc->loadHTML($match[0]);
+        $xpath = new DOMXPath($doc);
+        $img = $xpath->query('//img[1]')->item(0);
+        /* @var $img \DOMElement */
+        $img->setAttribute('src', Url::fromPath($img->getAttribute('src'))->getAbsoluteUrl());
+        return substr_replace($doc->saveXML($img), '', -2, 1);  // Replace '/>' with '>'
+    }
+
     /**
      * Render the section
      *
@@ -181,6 +202,11 @@ class SectionRenderer extends Renderer
                 array($this, 'highlightPhp'),
                 $this->parsedown->text(implode('', $section->getContent()))
             );
+            $html = preg_replace_callback(
+                '/<img[^>]+>/',
+                array($this, 'replaceImg'),
+                $html
+            );
             $content[] = preg_replace_callback(
                 '/<a\s+(?P<attribs>[^>]*?\s+)?href="#(?P<fragment>[^"]+)"/',
                 array($callback, 'render'),

From 38a6df91b9e0f2fd3b5210b70b16a67b04856b66 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 29 Jul 2014 11:10:49 +0200
Subject: [PATCH 86/96] doc/DocParser: Rename 'chapterName' to 'chapterTitle'

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index b74a0b377..12ec373fc 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -150,12 +150,12 @@ class DocParser
                         $nofollow = false;
                     }
                     if ($stack->isEmpty()) {
-                        $chapterName = $header;
-                        $section = new Section($id, $header, $level, $nofollow, $chapterName);
+                        $chapterTitle = $header;
+                        $section = new Section($id, $header, $level, $nofollow, $chapterTitle);
                         $tree->addRoot($section);
                     } else {
-                        $chapterName = $stack->bottom()->getTitle();
-                        $section = new Section($id, $header, $level, $nofollow, $chapterName);
+                        $chapterTitle = $stack->bottom()->getTitle();
+                        $section = new Section($id, $header, $level, $nofollow, $chapterTitle);
                         $tree->addChild($section, $stack->top());
                     }
                     $stack->push($section);

From 3cafc8910a8ee5daae4400b48f630b8820ef6d09 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 29 Jul 2014 11:12:06 +0200
Subject: [PATCH 87/96] doc/DocParser: Rename 'header' to 'title'

refs #4820
---
 modules/doc/library/Doc/DocParser.php | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 12ec373fc..d543f9f91 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -131,9 +131,7 @@ class DocParser
             foreach ($file as $line) {
                 $header = $this->extractHeader($line, $lastLine);
                 if ($header !== null) {
-                    list($header, $id, $level) = $header;  // When overwriting the variable to extract, it has to be
-                                                           // list()'s first parameter since list() assigns the values
-                                                           // starting with the right-most parameter
+                    list($title, $id, $level) = $header;
                     while (! $stack->isEmpty() && $stack->top()->getLevel() >= $level) {
                         $stack->pop();
                     }
@@ -143,19 +141,19 @@ class DocParser
                             /* @var $section Section */
                             $path[] = $section->getTitle();
                         }
-                        $path[] = $header;
+                        $path[] = $title;
                         $id = implode('-', $path);
                         $nofollow = true;
                     } else {
                         $nofollow = false;
                     }
                     if ($stack->isEmpty()) {
-                        $chapterTitle = $header;
-                        $section = new Section($id, $header, $level, $nofollow, $chapterTitle);
+                        $chapterTitle = $title;
+                        $section = new Section($id, $title, $level, $nofollow, $chapterTitle);
                         $tree->addRoot($section);
                     } else {
                         $chapterTitle = $stack->bottom()->getTitle();
-                        $section = new Section($id, $header, $level, $nofollow, $chapterTitle);
+                        $section = new Section($id, $title, $level, $nofollow, $chapterTitle);
                         $tree->addChild($section, $stack->top());
                     }
                     $stack->push($section);

From f6b9b13654f7f6e8d43ccd3c8b4b76d57b809ce2 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 29 Jul 2014 11:19:40 +0200
Subject: [PATCH 88/96] doc: Rename 'chapterName' to 'chapterTitle'

refs #4820
---
 .../doc/application/controllers/IcingawebController.php   | 8 ++++----
 modules/doc/application/controllers/ModuleController.php  | 6 +++---
 modules/doc/library/Doc/SectionRenderer.php               | 2 +-
 modules/doc/library/Doc/TocRenderer.php                   | 2 +-
 modules/doc/run.php                                       | 4 ++--
 5 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
index 813439169..55f60d36e 100644
--- a/modules/doc/application/controllers/IcingawebController.php
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -23,14 +23,14 @@ class Doc_IcingawebController extends DocController
      */
     public function chapterAction()
     {
-        $chapterName = $this->getParam('chapterName');
-        if ($chapterName === null) {
+        $chapterTitle = $this->getParam('chapterTitle');
+        if ($chapterTitle === null) {
             throw new Zend_Controller_Action_Exception(
-                $this->translate('Missing parameter \'chapterName\''),
+                $this->translate('Missing parameter \'chapterTitle\''),
                 404
             );
         }
-        $this->renderChapter(Icinga::app()->getApplicationDir('/../doc'), $chapterName, 'doc/icingaweb/chapter');
+        $this->renderChapter(Icinga::app()->getApplicationDir('/../doc'), $chapterTitle, 'doc/icingaweb/chapter');
     }
 
     /**
diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index b04a56400..3a8bfc6af 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -82,7 +82,7 @@ class Doc_ModuleController extends DocController
     /**
      * View a chapter of a module's documentation
      *
-     * @throws  Zend_Controller_Action_Exception    If the required parameter 'chapterName' is missing or if an error in
+     * @throws  Zend_Controller_Action_Exception    If the required parameter 'chapterTitle' is missing or if an error in
      *                                              the documentation module's library occurs
      * @see     assertModuleEnabled()
      */
@@ -90,10 +90,10 @@ class Doc_ModuleController extends DocController
     {
         $moduleName = $this->getParam('moduleName');
         $this->assertModuleEnabled($moduleName);
-        $chapterTitle = $this->getParam('chapterName');
+        $chapterTitle = $this->getParam('chapterTitle');
         if ($chapterTitle === null) {
             throw new Zend_Controller_Action_Exception(
-                $this->translate('Missing parameter \'chapterName\''),
+                $this->translate('Missing parameter \'chapterTitle\''),
                 404
             );
         }
diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php
index c7e6c6ffb..31143e3c8 100644
--- a/modules/doc/library/Doc/SectionRenderer.php
+++ b/modules/doc/library/Doc/SectionRenderer.php
@@ -57,7 +57,7 @@ class Callback
             array_merge(
                 $this->urlParams,
                 array(
-                    'chapterName' => SectionRenderer::encodeUrlParam($section->getChapterTitle())
+                    'chapterTitle' => SectionRenderer::encodeUrlParam($section->getChapterTitle())
                 )
             ),
             $this->url,
diff --git a/modules/doc/library/Doc/TocRenderer.php b/modules/doc/library/Doc/TocRenderer.php
index 8264c2d8e..f018c556d 100644
--- a/modules/doc/library/Doc/TocRenderer.php
+++ b/modules/doc/library/Doc/TocRenderer.php
@@ -85,7 +85,7 @@ class TocRenderer extends Renderer
                 array_merge(
                     $this->urlParams,
                     array(
-                        'chapterName' => $this->encodeUrlParam($section->getChapterTitle())
+                        'chapterTitle' => $this->encodeUrlParam($section->getChapterTitle())
                     )
                 ),
                 $this->url,
diff --git a/modules/doc/run.php b/modules/doc/run.php
index eaab1cb05..959b56095 100644
--- a/modules/doc/run.php
+++ b/modules/doc/run.php
@@ -8,7 +8,7 @@ if (Icinga::app()->isCli()) {
 }
 
 $docModuleChapter = new Zend_Controller_Router_Route(
-    'doc/module/:moduleName/chapter/:chapterName',
+    'doc/module/:moduleName/chapter/:chapterTitle',
     array(
         'controller'    => 'module',
         'action'        => 'chapter',
@@ -17,7 +17,7 @@ $docModuleChapter = new Zend_Controller_Router_Route(
 );
 
 $docIcingaWebChapter = new Zend_Controller_Router_Route(
-    'doc/icingaweb/chapter/:chapterName',
+    'doc/icingaweb/chapter/:chapterTitle',
     array(
         'controller'    => 'icingaweb',
         'action'        => 'chapter',

From f9e8ad3d0b7cb9002976647a5bfcf60fb6dbd45c Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 29 Jul 2014 11:45:16 +0200
Subject: [PATCH 89/96] doc: Remove 'locale' directory

Since #6432 has been fixed, the empty locale directory can be removed.

refs #4820
---
 modules/doc/application/locale/.gitkeep | 1 -
 1 file changed, 1 deletion(-)
 delete mode 100644 modules/doc/application/locale/.gitkeep

diff --git a/modules/doc/application/locale/.gitkeep b/modules/doc/application/locale/.gitkeep
deleted file mode 100644
index 9fc47527c..000000000
--- a/modules/doc/application/locale/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-Remove this file and the locale directory once #6432 has been fixed.

From 289aed84205924a57753a4fa527f1742f9e82b40 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 19 Aug 2014 09:45:53 +0200
Subject: [PATCH 90/96] doc: Use `chapterId' instead of `chapterTitle' in URLs

Manually given chapter IDs are meant to not change while a chapter's title could change.

refs #4820
---
 .../controllers/IcingawebController.php          | 10 +++++-----
 .../application/controllers/ModuleController.php | 10 +++++-----
 modules/doc/library/Doc/DocController.php        |  6 +++---
 modules/doc/library/Doc/DocParser.php            |  8 ++++----
 modules/doc/library/Doc/Section.php              | 16 ++++++++--------
 .../doc/library/Doc/SectionFilterIterator.php    | 14 +++++++-------
 modules/doc/library/Doc/SectionRenderer.php      | 12 ++++++------
 modules/doc/library/Doc/TocRenderer.php          |  2 +-
 modules/doc/run.php                              |  4 ++--
 9 files changed, 41 insertions(+), 41 deletions(-)

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
index 55f60d36e..117d6eb0e 100644
--- a/modules/doc/application/controllers/IcingawebController.php
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -19,18 +19,18 @@ class Doc_IcingawebController extends DocController
     /**
      * View a chapter of Icinga Web 2's documentation
      *
-     * @throws Zend_Controller_Action_Exception
+     * @throws Zend_Controller_Action_Exception If the required parameter 'chapterId' is missing
      */
     public function chapterAction()
     {
-        $chapterTitle = $this->getParam('chapterTitle');
-        if ($chapterTitle === null) {
+        $chapterId = $this->getParam('chapterId');
+        if ($chapterId === null) {
             throw new Zend_Controller_Action_Exception(
-                $this->translate('Missing parameter \'chapterTitle\''),
+                $this->translate('Missing parameter \'chapterId\''),
                 404
             );
         }
-        $this->renderChapter(Icinga::app()->getApplicationDir('/../doc'), $chapterTitle, 'doc/icingaweb/chapter');
+        $this->renderChapter(Icinga::app()->getApplicationDir('/../doc'), $chapterId, 'doc/icingaweb/chapter');
     }
 
     /**
diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index 3a8bfc6af..7429dbdf2 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -82,7 +82,7 @@ class Doc_ModuleController extends DocController
     /**
      * View a chapter of a module's documentation
      *
-     * @throws  Zend_Controller_Action_Exception    If the required parameter 'chapterTitle' is missing or if an error in
+     * @throws  Zend_Controller_Action_Exception    If the required parameter 'chapterId' is missing or if an error in
      *                                              the documentation module's library occurs
      * @see     assertModuleEnabled()
      */
@@ -90,10 +90,10 @@ class Doc_ModuleController extends DocController
     {
         $moduleName = $this->getParam('moduleName');
         $this->assertModuleEnabled($moduleName);
-        $chapterTitle = $this->getParam('chapterTitle');
-        if ($chapterTitle === null) {
+        $chapterId = $this->getParam('chapterId');
+        if ($chapterId === null) {
             throw new Zend_Controller_Action_Exception(
-                $this->translate('Missing parameter \'chapterTitle\''),
+                $this->translate('Missing parameter \'chapterId\''),
                 404
             );
         }
@@ -101,7 +101,7 @@ class Doc_ModuleController extends DocController
         try {
             $this->renderChapter(
                 $moduleManager->getModuleDir($moduleName, '/doc'),
-                $chapterTitle,
+                $chapterId,
                 'doc/module/chapter',
                 array('moduleName' => $moduleName)
             );
diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
index de2a7bac9..16824ce0c 100644
--- a/modules/doc/library/Doc/DocController.php
+++ b/modules/doc/library/Doc/DocController.php
@@ -12,16 +12,16 @@ class DocController extends ModuleActionController
      * Render a chapter
      *
      * @param string    $path           Path to the documentation
-     * @param string    $chapterTitle   Title of the chapter
+     * @param string    $chapterId      ID of the chapter
      * @param string    $url
      * @param array     $urlParams
      */
-    protected function renderChapter($path, $chapterTitle, $url, array $urlParams = array())
+    protected function renderChapter($path, $chapterId, $url, array $urlParams = array())
     {
         $parser = new DocParser($path);
         $this->view->sectionRenderer = new SectionRenderer(
             $parser->getDocTree(),
-            SectionRenderer::decodeUrlParam($chapterTitle),
+            SectionRenderer::decodeUrlParam($chapterId),
             $url,
             $urlParams
         );
diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index d543f9f91..559a9a7d3 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -148,12 +148,12 @@ class DocParser
                         $nofollow = false;
                     }
                     if ($stack->isEmpty()) {
-                        $chapterTitle = $title;
-                        $section = new Section($id, $title, $level, $nofollow, $chapterTitle);
+                        $chapterId = $id;
+                        $section = new Section($id, $title, $level, $nofollow, $chapterId);
                         $tree->addRoot($section);
                     } else {
-                        $chapterTitle = $stack->bottom()->getTitle();
-                        $section = new Section($id, $title, $level, $nofollow, $chapterTitle);
+                        $chapterId = $stack->bottom()->getId();
+                        $section = new Section($id, $title, $level, $nofollow, $chapterId);
                         $tree->addChild($section, $stack->top());
                     }
                     $stack->push($section);
diff --git a/modules/doc/library/Doc/Section.php b/modules/doc/library/Doc/Section.php
index 57419dc0a..ac458b6fe 100644
--- a/modules/doc/library/Doc/Section.php
+++ b/modules/doc/library/Doc/Section.php
@@ -40,11 +40,11 @@ class Section implements Identifiable
     protected $nofollow;
 
     /**
-     * The title of the chapter the section is part of
+     * The ID of the chapter the section is part of
      *
      * @var string
      */
-    protected $chapterTitle;
+    protected $chapterId;
 
     /**
      * The content of the section
@@ -60,15 +60,15 @@ class Section implements Identifiable
      * @param string    $title          The title of the section
      * @param int       $level          The header level
      * @param bool      $nofollow       Whether to instruct search engines to not index the link to the section
-     * @param string    $chapterTitle   The title of the chapter the section is part of
+     * @param string    $chapterId      The ID of the chapter the section is part of
      */
-    public function __construct($id, $title, $level, $nofollow, $chapterTitle)
+    public function __construct($id, $title, $level, $nofollow, $chapterId)
     {
         $this->id = $id;
         $this->title = $title;
         $this->level = $level;
         $this->nofollow = $nofollow;
-        $this->chapterTitle= $chapterTitle;
+        $this->chapterId= $chapterId;
     }
 
     /**
@@ -112,13 +112,13 @@ class Section implements Identifiable
     }
 
     /**
-     * The title of the chapter the section is part of
+     * The ID of the chapter the section is part of
      *
      * @return string
      */
-    public function getChapterTitle()
+    public function getChapterId()
     {
-        return $this->chapterTitle;
+        return $this->chapterId;
     }
 
     /**
diff --git a/modules/doc/library/Doc/SectionFilterIterator.php b/modules/doc/library/Doc/SectionFilterIterator.php
index 9b5d2c013..e20d80359 100644
--- a/modules/doc/library/Doc/SectionFilterIterator.php
+++ b/modules/doc/library/Doc/SectionFilterIterator.php
@@ -14,22 +14,22 @@ use Icinga\Data\Tree\NodeInterface;
 class SectionFilterIterator extends RecursiveFilterIterator implements Countable
 {
     /**
-     * The chapter title to filter for
+     * The chapter ID to filter for
      *
      * @var string
      */
-    protected $chapterTitle;
+    protected $chapterId;
 
     /**
      * Create a new SectionFilterIterator
      *
      * @param NodeInterface $node           Node
-     * @param string        $chapterTitle   The chapter title to filter for
+     * @param string        $chapterId      The chapter ID to filter for
      */
-    public function __construct(NodeInterface $node, $chapterTitle)
+    public function __construct(NodeInterface $node, $chapterId)
     {
         parent::__construct($node);
-        $this->chapterTitle = $chapterTitle;
+        $this->chapterId = $chapterId;
     }
 
     /**
@@ -42,7 +42,7 @@ class SectionFilterIterator extends RecursiveFilterIterator implements Countable
     {
         $section = $this->getInnerIterator()->current()->getValue();
         /* @var $section \Icinga\Module\Doc\Section */
-        if ($section->getChapterTitle() === $this->chapterTitle) {
+        if ($section->getChapterId() === $this->chapterId) {
             return true;
         }
         return false;
@@ -54,7 +54,7 @@ class SectionFilterIterator extends RecursiveFilterIterator implements Countable
      */
     public function getChildren()
     {
-        return new static($this->getInnerIterator()->getChildren(), $this->chapterTitle);
+        return new static($this->getInnerIterator()->getChildren(), $this->chapterId);
     }
 
     /**
diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php
index 31143e3c8..1b7c68f1b 100644
--- a/modules/doc/library/Doc/SectionRenderer.php
+++ b/modules/doc/library/Doc/SectionRenderer.php
@@ -57,7 +57,7 @@ class Callback
             array_merge(
                 $this->urlParams,
                 array(
-                    'chapterTitle' => SectionRenderer::encodeUrlParam($section->getChapterTitle())
+                    'chapterId' => SectionRenderer::encodeUrlParam($section->getChapterId())
                 )
             ),
             $this->url,
@@ -119,19 +119,19 @@ class SectionRenderer extends Renderer
      * Create a new section renderer
      *
      * @param   DocTree     $docTree        The documentation tree
-     * @param   string|null $chapterTitle   If not null, the chapter title to filter for
+     * @param   string|null $chapterId      If not null, the chapter ID to filter for
      * @param   string      $url            The URL to replace links with
      * @param   array       $urlParams      Additional URL parameters
      *
      * @throws  ChapterNotFoundException    If the chapter to filter for was not found
      */
-    public function __construct(DocTree $docTree, $chapterTitle, $url, array $urlParams)
+    public function __construct(DocTree $docTree, $chapterId, $url, array $urlParams)
     {
-        if ($chapterTitle !== null) {
-            $filter = new SectionFilterIterator($docTree, $chapterTitle);
+        if ($chapterId !== null) {
+            $filter = new SectionFilterIterator($docTree, $chapterId);
             if ($filter->count() === 0) {
                 throw new ChapterNotFoundException(
-                    mt('doc', 'Chapter') . ' \'' . $chapterTitle . '\' ' . mt('doc', 'not found')
+                    mt('doc', 'Chapter') . ' \'' . $chapterId . '\' ' . mt('doc', 'not found')
                 );
             }
             parent::__construct(
diff --git a/modules/doc/library/Doc/TocRenderer.php b/modules/doc/library/Doc/TocRenderer.php
index f018c556d..4b9d96a05 100644
--- a/modules/doc/library/Doc/TocRenderer.php
+++ b/modules/doc/library/Doc/TocRenderer.php
@@ -85,7 +85,7 @@ class TocRenderer extends Renderer
                 array_merge(
                     $this->urlParams,
                     array(
-                        'chapterTitle' => $this->encodeUrlParam($section->getChapterTitle())
+                        'chapterId' => $this->encodeUrlParam($section->getChapterId())
                     )
                 ),
                 $this->url,
diff --git a/modules/doc/run.php b/modules/doc/run.php
index 959b56095..7392e4c22 100644
--- a/modules/doc/run.php
+++ b/modules/doc/run.php
@@ -8,7 +8,7 @@ if (Icinga::app()->isCli()) {
 }
 
 $docModuleChapter = new Zend_Controller_Router_Route(
-    'doc/module/:moduleName/chapter/:chapterTitle',
+    'doc/module/:moduleName/chapter/:chapterId',
     array(
         'controller'    => 'module',
         'action'        => 'chapter',
@@ -17,7 +17,7 @@ $docModuleChapter = new Zend_Controller_Router_Route(
 );
 
 $docIcingaWebChapter = new Zend_Controller_Router_Route(
-    'doc/icingaweb/chapter/:chapterTitle',
+    'doc/icingaweb/chapter/:chapterId',
     array(
         'controller'    => 'icingaweb',
         'action'        => 'chapter',

From cb17590b7174f902562521f87aabc13eb1cfe511 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 19 Aug 2014 09:57:22 +0200
Subject: [PATCH 91/96] doc: rename `nofollow' to `noFollow'

refs #4820
---
 modules/doc/library/Doc/DocParser.php       |  8 ++++----
 modules/doc/library/Doc/Section.php         | 12 ++++++------
 modules/doc/library/Doc/SectionRenderer.php |  2 +-
 modules/doc/library/Doc/TocRenderer.php     |  2 +-
 4 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 559a9a7d3..8fa34ba77 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -143,17 +143,17 @@ class DocParser
                         }
                         $path[] = $title;
                         $id = implode('-', $path);
-                        $nofollow = true;
+                        $noFollow = true;
                     } else {
-                        $nofollow = false;
+                        $noFollow = false;
                     }
                     if ($stack->isEmpty()) {
                         $chapterId = $id;
-                        $section = new Section($id, $title, $level, $nofollow, $chapterId);
+                        $section = new Section($id, $title, $level, $noFollow, $chapterId);
                         $tree->addRoot($section);
                     } else {
                         $chapterId = $stack->bottom()->getId();
-                        $section = new Section($id, $title, $level, $nofollow, $chapterId);
+                        $section = new Section($id, $title, $level, $noFollow, $chapterId);
                         $tree->addChild($section, $stack->top());
                     }
                     $stack->push($section);
diff --git a/modules/doc/library/Doc/Section.php b/modules/doc/library/Doc/Section.php
index ac458b6fe..5cf3d61e2 100644
--- a/modules/doc/library/Doc/Section.php
+++ b/modules/doc/library/Doc/Section.php
@@ -37,7 +37,7 @@ class Section implements Identifiable
      *
      * @var bool
      */
-    protected $nofollow;
+    protected $noFollow;
 
     /**
      * The ID of the chapter the section is part of
@@ -59,15 +59,15 @@ class Section implements Identifiable
      * @param string    $id             The ID of the section
      * @param string    $title          The title of the section
      * @param int       $level          The header level
-     * @param bool      $nofollow       Whether to instruct search engines to not index the link to the section
+     * @param bool      $noFollow       Whether to instruct search engines to not index the link to the section
      * @param string    $chapterId      The ID of the chapter the section is part of
      */
-    public function __construct($id, $title, $level, $nofollow, $chapterId)
+    public function __construct($id, $title, $level, $noFollow, $chapterId)
     {
         $this->id = $id;
         $this->title = $title;
         $this->level = $level;
-        $this->nofollow = $nofollow;
+        $this->noFollow = $noFollow;
         $this->chapterId= $chapterId;
     }
 
@@ -106,9 +106,9 @@ class Section implements Identifiable
      *
      * @return bool
      */
-    public function isNofollow()
+    public function isNoFollow()
     {
-        return $this->nofollow;
+        return $this->noFollow;
     }
 
     /**
diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php
index 1b7c68f1b..bc7bfb742 100644
--- a/modules/doc/library/Doc/SectionRenderer.php
+++ b/modules/doc/library/Doc/SectionRenderer.php
@@ -69,7 +69,7 @@ class Callback
         return sprintf(
             '<a %s%shref="%s"',
             strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
-            $section->isNofollow() ? 'rel="nofollow" ' : '',
+            $section->isNoFollow() ? 'rel="nofollow" ' : '',
             $url->getAbsoluteUrl()
         );
     }
diff --git a/modules/doc/library/Doc/TocRenderer.php b/modules/doc/library/Doc/TocRenderer.php
index 4b9d96a05..b4ad4228c 100644
--- a/modules/doc/library/Doc/TocRenderer.php
+++ b/modules/doc/library/Doc/TocRenderer.php
@@ -96,7 +96,7 @@ class TocRenderer extends Renderer
             $url->setAnchor($this->encodeAnchor($section->getId()));
             $this->content[] = sprintf(
                 '<li><a %shref="%s">%s</a>',
-                $section->isNofollow() ? 'rel="nofollow" ' : '',
+                $section->isNoFollow() ? 'rel="nofollow" ' : '',
                 $url->getAbsoluteUrl(),
                 $view->escape($section->getTitle())
             );

From 5c52e447f5d01df93c8e101848193384a9373e2f Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 19 Aug 2014 10:26:38 +0200
Subject: [PATCH 92/96] doc: upper case first character of a documentation's
 title

refs #4820
---
 modules/doc/application/views/scripts/toc.phtml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/doc/application/views/scripts/toc.phtml b/modules/doc/application/views/scripts/toc.phtml
index 8ffe2820d..7e92a6b26 100644
--- a/modules/doc/application/views/scripts/toc.phtml
+++ b/modules/doc/application/views/scripts/toc.phtml
@@ -1,5 +1,5 @@
 <div class="controls">
-    <h1><?= $docName ?> <?= $this->translate('Documentation'); ?></h1>
+    <h1><?= $this->translate(sprintf('%s Documentation', ucfirst($docName))); ?></h1>
 </div>
 <div class="content toc" data-base-target="_next">
     <?= $tocRenderer->render($this, $this->getHelper('Url')); ?>

From 127e4f444f7536f1bba601c46e2a097bfa03f42c Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 19 Aug 2014 11:30:56 +0200
Subject: [PATCH 93/96] doc: show prev chapter, index and next chapter links

refs #4820
---
 .../controllers/IcingawebController.php       |  7 +-
 .../controllers/ModuleController.php          |  1 +
 .../doc/application/views/scripts/toc.phtml   |  2 +-
 modules/doc/library/Doc/DocController.php     |  5 +-
 modules/doc/library/Doc/SectionRenderer.php   | 79 ++++++++++++++++++-
 modules/doc/library/Doc/TocRenderer.php       |  2 +-
 modules/doc/public/css/module.less            | 19 +++++
 7 files changed, 108 insertions(+), 7 deletions(-)

diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
index 117d6eb0e..967a2b768 100644
--- a/modules/doc/application/controllers/IcingawebController.php
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -30,7 +30,12 @@ class Doc_IcingawebController extends DocController
                 404
             );
         }
-        $this->renderChapter(Icinga::app()->getApplicationDir('/../doc'), $chapterId, 'doc/icingaweb/chapter');
+        $this->renderChapter(
+            Icinga::app()->getApplicationDir('/../doc'),
+            $chapterId,
+            'doc/icingaweb/toc',
+            'doc/icingaweb/chapter'
+        );
     }
 
     /**
diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index 7429dbdf2..19418a800 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -102,6 +102,7 @@ class Doc_ModuleController extends DocController
             $this->renderChapter(
                 $moduleManager->getModuleDir($moduleName, '/doc'),
                 $chapterId,
+                $this->_helper->url->url(array('moduleName' => $moduleName), 'doc/module/toc'),
                 'doc/module/chapter',
                 array('moduleName' => $moduleName)
             );
diff --git a/modules/doc/application/views/scripts/toc.phtml b/modules/doc/application/views/scripts/toc.phtml
index 7e92a6b26..3f6c6b37c 100644
--- a/modules/doc/application/views/scripts/toc.phtml
+++ b/modules/doc/application/views/scripts/toc.phtml
@@ -1,6 +1,6 @@
 <div class="controls">
     <h1><?= $this->translate(sprintf('%s Documentation', ucfirst($docName))); ?></h1>
 </div>
-<div class="content toc" data-base-target="_next">
+<div class="content toc">
     <?= $tocRenderer->render($this, $this->getHelper('Url')); ?>
 </div>
diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
index 16824ce0c..88beeee96 100644
--- a/modules/doc/library/Doc/DocController.php
+++ b/modules/doc/library/Doc/DocController.php
@@ -13,15 +13,17 @@ class DocController extends ModuleActionController
      *
      * @param string    $path           Path to the documentation
      * @param string    $chapterId      ID of the chapter
+     * @param string    $tocUrl
      * @param string    $url
      * @param array     $urlParams
      */
-    protected function renderChapter($path, $chapterId, $url, array $urlParams = array())
+    protected function renderChapter($path, $chapterId, $tocUrl, $url, array $urlParams = array())
     {
         $parser = new DocParser($path);
         $this->view->sectionRenderer = new SectionRenderer(
             $parser->getDocTree(),
             SectionRenderer::decodeUrlParam($chapterId),
+            $tocUrl,
             $url,
             $urlParams
         );
@@ -60,6 +62,7 @@ class DocController extends ModuleActionController
         $this->view->sectionRenderer = new SectionRenderer(
             $docTree,
             null,
+            null,
             $url,
             $urlParams
         );
diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php
index bc7bfb742..0f925bafb 100644
--- a/modules/doc/library/Doc/SectionRenderer.php
+++ b/modules/doc/library/Doc/SectionRenderer.php
@@ -87,6 +87,8 @@ class SectionRenderer extends Renderer
      */
     protected $docTree;
 
+    protected $tocUrl;
+
     /**
      * The URL to replace links with
      *
@@ -120,12 +122,13 @@ class SectionRenderer extends Renderer
      *
      * @param   DocTree     $docTree        The documentation tree
      * @param   string|null $chapterId      If not null, the chapter ID to filter for
+     * @param   string      $tocUrl
      * @param   string      $url            The URL to replace links with
      * @param   array       $urlParams      Additional URL parameters
      *
      * @throws  ChapterNotFoundException    If the chapter to filter for was not found
      */
-    public function __construct(DocTree $docTree, $chapterId, $url, array $urlParams)
+    public function __construct(DocTree $docTree, $chapterId, $tocUrl, $url, array $urlParams)
     {
         if ($chapterId !== null) {
             $filter = new SectionFilterIterator($docTree, $chapterId);
@@ -142,6 +145,7 @@ class SectionRenderer extends Renderer
             parent::__construct($docTree, RecursiveIteratorIterator::SELF_FIRST);
         }
         $this->docTree = $docTree;
+        $this->tocUrl = $tocUrl;
         $this->url = $url;
         $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams);
         $this->parsedown = Parsedown::instance();
@@ -182,9 +186,11 @@ class SectionRenderer extends Renderer
      *
      * @param   View                    $view
      * @param   Zend_View_Helper_Url    $zendUrlHelper
+     * @param   bool                    $renderNavigation
+     *
      * @return  string
      */
-    public function render(View $view, Zend_View_Helper_Url $zendUrlHelper)
+    public function render(View $view, Zend_View_Helper_Url $zendUrlHelper, $renderNavigation = true)
     {
         $callback = new Callback($this->docTree, $view, $zendUrlHelper, $this->url, $this->urlParams);
         $content = array();
@@ -192,7 +198,7 @@ class SectionRenderer extends Renderer
             $section = $node->getValue();
             /* @var $section \Icinga\Module\Doc\Section */
             $content[] = sprintf(
-                '<a name="%1$s"></a> <h%2$d>%3$s</h%2$d>',
+                '<a name="%1$s"></a><h%2$d>%3$s</h%2$d>',
                 Renderer::encodeAnchor($section->getId()),
                 $section->getLevel(),
                 $view->escape($section->getTitle())
@@ -213,6 +219,73 @@ class SectionRenderer extends Renderer
                 $html
             );
         }
+        if ($renderNavigation) {
+            foreach ($this->docTree as $chapter) {
+                if ($chapter->getValue()->getId() === $section->getChapterId()) {
+                    $content[] = '<ul class="navigation">';
+                    $this->docTree->prev();
+                    $prev = $this->docTree->current();
+                    if ($prev !== null) {
+                        $prev = $prev->getValue();
+                        $path = $zendUrlHelper->url(
+                            array_merge(
+                                $this->urlParams,
+                                array(
+                                    'chapterId' => $this->encodeUrlParam($prev->getChapterId())
+                                )
+                            ),
+                            $this->url,
+                            false,
+                            false
+                        );
+                        $url = $view->url($path);
+                        $url->setAnchor($this->encodeAnchor($prev->getId()));
+                        $content[] = sprintf(
+                            '<li><a %shref="%s">%s</a></li>',
+                            $prev->isNoFollow() ? 'rel="nofollow" ' : '',
+                            $url->getAbsoluteUrl(),
+                            $view->escape($prev->getTitle())
+                        );
+                        $this->docTree->next();
+                        $this->docTree->next();
+                    } else {
+                        $this->docTree->rewind();
+                        $this->docTree->next();
+                    }
+                    $url = $view->url($this->tocUrl);
+                    $content[] = sprintf(
+                        '<li><a href="%s">%s</a></li>',
+                        $url->getAbsoluteUrl(),
+                        mt('doc', 'Index')
+                    );
+                    $next = $this->docTree->current();
+                    if ($next !== null) {
+                        $next = $next->getValue();
+                        $path = $zendUrlHelper->url(
+                            array_merge(
+                                $this->urlParams,
+                                array(
+                                    'chapterId' => $this->encodeUrlParam($next->getChapterId())
+                                )
+                            ),
+                            $this->url,
+                            false,
+                            false
+                        );
+                        $url = $view->url($path);
+                        $url->setAnchor($this->encodeAnchor($next->getId()));
+                        $content[] = sprintf(
+                            '<li><a %shref="%s">%s</a></li>',
+                            $next->isNoFollow() ? 'rel="nofollow" ' : '',
+                            $url->getAbsoluteUrl(),
+                            $view->escape($next->getTitle())
+                        );
+                    }
+                    $content[] = '</ul>';
+                    break;
+                }
+            }
+        }
         return implode("\n", $content);
     }
 }
diff --git a/modules/doc/library/Doc/TocRenderer.php b/modules/doc/library/Doc/TocRenderer.php
index b4ad4228c..4061e80e3 100644
--- a/modules/doc/library/Doc/TocRenderer.php
+++ b/modules/doc/library/Doc/TocRenderer.php
@@ -9,7 +9,7 @@ use Zend_View_Helper_Url;
 use Icinga\Web\View;
 
 /**
- * toc renderer
+ * TOC renderer
  */
 class TocRenderer extends Renderer
 {
diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less
index 52fff5c32..2676d0f24 100644
--- a/modules/doc/public/css/module.less
+++ b/modules/doc/public/css/module.less
@@ -31,3 +31,22 @@ code {
 pre > code {
   display: inline-block;
 }
+
+div.chapter > ul.navigation {
+  margin: 0;
+  padding: 0.4em;
+  text-align: center;
+  background-color: #888;
+
+  li {
+    list-style: none;
+    display: inline;
+    margin: 0.2em;
+    padding: 0;
+
+    a {
+      color: #fff;
+      text-decoration: none;
+    }
+  }
+}

From d35e0816c96619f6292bce4e8dd8d36220d970d8 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 19 Aug 2014 13:20:46 +0200
Subject: [PATCH 94/96] doc: use `sprintf' in exceptions

refs #4820
---
 .../application/controllers/ModuleController.php    |  4 ++--
 modules/doc/library/Doc/DocParser.php               | 13 +++++++++----
 modules/doc/library/Doc/DocTree.php                 |  2 +-
 modules/doc/library/Doc/SectionRenderer.php         |  2 +-
 4 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index 19418a800..26dac8626 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -44,13 +44,13 @@ class Doc_ModuleController extends DocController
         $moduleManager = Icinga::app()->getModuleManager();
         if (! $moduleManager->hasInstalled($moduleName)) {
             throw new Zend_Controller_Action_Exception(
-                $this->translate('Module') . ' \'' . $moduleName . '\' ' . $this->translate('is not installed'),
+                $this->translate(sprintf('Module \'%s\' is not installed', $moduleName)),
                 404
             );
         }
         if (! $moduleManager->hasEnabled($moduleName)) {
             throw new Zend_Controller_Action_Exception(
-                $this->translate('Module') . ' \'' . $moduleName . '\' ' . $this->translate('is not enabled'),
+                $this->translate(sprintf('Module \'%s\' is not enabled', $moduleName)),
                 404
             );
         }
diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index 8fa34ba77..e0311e6ba 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -41,19 +41,24 @@ class DocParser
     {
         if (! is_dir($path)) {
             throw new DocException(
-                mt('doc', 'Documentation directory') . ' \'' . $path . '\' ' . mt('doc', 'does not exist')
+                mt('doc', sprintf('Documentation directory \'%s\' does not exist', $path))
             );
         }
         if (! is_readable($path)) {
             throw new DocException(
-                mt('doc', 'Documentation directory') . ' \'' . $path . '\' ' . mt('doc', 'is not readable')
+                mt('doc', sprintf('Documentation directory \'%s\' is not readable', $path))
             );
         }
         $docIterator = new DocIterator($path);
         if ($docIterator->count() === 0) {
             throw new DocEmptyException(
-                mt('doc', 'Documentation directory') . ' \'' . $path . '\' '
-                    . mt('doc', 'does not contain any non-empty Markdown file (\'.md\' suffix')
+                mt(
+                    'doc',
+                    sprintf(
+                        'Documentation directory \'%s\' does not contain any non-empty Markdown file (\'.md\' suffix)',
+                        $path
+                    )
+                )
             );
         }
         $this->path = $path;
diff --git a/modules/doc/library/Doc/DocTree.php b/modules/doc/library/Doc/DocTree.php
index 07a0c5279..4223d8e99 100644
--- a/modules/doc/library/Doc/DocTree.php
+++ b/modules/doc/library/Doc/DocTree.php
@@ -57,7 +57,7 @@ class DocTree extends Node
         }
         if (! isset($this->nodes[$parentId])) {
             throw new LogicException(
-                sprintf('Can\'t add child node: there\'s no parent node having the id \'%s\'', $parentId)
+                mt('doc', sprintf('Can\'t add child node: there\'s no parent node having the id \'%s\'', $parentId))
             );
         }
         $this->nodes[$childId] = $this->nodes[$parentId]->appendChild($child);
diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php
index 0f925bafb..fab6d66c4 100644
--- a/modules/doc/library/Doc/SectionRenderer.php
+++ b/modules/doc/library/Doc/SectionRenderer.php
@@ -134,7 +134,7 @@ class SectionRenderer extends Renderer
             $filter = new SectionFilterIterator($docTree, $chapterId);
             if ($filter->count() === 0) {
                 throw new ChapterNotFoundException(
-                    mt('doc', 'Chapter') . ' \'' . $chapterId . '\' ' . mt('doc', 'not found')
+                    mt('doc', sprintf('Chapter \'%s\' not found', $chapterId))
                 );
             }
             parent::__construct(

From 55b3a5384a5e2ed89a974eadc6b454ac14ea5ab8 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 19 Aug 2014 13:38:18 +0200
Subject: [PATCH 95/96] doc: set page title

refs #4820
---
 modules/doc/application/views/scripts/toc.phtml |  2 +-
 modules/doc/library/Doc/DocController.php       |  3 +++
 modules/doc/library/Doc/SectionRenderer.php     | 15 ++++++++-------
 modules/doc/public/css/module.less              | 10 ++++++++++
 4 files changed, 22 insertions(+), 8 deletions(-)

diff --git a/modules/doc/application/views/scripts/toc.phtml b/modules/doc/application/views/scripts/toc.phtml
index 3f6c6b37c..ca6283d67 100644
--- a/modules/doc/application/views/scripts/toc.phtml
+++ b/modules/doc/application/views/scripts/toc.phtml
@@ -1,5 +1,5 @@
 <div class="controls">
-    <h1><?= $this->translate(sprintf('%s Documentation', ucfirst($docName))); ?></h1>
+    <h1><?= $title ?></h1>
 </div>
 <div class="content toc">
     <?= $tocRenderer->render($this, $this->getHelper('Url')); ?>
diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
index 88beeee96..66895bfb4 100644
--- a/modules/doc/library/Doc/DocController.php
+++ b/modules/doc/library/Doc/DocController.php
@@ -27,6 +27,7 @@ class DocController extends ModuleActionController
             $url,
             $urlParams
         );
+        $this->view->title = $chapterId;
         $this->_helper->viewRenderer('chapter', null, true);
     }
 
@@ -42,7 +43,9 @@ class DocController extends ModuleActionController
     {
         $parser = new DocParser($path);
         $this->view->tocRenderer = new TocRenderer($parser->getDocTree(), $url, $urlParams);
+        $name = ucfirst($name);
         $this->view->docName = $name;
+        $this->view->title = $this->translate(sprintf('%s Documentation', $name));
         $this->_helper->viewRenderer('toc', null, true);
     }
 
diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php
index fab6d66c4..6281b6d76 100644
--- a/modules/doc/library/Doc/SectionRenderer.php
+++ b/modules/doc/library/Doc/SectionRenderer.php
@@ -222,7 +222,7 @@ class SectionRenderer extends Renderer
         if ($renderNavigation) {
             foreach ($this->docTree as $chapter) {
                 if ($chapter->getValue()->getId() === $section->getChapterId()) {
-                    $content[] = '<ul class="navigation">';
+                    $navigation = array('<ul class="navigation">');
                     $this->docTree->prev();
                     $prev = $this->docTree->current();
                     if ($prev !== null) {
@@ -240,8 +240,8 @@ class SectionRenderer extends Renderer
                         );
                         $url = $view->url($path);
                         $url->setAnchor($this->encodeAnchor($prev->getId()));
-                        $content[] = sprintf(
-                            '<li><a %shref="%s">%s</a></li>',
+                        $navigation[] = sprintf(
+                            '<li class="prev"><a %shref="%s">%s</a></li>',
                             $prev->isNoFollow() ? 'rel="nofollow" ' : '',
                             $url->getAbsoluteUrl(),
                             $view->escape($prev->getTitle())
@@ -253,7 +253,7 @@ class SectionRenderer extends Renderer
                         $this->docTree->next();
                     }
                     $url = $view->url($this->tocUrl);
-                    $content[] = sprintf(
+                    $navigation[] = sprintf(
                         '<li><a href="%s">%s</a></li>',
                         $url->getAbsoluteUrl(),
                         mt('doc', 'Index')
@@ -274,14 +274,15 @@ class SectionRenderer extends Renderer
                         );
                         $url = $view->url($path);
                         $url->setAnchor($this->encodeAnchor($next->getId()));
-                        $content[] = sprintf(
-                            '<li><a %shref="%s">%s</a></li>',
+                        $navigation[] = sprintf(
+                            '<li class="next"><a %shref="%s">%s</a></li>',
                             $next->isNoFollow() ? 'rel="nofollow" ' : '',
                             $url->getAbsoluteUrl(),
                             $view->escape($next->getTitle())
                         );
                     }
-                    $content[] = '</ul>';
+                    $navigation[] = '</ul>';
+                    $content = array_merge($navigation, $content, $navigation);
                     break;
                 }
             }
diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less
index 2676d0f24..d6d0d2a94 100644
--- a/modules/doc/public/css/module.less
+++ b/modules/doc/public/css/module.less
@@ -48,5 +48,15 @@ div.chapter > ul.navigation {
       color: #fff;
       text-decoration: none;
     }
+
+    &.prev {
+      padding-right: 0.6em;
+      border-right: 2px solid #fff;
+    }
+
+    &.next {
+      padding-left: 0.6em;
+      border-left: 2px solid #fff;
+    }
   }
 }

From f52b3f7b8c5757c961db958aad9d361e18b40957 Mon Sep 17 00:00:00 2001
From: Eric Lippmann <eric.lippmann@netways.de>
Date: Tue, 19 Aug 2014 13:38:34 +0200
Subject: [PATCH 96/96] doc/config: add menu.ini

refs #4820
---
 config/modules/doc/menu.ini | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 config/modules/doc/menu.ini

diff --git a/config/modules/doc/menu.ini b/config/modules/doc/menu.ini
new file mode 100644
index 000000000..86889b239
--- /dev/null
+++ b/config/modules/doc/menu.ini
@@ -0,0 +1,5 @@
+[Documentation]
+title       = "Documentation"
+icon        = "img/icons/comment.png"
+url         = "doc"
+priority    = 80