diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:39:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:39:39 +0000 |
commit | 8ca6cc32b2c789a3149861159ad258f2cb9491e3 (patch) | |
tree | 2492de6f1528dd44eaa169a5c1555026d9cb75ec /modules/doc | |
parent | Initial commit. (diff) | |
download | icingaweb2-upstream.tar.xz icingaweb2-upstream.zip |
Adding upstream version 2.11.4.upstream/2.11.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'modules/doc')
35 files changed, 2794 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> diff --git a/modules/doc/configuration.php b/modules/doc/configuration.php new file mode 100644 index 0000000..8b909ef --- /dev/null +++ b/modules/doc/configuration.php @@ -0,0 +1,24 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +/** @var $this \Icinga\Application\Modules\Module */ + +$section = $this->menuSection(N_('Documentation'), array( + 'title' => 'Documentation', + 'icon' => 'book', + 'url' => 'doc', + 'priority' => 700 +)); + +$section->add('Icinga Web 2', array( + 'url' => 'doc/icingaweb/toc', +)); +$section->add('Module documentations', array( + 'url' => 'doc/module', +)); +$section->add(N_('Developer - Style'), array( + 'url' => 'doc/style/guide', + 'priority' => 790 +)); + +$this->provideSearchUrl($this->translate('Doc'), 'doc/search', -10); diff --git a/modules/doc/doc/01-About.md b/modules/doc/doc/01-About.md new file mode 100644 index 0000000..02e2cbf --- /dev/null +++ b/modules/doc/doc/01-About.md @@ -0,0 +1,6 @@ +# About the Doc Module <a id="doc-module-about"></a> + +Please read the following chapters for more insights on this module: + +* [Installation](02-Installation.md#doc-module-installation) +* [Module Documentation](03-Module-Documentation.md#module-documentation) diff --git a/modules/doc/doc/02-Installation.md b/modules/doc/doc/02-Installation.md new file mode 100644 index 0000000..6d93d42 --- /dev/null +++ b/modules/doc/doc/02-Installation.md @@ -0,0 +1,15 @@ +# Doc Module Installation <a id="doc-module-installation"></a> + +This module is provided with the Icinga Web 2 package and does +not need any extra installation step. + +## Enable the Module <a id="monitoring-module-enable"></a> + +Navigate to `Configuration` -> `Modules` -> `doc` and enable +the module. + +You can also enable the module during the setup wizard, or on the CLI: + +``` +icingacli module enable doc +``` diff --git a/modules/doc/doc/03-Module-Documentation.md b/modules/doc/doc/03-Module-Documentation.md new file mode 100644 index 0000000..5ce4a9a --- /dev/null +++ b/modules/doc/doc/03-Module-Documentation.md @@ -0,0 +1,87 @@ +# Writing Module Documentation <a id="module-documentation"></a> + +![Markdown](img/markdown.png) + +Icinga Web 2 is capable of viewing your module's documentation, if the documentation is written in +[Markdown](http://en.wikipedia.org/wiki/Markdown). Please refer to +[Markdown Syntax Documentation](http://daringfireball.net/projects/markdown/syntax) for Markdown's formatting syntax. + +## Where to Put Module Documentation? <a id="module-documentation-location"></a> + +By default, your module's Markdown documentation files must be placed in the `doc` directory beneath your module's root +directory, e.g.: + +``` +example-module/doc +``` + +## Chapters <a id="module-documentation-chapters"></a> + +Each Markdown documentation file represents a chapter of your module's documentation. The first found heading inside +each file is the chapter's title. The order of chapters is based on the case insensitive "Natural Order" of your files' +names. <dfn>Natural Order</dfn> means that the file names are ordered in the way which seems natural to humans. +It is best practice to prefix Markdown documentation file names with numbers to ensure that they appear in the correct +order, e.g.: + +``` +01-About.md +02-Installation.md +03-Configuration.md +``` + +## Table Of Contents <a id="module-documentation-toc"></a> + +The table of contents for your module's documentation is auto-generated based on all found headings inside each +Markdown documentation file. + +## Linking Between Headings <a id="module-documentation-linking"></a> + +For linking between headings, place an anchor **after the text** where you want to link to, e.g.: + +``` +# Heading <a id="heading"></a> Heading +``` + +Please note that anchors have to be unique across all your Markdown documentation files. + +Now you can reference the anchor either in the same or **in another** Markdown documentation file, e.g.: + +``` +This is a link to [Heading](#heading). +``` + +Other tools support linking between headings by giving the filename plus the anchor to link to, e.g.: + +``` +This is a link to [About/Heading](01-About.md#heading) +``` + +This syntax is also supported in Icinga Web 2. + +## Including Images <a id="module-documentation-images"></a> + +Images must placed in the `doc` directory beneath your module's root directory, e.g.: + +``` +/path/to/icingaweb2/modules/example-module/doc/img/example.png +``` + +Note that the `img` sub directory is not mandatory but good for organizing your directory structure. + +Module images can be accessed using the following URL: + +``` +{baseURL}/doc/module/{moduleName}/image/{image} e.g. icingaweb2/doc/module/example-module/image/img/example.png +``` + +Markdown's image syntax is very similar to Markdown's link syntax, but prefixed with an exclamation mark, e.g.: + +``` +![Alt text](http://path/to/img.png "Optional Title") +``` + +URLs to images inside your Markdown documentation files must be specified without the base URL, e.g.: + +``` +![Example](img/example.png) +``` diff --git a/modules/doc/doc/img/markdown.png b/modules/doc/doc/img/markdown.png Binary files differnew file mode 100644 index 0000000..93e729b --- /dev/null +++ b/modules/doc/doc/img/markdown.png diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php new file mode 100644 index 0000000..0caf3ad --- /dev/null +++ b/modules/doc/library/Doc/DocController.php @@ -0,0 +1,116 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc; + +use Icinga\Module\Doc\Renderer\DocSectionRenderer; +use Icinga\Module\Doc\Renderer\DocTocRenderer; +use Icinga\Web\Controller; +use Icinga\Web\Url; +use Icinga\Web\Widget\Tabextension\OutputFormat; + +class DocController extends Controller +{ + /** + * {@inheritdoc} + */ + protected function moduleInit() + { + // Our UrlParams object does not take parameters from custom routes into account which is why we have to set + // them explicitly + if ($this->hasParam('chapter')) { + $this->params->set('chapter', $this->getParam('chapter')); + } + if ($this->hasParam('image')) { + $this->params->set('image', $this->getParam('image')); + } + if ($this->hasParam('moduleName')) { + $this->params->set('moduleName', $this->getParam('moduleName')); + } + } + + /** + * Render a chapter + * + * @param string $path Path to the documentation + * @param string $chapter ID of the chapter + * @param string $url URL to replace links with + * @param string $imageUrl URL to images + * @param array $urlParams Additional URL parameters + */ + protected function renderChapter($path, $chapter, $url, $imageUrl = null, array $urlParams = array()) + { + $parser = new DocParser($path); + $section = new DocSectionRenderer($parser->getDocTree(), DocSectionRenderer::decodeUrlParam($chapter)); + $this->view->section = $section + ->setHighlightSearch($this->params->get('highlight-search')) + ->setImageUrl($imageUrl) + ->setUrl($url) + ->setUrlParams($urlParams); + $first = null; + foreach ($section as $first) { + break; + } + $title = $first === null ? ucfirst($chapter) : $first->getTitle(); + $this->view->title = $title; + $this->getTabs() + ->add('toc', array( + 'active' => true, + 'title' => $title, + 'url' => Url::fromRequest() + )) + ->extend(new OutputFormat(array(OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON))); + $this->render('chapter', null, true); + } + + /** + * Render a toc + * + * @param string $path Path to the documentation + * @param string $name Name of the documentation + * @param string $url URL to replace links with + * @param array $urlParams Additional URL parameters + */ + protected function renderToc($path, $name, $url, array $urlParams = array()) + { + $parser = new DocParser($path); + $toc = new DocTocRenderer($parser->getDocTree()->getIterator()); + $this->view->toc = $toc + ->setUrl($url) + ->setUrlParams($urlParams); + $name = ucfirst($name); + $title = sprintf($this->translate('%s Documentation'), $name); + $this->getTabs() + ->add('toc', array( + 'active' => true, + 'title' => $title, + 'url' => Url::fromRequest() + )) + ->extend(new OutputFormat(array(OutputFormat::TYPE_CSV, OutputFormat::TYPE_JSON))); + $this->render('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); + $toc = new DocTocRenderer($parser->getDocTree()->getIterator()); + $this->view->toc = $toc + ->setUrl($url) + ->setUrlParams($urlParams); + $section = new DocSectionRenderer($parser->getDocTree()); + $this->view->section = $section + ->setUrl($url) + ->setUrlParams($urlParams); + $this->view->title = sprintf($this->translate('%s Documentation'), $name); + $this->_request->setParam('format', 'pdf'); + $this->_helper->viewRenderer->setRender('pdf', null, true); + } +} diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php new file mode 100644 index 0000000..7ddeaa9 --- /dev/null +++ b/modules/doc/library/Doc/DocParser.php @@ -0,0 +1,235 @@ +<?php +/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc; + +use CachingIterator; +use RecursiveIteratorIterator; +use SplFileObject; +use SplStack; +use Icinga\Data\Tree\SimpleTree; +use Icinga\Exception\NotReadableError; +use Icinga\Util\DirectoryIterator; +use Icinga\Module\Doc\Exception\DocException; + +/** + * Parser for documentation written in Markdown + */ +class DocParser +{ + /** + * Internal identifier for Atx-style headers + * + * @var int + */ + const HEADER_ATX = 1; + + /** + * Internal identifier for Setext-style headers + * + * @var int + */ + const HEADER_SETEXT = 2; + + /** + * Path to the documentation + * + * @var string + */ + protected $path; + + /** + * Iterator over documentation files + * + * @var DirectoryIterator + */ + protected $docIterator; + + /** + * Create a new documentation parser for the given path + * + * @param string $path Path to the documentation + * + * @throws DocException If the documentation directory does not exist + * @throws NotReadableError If the documentation directory is not readable + */ + public function __construct($path) + { + if (! DirectoryIterator::isReadable($path)) { + throw new DocException( + mt('doc', 'Documentation directory \'%s\' is not readable'), + $path + ); + } + $this->path = $path; + $this->docIterator = new DirectoryIterator($path, 'md', DirectoryIterator::FILES_FIRST); + } + + /** + * Extract atx- or setext-style headers from the given lines + * + * @param string $line + * @param string $nextLine + * + * @return array|null An array containing the header and the header level or null if there's nothing to extract + */ + protected function extractHeader($line, $nextLine) + { + if (! $line) { + return null; + } + $header = null; + if ($line + && $line[0] === '#' + && preg_match('/^#+/', $line, $match) === 1 + ) { + // Atx + $level = strlen($match[0]); + $header = trim(substr($line, $level)); + if (! $header) { + return null; + } + $headerStyle = static::HEADER_ATX; + } elseif ($nextLine + && ($nextLine[0] === '=' || $nextLine[0] === '-') + && preg_match('/^[=-]+\s*$/', $nextLine, $match) === 1 + ) { + // Setext + $header = trim($line); + if (! $header) { + return null; + } + if ($match[0][0] === '=') { + $level = 1; + } else { + $level = 2; + } + $headerStyle = static::HEADER_SETEXT; + } + if ($header === null) { + return null; + } + if (strpos($header, '<') !== false + && preg_match('#(?:<(?P<tag>a|span) (?:id|name)="(?P<id>.+)"></(?P=tag)>)\s*#u', $header, $match) + ) { + $header = str_replace($match[0], '', $header); + $id = $match['id']; + } else { + $id = null; + } + /** @noinspection PhpUndefinedVariableInspection */ + return array($header, $id, $level, $headerStyle); + } + + /** + * Generate unique section ID + * + * @param string $id + * @param string $filename + * @param SimpleTree $tree + * + * @return string + */ + protected function uuid($id, $filename, SimpleTree $tree) + { + $id = str_replace(' ', '-', $id); + if ($tree->getNode($id) === null) { + return $id; + } + $id = $id . '-' . md5($filename); + $offset = 0; + while ($tree->getNode($id)) { + if ($offset++ === 0) { + $id .= '-' . $offset; + } else { + $id = substr($id, 0, -1) . $offset; + } + } + return $id; + } + + /** + * Get the documentation tree + * + * @return SimpleTree + */ + public function getDocTree() + { + $tree = new SimpleTree(); + foreach (new RecursiveIteratorIterator($this->docIterator) as $filename) { + $file = new SplFileObject($filename); + $file->setFlags(SplFileObject::READ_AHEAD); + $stack = new SplStack(); + $cachingIterator = new CachingIterator($file); + $insideFencedCodeBlock = false; + + for ($cachingIterator->rewind(); $cachingIterator->valid(); $cachingIterator->next()) { + $line = $cachingIterator->current(); + $header = null; + + if (substr($line, 0, 3) === '```') { + $insideFencedCodeBlock = ! $insideFencedCodeBlock; + } elseif (! $insideFencedCodeBlock) { + $fileIterator = $cachingIterator->getInnerIterator(); + $header = $this->extractHeader($line, $fileIterator->valid() ? $fileIterator->current() : null); + } + + if ($header !== null) { + list($title, $id, $level, $headerStyle) = $header; + while (! $stack->isEmpty() && $stack->top()->getLevel() >= $level) { + $stack->pop(); + } + if ($id === null) { + $path = array(); + foreach ($stack as $section) { + /** @var $section DocSection */ + $path[] = $section->getTitle(); + } + $path[] = $title; + $id = implode('-', $path); + $noFollow = true; + } else { + $noFollow = false; + } + + $id = $this->uuid($id, $filename, $tree); + + $section = new DocSection(); + $section + ->setId($id) + ->setTitle($title) + ->setLevel($level) + ->setNoFollow($noFollow); + if ($stack->isEmpty()) { + $section->setChapter($section); + $tree->addChild($section); + } else { + $section->setChapter($stack->bottom()); + $tree->addChild($section, $stack->top()); + } + $stack->push($section); + if ($headerStyle === static::HEADER_SETEXT) { + $cachingIterator->next(); + continue; + } + } else { + if ($stack->isEmpty()) { + $title = ucfirst($file->getBasename('.' . pathinfo($file->getFilename(), PATHINFO_EXTENSION))); + $id = $this->uuid($title, $filename, $tree); + $section = new DocSection(); + $section + ->setId($id) + ->setTitle($title) + ->setLevel(1) + ->setNoFollow(true); + $section->setChapter($section); + $tree->addChild($section); + $stack->push($section); + } + $stack->top()->appendContent($line); + } + } + } + return $tree; + } +} diff --git a/modules/doc/library/Doc/DocSection.php b/modules/doc/library/Doc/DocSection.php new file mode 100644 index 0000000..ce5297e --- /dev/null +++ b/modules/doc/library/Doc/DocSection.php @@ -0,0 +1,159 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc; + +use Icinga\Data\Tree\TreeNode; + +/** + * A section of a documentation + */ +class DocSection extends TreeNode +{ + /** + * Chapter the section belongs to + * + * @var DocSection + */ + protected $chapter; + + /** + * Content of the section + * + * @var array + */ + protected $content = array(); + + /** + * Header level + * + * @var int + */ + protected $level; + + /** + * Whether to instruct search engines to not index the link to the section + * + * @var bool + */ + protected $noFollow; + + /** + * Title of the section + * + * @var string + */ + protected $title; + + /** + * Set the chapter the section belongs to + * + * @param DocSection $section + * + * @return $this + */ + public function setChapter(DocSection $section) + { + $this->chapter = $section; + return $this; + } + + /** + * Get the chapter the section belongs to + * + * @return DocSection + */ + public function getChapter() + { + return $this->chapter; + } + + /** + * 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; + } + + /** + * Set the header level + * + * @param int $level Header level + * + * @return $this + */ + public function setLevel($level) + { + $this->level = (int) $level; + return $this; + } + + /** + * Get the header level + * + * @return int + */ + public function getLevel() + { + return $this->level; + } + + /** + * Set 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 + * + * @return $this + */ + public function setNoFollow($noFollow = true) + { + $this->noFollow = (bool) $noFollow; + return $this; + } + + /** + * Get whether to instruct search engines to not index the link to the section + * + * @return bool + */ + public function getNoFollow() + { + return $this->noFollow; + } + + /** + * Set the title of the section + * + * @param string $title Title of the section + * + * @return $this + */ + public function setTitle($title) + { + $this->title = (string) $title; + return $this; + } + + /** + * Get the title of the section + * + * @return string + */ + public function getTitle() + { + return $this->title; + } +} diff --git a/modules/doc/library/Doc/DocSectionFilterIterator.php b/modules/doc/library/Doc/DocSectionFilterIterator.php new file mode 100644 index 0000000..bac5a67 --- /dev/null +++ b/modules/doc/library/Doc/DocSectionFilterIterator.php @@ -0,0 +1,73 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc; + +use Countable; +use RecursiveFilterIterator; +use Icinga\Data\Tree\TreeNodeIterator; + +/** + * Recursive filter iterator over sections that are part of a particular chapter + * + * @method TreeNodeIterator getInnerIterator() { + * {@inheritdoc} + * } + */ +class DocSectionFilterIterator extends RecursiveFilterIterator implements Countable +{ + /** + * Chapter to filter for + * + * @var string + */ + protected $chapter; + + /** + * Create a new recursive filter iterator over sections that are part of a particular chapter + * + * @param TreeNodeIterator $iterator + * @param string $chapter The chapter to filter for + */ + public function __construct(TreeNodeIterator $iterator, $chapter) + { + parent::__construct($iterator); + $this->chapter = $chapter; + } + + /** + * 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(): bool + { + $section = $this->current(); + /** @var \Icinga\Module\Doc\DocSection $section */ + if ($section->getChapter()->getId() === $this->chapter) { + return true; + } + return false; + } + + public function getChildren(): self + { + return new static($this->getInnerIterator()->getChildren(), $this->chapter); + } + + public function count(): int + { + return iterator_count($this); + } + + /** + * Whether the filter swallowed every section + * + * @return bool + */ + public function isEmpty() + { + return $this->count() === 0; + } +} diff --git a/modules/doc/library/Doc/Exception/ChapterNotFoundException.php b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php new file mode 100644 index 0000000..7fa7807 --- /dev/null +++ b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php @@ -0,0 +1,11 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Exception; + +/** + * Exception thrown if a chapter was not found + */ +class ChapterNotFoundException extends DocException +{ +} diff --git a/modules/doc/library/Doc/Exception/DocException.php b/modules/doc/library/Doc/Exception/DocException.php new file mode 100644 index 0000000..1d9e871 --- /dev/null +++ b/modules/doc/library/Doc/Exception/DocException.php @@ -0,0 +1,13 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Exception; + +use Icinga\Exception\IcingaException; + +/** + * Exception thrown if an error in the documentation module's library occurs + */ +class DocException extends IcingaException +{ +} diff --git a/modules/doc/library/Doc/Renderer/DocRenderer.php b/modules/doc/library/Doc/Renderer/DocRenderer.php new file mode 100644 index 0000000..cb1bc39 --- /dev/null +++ b/modules/doc/library/Doc/Renderer/DocRenderer.php @@ -0,0 +1,208 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Renderer; + +use Exception; +use Icinga\Exception\IcingaException; +use RecursiveIteratorIterator; +use Icinga\Application\Icinga; +use Icinga\Web\View; + +/** + * Base class for toc and section renderer + */ +abstract class DocRenderer extends RecursiveIteratorIterator +{ + /** + * URL to images + * + * @var string + */ + protected $imageUrl; + + /** + * URL to replace links with + * + * @var string + */ + protected $url; + + /** + * Additional URL parameters + * + * @var array + */ + protected $urlParams = array(); + + /** + * View + * + * @var View|null + */ + protected $view; + + /** + * Get the URL to images + * + * @return string + */ + public function getImageUrl() + { + return $this->imageUrl; + } + + /** + * Set the URL to images + * + * @param string $imageUrl + * + * @return $this + */ + public function setImageUrl($imageUrl) + { + $this->imageUrl = (string) $imageUrl; + return $this; + } + /** + * Get the URL to replace links with + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the URL to replace links with + * + * @param string $url + * + * @return $this + */ + public function setUrl($url) + { + $this->url = (string) $url; + return $this; + } + + /** + * Get additional URL parameters + * + * @return array + */ + public function getUrlParams() + { + return $this->urlParams; + } + + /** + * Set additional URL parameters + * + * @param array $urlParams + * + * @return $this + */ + public function setUrlParams(array $urlParams) + { + $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams); + return $this; + } + + /** + * Get the view + * + * @return View + */ + public function getView() + { + if ($this->view === null) { + $this->view = Icinga::app()->getViewRenderer()->view; + } + return $this->view; + } + + /** + * Set the view + * + * @param View $view + * + * @return $this + */ + public function setView(View $view) + { + $this->view = $view; + return $this; + } + + /** + * 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 + * + * @return string + */ + abstract public function render(); + + /** + * Render to HTML + * + * @return string + * @see \Icinga\Module\Doc\Renderer::render() For the render method. + */ + public function __toString() + { + try { + return $this->render(); + } catch (Exception $e) { + return $e->getMessage() . ': ' . IcingaException::getConfidentialTraceAsString($e); + } + } +} diff --git a/modules/doc/library/Doc/Renderer/DocSearchRenderer.php b/modules/doc/library/Doc/Renderer/DocSearchRenderer.php new file mode 100644 index 0000000..c6e9ae2 --- /dev/null +++ b/modules/doc/library/Doc/Renderer/DocSearchRenderer.php @@ -0,0 +1,131 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Renderer; + +use RecursiveIteratorIterator; +use Icinga\Module\Doc\Search\DocSearchIterator; +use Icinga\Module\Doc\Search\DocSearchMatch; + +/** + * Renderer for doc searches + */ +class DocSearchRenderer extends DocRenderer +{ + /** + * The content to render + * + * @var array + */ + protected $content = array(); + + /** + * Create a new renderer for doc searches + * + * @param DocSearchIterator $iterator + */ + public function __construct(DocSearchIterator $iterator) + { + parent::__construct($iterator, RecursiveIteratorIterator::SELF_FIRST); + } + + public function beginIteration(): void + { + $this->content[] = '<nav role="navigation"><ul class="toc">'; + } + + public function endIteration(): void + { + $this->content[] = '</ul></nav>'; + } + + public function beginChildren(): void + { + if ($this->getInnerIterator()->getMatches()) { + $this->content[] = '<ul class="toc">'; + } + } + + public function endChildren(): void + { + if ($this->getInnerIterator()->getMatches()) { + $this->content[] = '</ul>'; + } + } + + public function render() + { + foreach ($this as $section) { + if (($matches = $this->getInnerIterator()->getMatches()) === null) { + continue; + } + $title = $this->getView()->escape($section->getTitle()); + $contentMatches = array(); + foreach ($matches as $match) { + if ($match->getMatchType() === DocSearchMatch::MATCH_HEADER) { + $title = $match->highlight(); + } else { + $contentMatches[] = sprintf( + '<p>%s</p>', + $match->highlight() + ); + } + } + $path = $this->getView()->getHelper('Url')->url( + array_merge( + $this->getUrlParams(), + array( + 'chapter' => $this->encodeUrlParam($section->getChapter()->getId()) + ) + ), + $this->url, + false, + false + ); + $url = $this->getView()->url( + $path, + array('highlight-search' => $this->getInnerIterator()->getSearch()->getInput()) + ); + /** @var \Icinga\Web\Url $url */ + $url->setAnchor($this->encodeAnchor($section->getId())); + $urlAttributes = array( + 'data-base-target' => '_next', + 'title' => $section->getId() === $section->getChapter()->getId() + ? sprintf( + $this->getView()->translate( + 'Show all matches of "%s" in the chapter "%s"', + 'search.render.section.link' + ), + $this->getInnerIterator()->getSearch()->getInput(), + $section->getChapter()->getTitle() + ) + : sprintf( + $this->getView()->translate( + 'Show all matches of "%s" in the section "%s" of the chapter "%s"', + 'search.render.section.link' + ), + $this->getInnerIterator()->getSearch()->getInput(), + $section->getTitle(), + $section->getChapter()->getTitle() + ) + ); + if ($section->getNoFollow()) { + $urlAttributes['rel'] = 'nofollow'; + } + $this->content[] = '<li>' . $this->getView()->qlink( + $title, + $url->getAbsoluteUrl(), + null, + $urlAttributes, + false + ); + if (! empty($contentMatches)) { + $this->content = array_merge($this->content, $contentMatches); + } + if (! $section->hasChildren()) { + $this->content[] = '</li>'; + } + } + return implode("\n", $this->content); + } +} diff --git a/modules/doc/library/Doc/Renderer/DocSectionRenderer.php b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php new file mode 100644 index 0000000..de2d4d4 --- /dev/null +++ b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php @@ -0,0 +1,346 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Renderer; + +require_once 'Parsedown/Parsedown.php'; + +use DOMDocument; +use DOMXPath; +use Parsedown; +use RecursiveIteratorIterator; +use Icinga\Data\Tree\SimpleTree; +use Icinga\Module\Doc\Exception\ChapterNotFoundException; +use Icinga\Module\Doc\DocSectionFilterIterator; +use Icinga\Module\Doc\Search\DocSearch; +use Icinga\Module\Doc\Search\DocSearchMatch; +use Icinga\Web\Dom\DomNodeIterator; +use Icinga\Web\Url; +use Icinga\Web\View; + +/** + * Section renderer + */ +class DocSectionRenderer extends DocRenderer +{ + /** + * Content to render + * + * @var array + */ + protected $content = array(); + + /** + * Search criteria to highlight + * + * @var string + */ + protected $highlightSearch; + + /** + * Parsedown instance + * + * @var Parsedown + */ + protected $parsedown; + + /** + * Documentation tree + * + * @var SimpleTree + */ + protected $tree; + + /** + * Create a new section renderer + * + * @param SimpleTree $tree The documentation tree + * @param string|null $chapter If not null, the chapter to filter for + * + * @throws ChapterNotFoundException If the chapter to filter for was not found + */ + public function __construct(SimpleTree $tree, $chapter = null) + { + if ($chapter !== null) { + $filter = new DocSectionFilterIterator($tree->getIterator(), $chapter); + if ($filter->isEmpty()) { + throw new ChapterNotFoundException( + mt('doc', 'Chapter %s not found'), + $chapter + ); + } + parent::__construct( + $filter, + RecursiveIteratorIterator::SELF_FIRST + ); + } else { + parent::__construct($tree->getIterator(), RecursiveIteratorIterator::SELF_FIRST); + } + $this->tree = $tree; + $this->parsedown = Parsedown::instance(); + } + + /** + * Set the search criteria to highlight + * + * @param string $highlightSearch + * + * @return $this + */ + public function setHighlightSearch($highlightSearch) + { + $this->highlightSearch = $highlightSearch; + return $this; + } + + /** + * Get the search criteria to highlight + * + * @return string + */ + public function getHighlightSearch() + { + return $this->highlightSearch; + } + + /** + * Syntax highlighting for PHP code + * + * @param array $match + * + * @return string + */ + protected function highlightPhp($match) + { + return '<pre>' . highlight_string(htmlspecialchars_decode($match[1]), true) . '</pre>'; + } + + /** + * Highlight search criteria + * + * @param string $html + * @param DocSearch $search Search criteria + * + * @return string + */ + protected function highlightSearch($html, DocSearch $search) + { + $doc = new DOMDocument(); + @$doc->loadHTML($html); + $iter = new RecursiveIteratorIterator(new DomNodeIterator($doc), RecursiveIteratorIterator::SELF_FIRST); + foreach ($iter as $node) { + if ($node->nodeType !== XML_TEXT_NODE + || ($node->parentNode->nodeType === XML_ELEMENT_NODE && $node->parentNode->tagName === 'code') + ) { + continue; + } + $text = $node->nodeValue; + if (($match = $search->search($text)) === null) { + continue; + } + $matches = $match->getMatches(); + ksort($matches); + $offset = 0; + $fragment = $doc->createDocumentFragment(); + foreach ($matches as $position => $match) { + $fragment->appendChild($doc->createTextNode(substr($text, $offset, $position - $offset))); + $fragment->appendChild($doc->createElement('span', $match)) + ->setAttribute('class', DocSearchMatch::HIGHLIGHT_CSS_CLASS); + $offset = $position + strlen($match); + } + $fragment->appendChild($doc->createTextNode(substr($text, $offset))); + $node->parentNode->replaceChild($fragment, $node); + } + // Remove <!DOCTYPE + $doc->removeChild($doc->doctype); + // Remove <html><body> and </body></html> + return substr($doc->saveHTML(), 12, -15); + } + + /** + * Markup notes + * + * @param array $match + * + * @return string + */ + protected function markupNotes($match) + { + $doc = new DOMDocument(); + $doc->loadHTML($match[0]); + $xpath = new DOMXPath($doc); + $blockquote = $xpath->query('//blockquote[1]')->item(0); + /** @var \DOMElement $blockquote */ + if (strtolower(substr(trim($blockquote->nodeValue), 0, 5)) === 'note:') { + $blockquote->setAttribute('class', 'note'); + } + return $doc->saveXML($blockquote); + } + + /** + * 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 \DOMElement $img */ + $path = $this->getView()->getHelper('Url')->url( + array_merge( + array( + 'image' => trim($img->getAttribute('src')) + ), + $this->urlParams + ), + $this->imageUrl, + false, + false + ); + $url = $this->getView()->url($path); + /** @var \Icinga\Web\Url $url */ + $img->setAttribute('src', $url->getAbsoluteUrl()); + return substr_replace($doc->saveXML($img), '', -2, 1); // Replace '/>' with '>' + } + + /** + * Replace chapter link + * + * @param array $match + * + * @return string + */ + protected function replaceChapterLink($match) + { + if (($chapter = $this->tree->getNode($this->decodeAnchor($match['chapter']))) === null) { + return $match[0]; + } + /** @var \Icinga\Module\Doc\DocSection $section */ + $path = $this->getView()->getHelper('Url')->url( + array_merge( + $this->urlParams, + array( + 'chapter' => $this->encodeUrlParam($chapter->getChapter()->getId()) + ) + ), + $this->url, + false, + false + ); + $url = $this->getView()->url($path); + /** @var \Icinga\Web\Url $url */ + return sprintf( + '<a %s%shref="%s"', + strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '', + $chapter->getNoFollow() ? 'rel="nofollow" ' : '', + $url->getAbsoluteUrl() + ); + } + + /** + * Replace section link + * + * @param array $match + * + * @return string + */ + protected function replaceSectionLink($match) + { + if (($section = $this->tree->getNode($this->decodeAnchor($match['section']))) === null) { + return $match[0]; + } + /** @var \Icinga\Module\Doc\DocSection $section */ + $path = $this->getView()->getHelper('Url')->url( + array_merge( + $this->urlParams, + array( + 'chapter' => $this->encodeUrlParam($section->getChapter()->getId()) + ) + ), + $this->url, + false, + false + ); + $url = $this->getView()->url($path); + /** @var \Icinga\Web\Url $url */ + $url->setAnchor($this->encodeAnchor($section->getId())); + return sprintf( + '<a %s%shref="%s"', + strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '', + $section->getNoFollow() ? 'rel="nofollow" ' : '', + $url->getAbsoluteUrl() + ); + } + + /** + * {@inheritdoc} + */ + public function render() + { + $search = null; + if (($highlightSearch = $this->getHighlightSearch()) !== null) { + $search = new DocSearch($highlightSearch); + } + foreach ($this as $section) { + $title = $section->getTitle(); + if ($search !== null && ($match = $search->search($title)) !== null) { + $title = $match->highlight(); + } else { + $title = $this->getView()->escape($title); + } + $number = ''; + for ($i = 0; $i < $this->getDepth() + 1; ++$i) { + if ($i > 0) { + $number .= '.'; + } + $number .= $this->getSubIterator($i)->key() + 1; + } + $this->content[] = sprintf( + '<a name="%1$s"></a><h%2$d>%3$s. %4$s</h%2$d>', + static::encodeAnchor($section->getId()), + $section->getLevel(), + $number, + $title + ); + $html = $this->parsedown->text(implode('', $section->getContent())); + if (empty($html)) { + continue; + } + $html = preg_replace_callback( + '#<pre><code class="language-php">(.*?)</code></pre>#s', + array($this, 'highlightPhp'), + $html + ); + $html = preg_replace_callback( + '/<img[^>]+>/', + array($this, 'replaceImg'), + $html + ); + $html = preg_replace_callback( + '#<blockquote>.+?</blockquote>#ms', + array($this, 'markupNotes'), + $html + ); + $html = preg_replace_callback( + '/<a\s+(?P<attribs>[^>]*?\s+)?href="(?:(?!http:\/\/)[^"#]*)#(?P<section>[^"]+)"/', + array($this, 'replaceSectionLink'), + $html + ); + $html = preg_replace_callback( + '/<a\s+(?P<attribs>[^>]*?\s+)?href="(?:\d+-)?(?P<chapter>[^\/"#]+).md"/', + array($this, 'replaceChapterLink'), + $html + ); + if ($search !== null) { + $html = $this->highlightSearch($html, $search); + } + $this->content[] = $html; + } + return implode("\n", $this->content); + } +} diff --git a/modules/doc/library/Doc/Renderer/DocTocRenderer.php b/modules/doc/library/Doc/Renderer/DocTocRenderer.php new file mode 100644 index 0000000..09e9a1d --- /dev/null +++ b/modules/doc/library/Doc/Renderer/DocTocRenderer.php @@ -0,0 +1,117 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Renderer; + +use Icinga\Data\Tree\TreeNodeIterator; +use RecursiveIteratorIterator; + +/** + * TOC renderer + */ +class DocTocRenderer extends DocRenderer +{ + /** + * CSS class for the HTML list element + * + * @var string + */ + const CSS_CLASS = 'toc'; + + /** + * Tag for the HTML list element + * + * @var string + */ + const HTML_LIST_TAG = 'ol'; + + /** + * Content to render + * + * @var array + */ + protected $content = array(); + + /** + * Create a new toc renderer + * + * @param TreeNodeIterator $iterator + */ + public function __construct(TreeNodeIterator $iterator) + { + parent::__construct($iterator, RecursiveIteratorIterator::SELF_FIRST); + } + + public function beginIteration(): void + { + $this->content[] = sprintf('<nav role="navigation"><%s class="%s">', static::HTML_LIST_TAG, static::CSS_CLASS); + } + + public function endIteration(): void + { + $this->content[] = sprintf('</%s></nav>', static::HTML_LIST_TAG); + } + + public function beginChildren(): void + { + $this->content[] = sprintf('<%s class="%s">', static::HTML_LIST_TAG, static::CSS_CLASS); + } + + public function endChildren(): void + { + $this->content[] = sprintf('</%s>', static::HTML_LIST_TAG); + } + + public function render() + { + if ($this->getInnerIterator()->isEmpty()) { + return '<p>' . mt('doc', 'Documentation is empty.') . '</p>'; + } + $view = $this->getView(); + $zendUrlHelper = $view->getHelper('Url'); + foreach ($this as $section) { + $path = $zendUrlHelper->url( + array_merge( + $this->urlParams, + array( + 'chapter' => $this->encodeUrlParam($section->getChapter()->getId()) + ) + ), + $this->url, + false, + false + ); + $url = $view->url($path); + /** @var \Icinga\Web\Url $url */ + if ($this->getDepth() > 0) { + $url->setAnchor($this->encodeAnchor($section->getId())); + } + $urlAttributes = array( + 'data-base-target' => '_next', + 'title' => $section->getId() === $section->getChapter()->getId() + ? sprintf( + $view->translate('Show the chapter "%s"', 'toc.render.section.link'), + $section->getChapter()->getTitle() + ) + : sprintf( + $view->translate('Show the section "%s" of the chapter "%s"', 'toc.render.section.link'), + $section->getTitle(), + $section->getChapter()->getTitle() + ) + ); + if ($section->getNoFollow()) { + $urlAttributes['rel'] = 'nofollow'; + } + $this->content[] = '<li>' . $this->getView()->qlink( + $section->getTitle(), + $url->getAbsoluteUrl(), + null, + $urlAttributes + ); + if (! $section->hasChildren()) { + $this->content[] = '</li>'; + } + } + return implode("\n", $this->content); + } +} diff --git a/modules/doc/library/Doc/Search/DocSearch.php b/modules/doc/library/Doc/Search/DocSearch.php new file mode 100644 index 0000000..20493e4 --- /dev/null +++ b/modules/doc/library/Doc/Search/DocSearch.php @@ -0,0 +1,95 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Search; + +/** + * Search documentation for a given search string + */ +class DocSearch +{ + /** + * Search string + * + * @var string + */ + protected $input; + + /** + * Search criteria + * + * @var array + */ + protected $search; + + /** + * Create a new doc search from the given search string + * + * @param string $search + */ + public function __construct($search) + { + $this->input = $search = (string) $search; + $criteria = array(); + if (preg_match_all('/"(?P<search>[^"]*)"/', $search, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { + $unquoted = array(); + $offset = 0; + foreach ($matches as $match) { + $fullMatch = $match[0]; + $searchMatch = $match['search']; + $unquoted[] = substr($search, $offset, $fullMatch[1] - $offset); + $offset = $fullMatch[1] + strlen($fullMatch[0]); + if (strlen($searchMatch[0]) > 0) { + $criteria[] = $searchMatch[0]; + } + } + $unquoted[] = substr($search, $offset); + $search = implode(' ', $unquoted); + } + $this->search = array_map( + 'strtolower', + array_unique(array_merge($criteria, array_filter(explode(' ', trim($search))))) + ); + } + + /** + * Get the search criteria + * + * @return array + */ + public function getCriteria() + { + return $this->search; + } + + /** + * Get the search string + * + * @return string + */ + public function getInput() + { + return $this->input; + } + + /** + * Search in the given line + * + * @param string $line + * + * @return DocSearchMatch|null + */ + public function search($line) + { + $match = new DocSearchMatch(); + $match->setLine($line); + foreach ($this->search as $criteria) { + $offset = 0; + while (($position = stripos($line, $criteria, $offset)) !== false) { + $match->appendMatch(substr($line, $position, strlen($criteria)), $position); + $offset = $position + 1; + } + } + return $match->isEmpty() ? null : $match; + } +} diff --git a/modules/doc/library/Doc/Search/DocSearchIterator.php b/modules/doc/library/Doc/Search/DocSearchIterator.php new file mode 100644 index 0000000..fd2c903 --- /dev/null +++ b/modules/doc/library/Doc/Search/DocSearchIterator.php @@ -0,0 +1,113 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Search; + +use RecursiveFilterIterator; +use RecursiveIteratorIterator; +use Icinga\Data\Tree\TreeNodeIterator; + +/** + * Iterator over doc sections that match a given search criteria + */ +class DocSearchIterator extends RecursiveFilterIterator +{ + /** + * Search criteria + * + * @var DocSearch + */ + protected $search; + + /** + * Current search matches + * + * @var DocSearchMatch[]|null + */ + protected $matches; + + /** + * Create a new iterator over doc sections that match the given search criteria + * + * @param TreeNodeIterator $iterator + * @param DocSearch $search + */ + public function __construct(TreeNodeIterator $iterator, DocSearch $search) + { + $this->search = $search; + parent::__construct($iterator); + } + + /** + * Accept sections that match the search + * + * @return bool Whether the current element of the iterator is acceptable + * through this filter + */ + public function accept(): bool + { + $section = $this->current(); + /** @var $section \Icinga\Module\Doc\DocSection */ + $matches = array(); + if (($match = $this->search->search($section->getTitle())) !== null) { + $matches[] = $match->setMatchType(DocSearchMatch::MATCH_HEADER); + } + foreach ($section->getContent() as $lineno => $line) { + if (($match = $this->search->search($line)) !== null) { + $matches[] = $match + ->setMatchType(DocSearchMatch::MATCH_CONTENT) + ->setLineno($lineno); + } + } + if (! empty($matches)) { + $this->matches = $matches; + return true; + } + if ($section->hasChildren()) { + $this->matches = null; + return true; + } + return false; + } + + /** + * Get the search criteria + * + * @return DocSearch + */ + public function getSearch() + { + return $this->search; + } + + public function getChildren(): self + { + return new static($this->getInnerIterator()->getChildren(), $this->search); + } + + /** + * Whether the search did not yield any match + * + * @return bool + */ + public function isEmpty() + { + $iter = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST); + foreach ($iter as $section) { + if ($iter->getInnerIterator()->getMatches() !== null) { + return false; + } + } + return true; + } + + /** + * Get current matches + * + * @return DocSearchMatch[]|null + */ + public function getMatches() + { + return $this->matches; + } +} diff --git a/modules/doc/library/Doc/Search/DocSearchMatch.php b/modules/doc/library/Doc/Search/DocSearchMatch.php new file mode 100644 index 0000000..0f21748 --- /dev/null +++ b/modules/doc/library/Doc/Search/DocSearchMatch.php @@ -0,0 +1,215 @@ +<?php +/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +namespace Icinga\Module\Doc\Search; + +use UnexpectedValueException; +use Icinga\Application\Icinga; +use Icinga\Web\View; + +/** + * A doc search match + */ +class DocSearchMatch +{ + /** + * CSS class for highlighting matches + * + * @var string + */ + const HIGHLIGHT_CSS_CLASS = 'search-highlight'; + + /** + * Header match + * + * @var int + */ + const MATCH_HEADER = 1; + + /** + * Content match + * + * @var int + */ + const MATCH_CONTENT = 2; + + /** + * Line + * + * @var string + */ + protected $line; + + /** + * Line number + * + * @var int + */ + protected $lineno; + + /** + * Type of the match + * + * @var int + */ + protected $matchType; + + /** + * Matches + * + * @var array + */ + protected $matches = array(); + + /** + * View + * + * @var View|null + */ + protected $view; + + /** + * Set the line + * + * @param string $line + * + * @return $this + */ + public function setLine($line) + { + $this->line = (string) $line; + return $this; + } + + /** + * Get the line + * + * @return string + */ + public function getLine() + { + return $this->line; + } + + /** + * Set the line number + * + * @param int $lineno + * + * @return $this + */ + public function setLineno($lineno) + { + $this->lineno = (int) $lineno; + return $this; + } + + /** + * Set the match type + * + * @param int $matchType + * + * @return $this + */ + public function setMatchType($matchType) + { + $matchType = (int) $matchType; + if ($matchType !== static::MATCH_HEADER && $matchType !== static::MATCH_CONTENT) { + throw new UnexpectedValueException(); + } + $this->matchType = $matchType; + return $this; + } + + /** + * Get the match type + * + * @return int + */ + public function getMatchType() + { + return $this->matchType; + } + + /** + * Append a match + * + * @param string $match + * @param int $position + * + * @return $this + */ + public function appendMatch($match, $position) + { + $this->matches[(int) $position] = (string) $match; + return $this; + } + + /** + * Get the matches + * + * @return array + */ + public function getMatches() + { + return $this->matches; + } + + /** + * Set the view + * + * @param View $view + * + * @return $this + */ + public function setView(View $view) + { + $this->view = $view; + return $this; + } + + /** + * Get the view + * + * @return View + */ + public function getView() + { + if ($this->view === null) { + $this->view = Icinga::app()->getViewRenderer()->view; + } + return $this->view; + } + + /** + * Get the line having matches highlighted + * + * @return string + */ + public function highlight() + { + $highlighted = ''; + $offset = 0; + $matches = $this->getMatches(); + ksort($matches); + foreach ($matches as $position => $match) { + $highlighted .= $this->getView()->escape(substr($this->line, $offset, $position - $offset)) + . '<span class="' . static::HIGHLIGHT_CSS_CLASS .'">' + . $this->getView()->escape($match) + . '</span>'; + $offset = $position + strlen($match); + } + $highlighted .= $this->getView()->escape(substr($this->line, $offset)); + return $highlighted; + } + + /** + * Whether the match is empty + * + * @return bool + */ + public function isEmpty() + { + return empty($this->matches); + } +} diff --git a/modules/doc/module.info b/modules/doc/module.info new file mode 100644 index 0000000..ac596db --- /dev/null +++ b/modules/doc/module.info @@ -0,0 +1,4 @@ +Module: doc +Version: 2.11.4 +Description: Documentation module + Extracts, shows and exports documentation for Icinga Web 2 and its modules. diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less new file mode 100644 index 0000000..007c5c5 --- /dev/null +++ b/modules/doc/public/css/module.less @@ -0,0 +1,109 @@ +/*! Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +// Mixins + +.gradient(@a: @gray-lighter; @b: @gray-lightest) { + background: @a; + background: -webkit-gradient(linear, left top, left bottom, from(@a), to(@b)); + background: -webkit-linear-gradient(top, @a, @b); + background: -moz-linear-gradient(top, @a, @b); + background: -ms-linear-gradient(top, @a, @b); + background: -o-linear-gradient(top, @a, @b); + background: linear-gradient(to bottom, @a, @b); +} + +// General styles + +code { + color: @icinga-blue; + font-family: @font-family-fixed; +} + +pre > code { + color: inherit; +} + +.chapter a { + border-bottom: 1px dotted @gray-light; + font-weight: @font-weight-bold; + + &:hover { + border-bottom: 1px solid @text-color; + text-decoration: none; + } +} + +.content { + font-size: 1.167em; +} + +.search-highlight { + .rounded-corners(); + + background: @icinga-blue; + color: @text-color-on-icinga-blue; + padding: 0 0.3em 0 0.3em; +} + +.toc { + counter-reset: li; + list-style-type: none; + margin: 0; + padding: 0; + + li { + counter-increment: li; + margin-top: 0.25em; + + > .toc { + margin-left: 2em; + } + + a { + &:before { + color: @icinga-blue; + content: counters(li,".") " "; + display: inline-block; + font-size: small; + font-weight: @font-weight-bold; + min-width: 1.5em; + padding: 0.25em; + text-align: center; + } + + display: block; + } + } +} + +// Table styles + +table { + margin-bottom: 1em; + width: 100%; +} + +tbody > tr:nth-child(odd) { + .gradient() +} + +tbody > tr:nth-child(even) { + background: @body-bg-color; +} + +td, th { + padding: 0.5em; +} + +td { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +th { + border-bottom: 2px solid @icinga-blue; + font-weight: @font-weight-bold; + text-align: left; + text-transform: uppercase; +} diff --git a/modules/doc/public/js/module.js b/modules/doc/public/js/module.js new file mode 100644 index 0000000..d5571ee --- /dev/null +++ b/modules/doc/public/js/module.js @@ -0,0 +1,30 @@ +/*! Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */ + +(function(Icinga) { + + var Doc = function(module) { + this.module = module; + this.initialize(); + this.module.icinga.logger.debug('Doc module loaded'); + }; + + Doc.prototype = { + + initialize: function() + { + this.module.on('rendered', this.rendered); + this.module.icinga.logger.debug('Doc module initialized'); + }, + + rendered: function(event) { + var $container = $(event.currentTarget); + if ($('> .content.styleguide', $container).length) { + $container.removeClass('module-doc'); + } + } + }; + + Icinga.availableModules.doc = Doc; + +}(Icinga)); + diff --git a/modules/doc/run.php b/modules/doc/run.php new file mode 100644 index 0000000..df9dd09 --- /dev/null +++ b/modules/doc/run.php @@ -0,0 +1,64 @@ +<?php +/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ + +use Icinga\Application\Icinga; + +if (Icinga::app()->isCli()) { + return; +} + +$docModuleChapter = new Zend_Controller_Router_Route( + 'doc/module/:moduleName/chapter/:chapter', + array( + 'controller' => 'module', + 'action' => 'chapter', + 'module' => 'doc' + ) +); + +$docIcingaWebChapter = new Zend_Controller_Router_Route( + 'doc/icingaweb/chapter/:chapter', + array( + 'controller' => 'icingaweb', + 'action' => 'chapter', + 'module' => 'doc' + ) +); + +$docModuleToc = new Zend_Controller_Router_Route( + 'doc/module/:moduleName/toc', + array( + 'controller' => 'module', + 'action' => 'toc', + 'module' => 'doc' + ) +); + +$docModulePdf = new Zend_Controller_Router_Route( + 'doc/module/:moduleName/pdf', + array( + 'controller' => 'module', + 'action' => 'pdf', + 'module' => 'doc' + ) +); + +$docModuleImg = new Zend_Controller_Router_Route_Regex( + 'doc/module/([^/]+)/image/(.+)', + array( + 'controller' => 'module', + 'action' => 'image', + 'module' => 'doc' + ), + array( + 'moduleName' => 1, + 'image' => 2 + ), + 'doc/module/%s/image/%s' +); + +$this->addRoute('doc/module/chapter', $docModuleChapter); +$this->addRoute('doc/icingaweb/chapter', $docIcingaWebChapter); +$this->addRoute('doc/module/toc', $docModuleToc); +$this->addRoute('doc/module/pdf', $docModulePdf); +$this->addRoute('doc/module/img', $docModuleImg); |