summaryrefslogtreecommitdiffstats
path: root/modules/doc/application
diff options
context:
space:
mode:
Diffstat (limited to 'modules/doc/application')
-rw-r--r--modules/doc/application/controllers/IcingawebController.php62
-rw-r--r--modules/doc/application/controllers/IndexController.php27
-rw-r--r--modules/doc/application/controllers/ModuleController.php206
-rw-r--r--modules/doc/application/controllers/SearchController.php97
-rw-r--r--modules/doc/application/controllers/StyleController.php41
-rw-r--r--modules/doc/application/views/scripts/chapter.phtml6
-rw-r--r--modules/doc/application/views/scripts/index/index.phtml19
-rw-r--r--modules/doc/application/views/scripts/module/index.phtml19
-rw-r--r--modules/doc/application/views/scripts/pdf.phtml5
-rw-r--r--modules/doc/application/views/scripts/search/index.phtml8
-rw-r--r--modules/doc/application/views/scripts/style/font.phtml15
-rw-r--r--modules/doc/application/views/scripts/style/guide.phtml112
-rw-r--r--modules/doc/application/views/scripts/toc.phtml6
13 files changed, 623 insertions, 0 deletions
diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
new file mode 100644
index 0000000..e841c41
--- /dev/null
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -0,0 +1,62 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Doc\DocController;
+
+class IcingawebController extends DocController
+{
+ /**
+ * Get the path to Icinga Web 2's documentation
+ *
+ * @return string
+ *
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If Icinga Web 2's documentation is not available
+ */
+ protected function getPath()
+ {
+ $path = Icinga::app()->getBaseDir('doc');
+ if (is_dir($path)) {
+ return $path;
+ }
+ if (($path = $this->Config()->get('documentation', 'icingaweb2')) !== null) {
+ if (is_dir($path)) {
+ return $path;
+ }
+ }
+ $this->httpNotFound($this->translate('Documentation for Icinga Web 2 is not available'));
+ }
+
+ /**
+ * View the toc of Icinga Web 2's documentation
+ */
+ public function tocAction()
+ {
+ $this->renderToc($this->getPath(), 'Icinga Web 2', 'doc/icingaweb/chapter');
+ }
+
+ /**
+ * View a chapter of Icinga Web 2's documentation
+ *
+ * @throws \Icinga\Exception\MissingParameterException If the required parameter 'chapter' is missing
+ */
+ public function chapterAction()
+ {
+ $chapter = $this->params->getRequired('chapter');
+ $this->renderChapter(
+ $this->getPath(),
+ $chapter,
+ 'doc/icingaweb/chapter'
+ );
+ }
+
+ /**
+ * View Icinga Web 2's documentation as PDF
+ */
+ public function pdfAction()
+ {
+ $this->renderPdf($this->getPath(), 'Icinga Web 2', 'doc/icingaweb/chapter');
+ }
+}
diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php
new file mode 100644
index 0000000..3ff5aa1
--- /dev/null
+++ b/modules/doc/application/controllers/IndexController.php
@@ -0,0 +1,27 @@
+<?php
+/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use Icinga\Module\Doc\DocController;
+use Icinga\Web\Url;
+
+/**
+ * Documentation module index
+ */
+class IndexController extends DocController
+{
+ /**
+ * Documentation module landing page
+ *
+ * Lists documentation links
+ */
+ public function indexAction()
+ {
+ $this->getTabs()->add('documentation', array(
+ 'active' => true,
+ 'title' => $this->translate('Documentation', 'Tab title'),
+ 'url' => Url::fromRequest()
+ ));
+ }
+}
diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
new file mode 100644
index 0000000..47dfb1c
--- /dev/null
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -0,0 +1,206 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use finfo;
+use SplFileInfo;
+use Icinga\Application\Icinga;
+use Icinga\Module\Doc\DocController;
+use Icinga\Module\Doc\Exception\DocException;
+use Icinga\Web\Url;
+
+class ModuleController extends DocController
+{
+ /**
+ * Get the path to a module documentation
+ *
+ * @param string $module The name of the module
+ * @param string $default The default path
+ * @param bool $suppressErrors Whether to not throw an exception if the module documentation is not available
+ *
+ * @return string|null Path to the documentation or null if the module documentation is not available
+ * and errors are suppressed
+ *
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the module documentation is not available and errors
+ * are not suppressed
+ */
+ protected function getPath($module, $default, $suppressErrors = false)
+ {
+ if (is_dir($default)) {
+ return $default;
+ }
+ if (($path = $this->Config()->get('documentation', 'modules')) !== null) {
+ $path = str_replace('{module}', $module, $path);
+ if (is_dir($path)) {
+ return $path;
+ }
+ }
+ if ($suppressErrors) {
+ return null;
+ }
+ $this->httpNotFound($this->translate('Documentation for module \'%s\' is not available'), $module);
+ }
+
+ /**
+ * List modules which are enabled and having the 'doc' directory
+ */
+ public function indexAction()
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ $modules = array();
+ foreach ($moduleManager->listInstalledModules() as $module) {
+ $path = $this->getPath($module, $moduleManager->getModuleDir($module, '/doc'), true);
+ if ($path !== null) {
+ $modules[] = $moduleManager->getModule($module, false);
+ }
+ }
+ $this->view->modules = $modules;
+ $this->getTabs()->add('module-documentation', array(
+ 'active' => true,
+ 'title' => $this->translate('Module Documentation', 'Tab title'),
+ 'url' => Url::fromRequest()
+ ));
+ }
+
+ /**
+ * Assert that the given module is installed
+ *
+ * @param string $moduleName
+ *
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
+ */
+ protected function assertModuleInstalled($moduleName)
+ {
+ $moduleManager = Icinga::app()->getModuleManager();
+ if (! $moduleManager->hasInstalled($moduleName)) {
+ $this->httpNotFound($this->translate('Module \'%s\' is not installed'), $moduleName);
+ }
+ }
+
+ /**
+ * View the toc of a module's documentation
+ *
+ * @throws \Icinga\Exception\MissingParameterException If the required parameter 'moduleName' is empty
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
+ * @see assertModuleInstalled()
+ */
+ public function tocAction()
+ {
+ $module = $this->params->getRequired('moduleName');
+ $this->assertModuleInstalled($module);
+ $moduleManager = Icinga::app()->getModuleManager();
+ $name = $moduleManager->getModule($module, false)->getTitle();
+ try {
+ $this->renderToc(
+ $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
+ $name,
+ 'doc/module/chapter',
+ array('moduleName' => $module)
+ );
+ } catch (DocException $e) {
+ $this->httpNotFound($e->getMessage());
+ }
+ }
+
+ /**
+ * View a chapter of a module's documentation
+ *
+ * @throws \Icinga\Exception\MissingParameterException If one of the required parameters 'moduleName' and
+ * 'chapter' is empty
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
+ * @see assertModuleInstalled()
+ */
+ public function chapterAction()
+ {
+ $module = $this->params->getRequired('moduleName');
+ $this->assertModuleInstalled($module);
+ $chapter = $this->params->getRequired('chapter');
+ $this->view->moduleName = $module;
+ try {
+ $this->renderChapter(
+ $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
+ $chapter,
+ 'doc/module/chapter',
+ 'doc/module/img',
+ array('moduleName' => $module)
+ );
+ } catch (DocException $e) {
+ $this->httpNotFound($e->getMessage());
+ }
+ }
+
+ /**
+ * Deliver images
+ */
+ public function imageAction()
+ {
+ $module = $this->params->getRequired('moduleName');
+ $image = $this->params->getRequired('image');
+ $docPath = $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc'));
+ $imagePath = realpath($docPath . '/' . $image);
+ if ($imagePath === false || substr($imagePath, 0, strlen($docPath)) !== $docPath) {
+ $this->httpNotFound('%s does not exist', $image);
+ }
+
+ $this->_helper->viewRenderer->setNoRender(true);
+ $this->_helper->layout()->disableLayout();
+
+ $imageInfo = new SplFileInfo($imagePath);
+
+ $etag = md5($imageInfo->getMTime() . $imagePath);
+ $lastModified = gmdate('D, d M Y H:i:s T', $imageInfo->getMTime());
+ $match = false;
+
+ if (isset($_SERER['HTTP_IF_NONE_MATCH'])) {
+ $ifNoneMatch = explode(', ', stripslashes($_SERVER['HTTP_IF_NONE_MATCH']));
+ foreach ($ifNoneMatch as $tag) {
+ if ($tag === $etag) {
+ $match = true;
+ break;
+ }
+ }
+ } elseif (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+ $lastModifiedSince = stripslashes($_SERVER['HTTP_IF_MODIFIED_SINCE']);
+ if ($lastModifiedSince === $lastModified) {
+ $match = true;
+ }
+ }
+
+ $this->getResponse()
+ ->setHeader('ETag', $etag)
+ ->setHeader('Cache-Control', 'no-transform,public,max-age=3600,must-revalidate', true);
+
+ if ($match) {
+ $this->getResponse()->setHttpResponseCode(304);
+ } else {
+ $finfo = new finfo();
+ $this->getResponse()
+ ->setHeader('Content-Type', $finfo->file($imagePath, FILEINFO_MIME_TYPE))
+ ->setHeader('Content-Length', $imageInfo->getSize())
+ ->sendHeaders();
+
+ ob_end_clean();
+ readfile($imagePath);
+ }
+ }
+
+ /**
+ * View a module's documentation as PDF
+ *
+ * @throws \Icinga\Exception\MissingParameterException If the required parameter 'moduleName' is empty
+ * @throws \Icinga\Exception\Http\HttpNotFoundException If the given module is not installed
+ * @see assertModuleInstalled()
+ */
+ public function pdfAction()
+ {
+ $module = $this->params->getRequired('moduleName');
+ $this->assertModuleInstalled($module);
+ $this->renderPdf(
+ $this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
+ $module,
+ 'doc/module/chapter',
+ array('moduleName' => $module)
+ );
+ }
+}
diff --git a/modules/doc/application/controllers/SearchController.php b/modules/doc/application/controllers/SearchController.php
new file mode 100644
index 0000000..6ae2b14
--- /dev/null
+++ b/modules/doc/application/controllers/SearchController.php
@@ -0,0 +1,97 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Module\Doc\DocController;
+use Icinga\Module\Doc\DocParser;
+use Icinga\Module\Doc\Exception\DocException;
+use Icinga\Module\Doc\Renderer\DocSearchRenderer;
+use Icinga\Module\Doc\Search\DocSearch;
+use Icinga\Module\Doc\Search\DocSearchIterator;
+
+class SearchController extends DocController
+{
+ /**
+ * Render search
+ */
+ public function indexAction()
+ {
+ $parser = new DocParser($this->getWebPath());
+ $search = new DocSearchRenderer(
+ new DocSearchIterator(
+ $parser->getDocTree()->getIterator(),
+ new DocSearch($this->params->get('q'))
+ )
+ );
+ $search->setUrl('doc/icingaweb/chapter');
+ if (strlen($this->params->get('q')) < 3) {
+ $this->view->searches = array();
+ return;
+ }
+ $searches = array(
+ 'Icinga Web 2' => $search
+ );
+ foreach (Icinga::app()->getModuleManager()->listEnabledModules() as $module) {
+ if (($path = $this->getModulePath($module)) !== null) {
+ try {
+ $parser = new DocParser($path);
+ $search = new DocSearchRenderer(
+ new DocSearchIterator(
+ $parser->getDocTree()->getIterator(),
+ new DocSearch($this->params->get('q'))
+ )
+ );
+ } catch (DocException $e) {
+ continue;
+ }
+ $search
+ ->setUrl('doc/module/chapter')
+ ->setUrlParams(array('moduleName' => $module));
+ $searches[$module] = $search;
+ }
+ }
+ $this->view->searches = $searches;
+ }
+
+ /**
+ * Get the path to a module's documentation
+ *
+ * @param string $module
+ *
+ * @return string|null
+ */
+ protected function getModulePath($module)
+ {
+ if (is_dir(($path = Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')))) {
+ return $path;
+ }
+ if (($path = $this->Config()->get('documentation', 'modules')) !== null) {
+ $path = str_replace('{module}', $module, $path);
+ if (is_dir($path)) {
+ return $path;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the path to Icinga Web 2's documentation
+ *
+ * @return string
+ */
+ protected function getWebPath()
+ {
+ $path = Icinga::app()->getBaseDir('doc');
+ if (is_dir($path)) {
+ return $path;
+ }
+ if (($path = $this->Config()->get('documentation', 'icingaweb2')) !== null) {
+ if (is_dir($path)) {
+ return $path;
+ }
+ }
+ $this->httpNotFound($this->translate('Documentation for Icinga Web 2 is not available'));
+ }
+}
diff --git a/modules/doc/application/controllers/StyleController.php b/modules/doc/application/controllers/StyleController.php
new file mode 100644
index 0000000..5890367
--- /dev/null
+++ b/modules/doc/application/controllers/StyleController.php
@@ -0,0 +1,41 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Controllers;
+
+use Icinga\Application\Icinga;
+use Icinga\Web\Controller;
+use Icinga\Web\Widget;
+
+class StyleController extends Controller
+{
+ public function guideAction()
+ {
+ $this->view->tabs = $this->tabs()->activate('guide');
+ }
+
+ public function fontAction()
+ {
+ $this->view->tabs = $this->tabs()->activate('font');
+ $confFile = Icinga::app()->getApplicationDir('fonts/fontello-ifont/config.json');
+ $this->view->font = json_decode(file_get_contents($confFile));
+ }
+
+ protected function tabs()
+ {
+ return Widget::create('tabs')->add(
+ 'guide',
+ array(
+ 'label' => $this->translate('Style Guide'),
+ 'url' => 'doc/style/guide'
+ )
+ )->add(
+ 'font',
+ array(
+ 'label' => $this->translate('Icons'),
+ 'title' => $this->translate('List all available icons'),
+ 'url' => 'doc/style/font'
+ )
+ );
+ }
+}
diff --git a/modules/doc/application/views/scripts/chapter.phtml b/modules/doc/application/views/scripts/chapter.phtml
new file mode 100644
index 0000000..8cd4f6e
--- /dev/null
+++ b/modules/doc/application/views/scripts/chapter.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $this->tabs ?>
+</div>
+<div class="content chapter">
+ <?= /** @var \Icinga\Module\Doc\Renderer\DocSectionRenderer $section */ $section ?>
+</div>
diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml
new file mode 100644
index 0000000..9bf745a
--- /dev/null
+++ b/modules/doc/application/views/scripts/index/index.phtml
@@ -0,0 +1,19 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
+</div>
+<div class="content">
+ <ul>
+ <li><?= $this->qlink(
+ 'Icinga Web 2',
+ 'doc/icingaweb/toc',
+ null,
+ array('title' => $this->translate('Show the documentation\'s table of contents for Icinga Web 2'))
+ ) ?></li>
+ <li><?= $this->qlink(
+ $this->translate('Module documentations'),
+ 'doc/module/',
+ null,
+ array('title' => $this->translate('List all modules for which documentation is available'))
+ ) ?></li>
+ </ul>
+</div>
diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml
new file mode 100644
index 0000000..f70d69a
--- /dev/null
+++ b/modules/doc/application/views/scripts/module/index.phtml
@@ -0,0 +1,19 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+
+<div class="content">
+ <ul>
+ <?php foreach ($modules as $module): /** @var \Icinga\Application\Modules\Module $module */ ?>
+ <li><?= $this->qlink(
+ $module->getTitle(),
+ 'doc/module/toc',
+ array('moduleName' => $module->getName()),
+ array('title' => sprintf(
+ $this->translate('Show the documentation\'s table of contents for the %s'),
+ $module->getTitle()
+ ))
+ ) ?></li>
+ <?php endforeach ?>
+ </ul>
+</div>
diff --git a/modules/doc/application/views/scripts/pdf.phtml b/modules/doc/application/views/scripts/pdf.phtml
new file mode 100644
index 0000000..2666efb
--- /dev/null
+++ b/modules/doc/application/views/scripts/pdf.phtml
@@ -0,0 +1,5 @@
+<div class="content">
+ <h1><?= /** @var string $title */ $title ?></h1>
+ <?= /** @var \Icinga\Module\Doc\Renderer\DocTocRenderer $toc */ $toc ?>
+ <?= /** @var \Icinga\Module\Doc\Renderer\DocSectionRenderer $section */ $section ?>
+</div>
diff --git a/modules/doc/application/views/scripts/search/index.phtml b/modules/doc/application/views/scripts/search/index.phtml
new file mode 100644
index 0000000..c613f04
--- /dev/null
+++ b/modules/doc/application/views/scripts/search/index.phtml
@@ -0,0 +1,8 @@
+<div class="content">
+ <?php foreach (/** @var \Icinga\Module\Doc\Renderer\DocSearchRenderer[] $searches */ $searches as $title => $search): ?>
+ <h2><?= $this->escape($title) ?></h2>
+ <?= $search->isEmpty()
+ ? $this->translate('No documentation found matching the filter')
+ : $search ?>
+ <?php endforeach ?>
+</div>
diff --git a/modules/doc/application/views/scripts/style/font.phtml b/modules/doc/application/views/scripts/style/font.phtml
new file mode 100644
index 0000000..c84a983
--- /dev/null
+++ b/modules/doc/application/views/scripts/style/font.phtml
@@ -0,0 +1,15 @@
+<div class="controls">
+<?= $this->tabs ?>
+<h1>Icinga Web 2 Icons</h1>
+</div>
+
+<div class="content">
+<?php foreach ($this->font->glyphs as $icon): ?>
+<!-- TODO: move CSS away //-->
+<div style="width: 33%; font-size: 1.5em; height: 2em; float: left;" class="<?=
+ $this->font->css_prefix_text . $icon->css
+?>">
+<?= $this->escape($icon->css) ?> <span style="font-size: 0.6em">(0x<?= dechex($icon->code) ?>)</span>
+</div>
+<?php endforeach ?>
+</div>
diff --git a/modules/doc/application/views/scripts/style/guide.phtml b/modules/doc/application/views/scripts/style/guide.phtml
new file mode 100644
index 0000000..f2f57d2
--- /dev/null
+++ b/modules/doc/application/views/scripts/style/guide.phtml
@@ -0,0 +1,112 @@
+<div class="controls">
+ <?= $this->tabs ?>
+</div>
+
+<div class="content styleguide">
+ <div class="section">
+ <h1>Icinga Web 2 Design Guidelines</h1>
+
+ <ul class="toc">
+ <li><a href="#headings">Headings</a></li>
+ <li><a href="#block-content">Block Content</a></li>
+ <li><a href="#tables">Tables</a></li>
+ <li><a href="#comment-list">Comment List</a></li>
+ <li><a href="#blockquote">Blockquote</a></li>
+ </ul>
+ </div>
+
+ <div class="section">
+ <h2 id="headings">Headings</h2>
+ <h1>Header h1</h1>
+ <h2>Header h2</h2>
+ <h3>Header h3</h3>
+ <h4>Header h4</h4>
+ <h5>Header h5</h5>
+ <h6>Header h6</h6>
+ </div>
+
+ <div class="section">
+ <h2 id="block-content">Block Content</h2>
+ <h3>Paragraph</h3>
+ <p>
+ This is a paragraph. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
+ invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo
+ dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
+ A <a href="#">link inside a paragraph</a>.
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
+ dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+ Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
+ </p>
+ </div>
+
+ <div class="section">
+ <h2 id="tables">Tables</h2>
+ <table class="common-table">
+ <thead>
+ <tr>
+ <th>Table Head - th in thead</th>
+ <td>td in thead<td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <th>Tbody - th</th>
+ <td>Tbody - td</td>
+ </tr>
+ <tr>
+ <th>Tbody - th</th>
+ <td>Tbody - td</td>
+ </tr>
+ <tr>
+ <th>Tbody - th</th>
+ <td>Tbody - td</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="section">
+ <h2 id="comment-list"><?= $this->translate('Comment List') ?></h2>
+ <dl class="comment-list">
+ <dt>
+ John Doe
+ <span class="comment-time">
+ <?= $this->translate('commented') ?>
+ <span class="relative-time"><?= $this->translate('some time ago') ?></span>
+ </span>
+ <i class="remove-action icon-cancel"></i>
+ </dt>
+ <dd>
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore
+ <br>
+ et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+ </dd>
+ <dt>
+ Richard Roe
+ <span class="comment-time">
+ <?= $this->translate('commented') ?>
+ <span class="relative-time"><?= $this->translate('some time ago') ?></span>
+ </span>
+ <i class="remove-action icon-cancel"></i>
+ </dt>
+ <dd>
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore
+ <br>
+ et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+ </dd>
+ </dl>
+ </div>
+
+ <div class="section">
+ <h2 id="blockquote"><?= $this->translate('Blockquote') ?></h2>
+ <blockquote>
+ Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
+ invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
+ At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren,
+ no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
+ consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore
+ magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
+ Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
+ </blockquote>
+ </div>
+</div>
diff --git a/modules/doc/application/views/scripts/toc.phtml b/modules/doc/application/views/scripts/toc.phtml
new file mode 100644
index 0000000..d08830b
--- /dev/null
+++ b/modules/doc/application/views/scripts/toc.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+ <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?>
+</div>
+<div class="content">
+ <?= /** @var \Icinga\Module\Doc\Renderer\DocTocRenderer $toc */ $toc ?>
+</div>