summaryrefslogtreecommitdiffstats
path: root/modules/doc/library/Doc/Renderer
diff options
context:
space:
mode:
Diffstat (limited to 'modules/doc/library/Doc/Renderer')
-rw-r--r--modules/doc/library/Doc/Renderer/DocRenderer.php208
-rw-r--r--modules/doc/library/Doc/Renderer/DocSearchRenderer.php131
-rw-r--r--modules/doc/library/Doc/Renderer/DocSectionRenderer.php345
-rw-r--r--modules/doc/library/Doc/Renderer/DocTocRenderer.php117
4 files changed, 801 insertions, 0 deletions
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..c61dfac
--- /dev/null
+++ b/modules/doc/library/Doc/Renderer/DocSectionRenderer.php
@@ -0,0 +1,345 @@
+<?php
+/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Module\Doc\Renderer;
+
+use DOMDocument;
+use DOMXPath;
+use Icinga\Module\Doc\DocSection;
+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 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 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 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 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 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);
+ }
+}