From 8ca6cc32b2c789a3149861159ad258f2cb9491e3 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 14:39:39 +0200 Subject: Adding upstream version 2.11.4. Signed-off-by: Daniel Baumann --- modules/doc/library/Doc/Renderer/DocRenderer.php | 208 +++++++++++++ .../doc/library/Doc/Renderer/DocSearchRenderer.php | 131 ++++++++ .../library/Doc/Renderer/DocSectionRenderer.php | 346 +++++++++++++++++++++ .../doc/library/Doc/Renderer/DocTocRenderer.php | 117 +++++++ 4 files changed, 802 insertions(+) create mode 100644 modules/doc/library/Doc/Renderer/DocRenderer.php create mode 100644 modules/doc/library/Doc/Renderer/DocSearchRenderer.php create mode 100644 modules/doc/library/Doc/Renderer/DocSectionRenderer.php create mode 100644 modules/doc/library/Doc/Renderer/DocTocRenderer.php (limited to 'modules/doc/library/Doc/Renderer') 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 @@ +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 @@ +content[] = ''; + } + + public function beginChildren(): void + { + if ($this->getInnerIterator()->getMatches()) { + $this->content[] = ''; + } + } + + 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( + '

%s

', + $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[] = '
  • ' . $this->getView()->qlink( + $title, + $url->getAbsoluteUrl(), + null, + $urlAttributes, + false + ); + if (! empty($contentMatches)) { + $this->content = array_merge($this->content, $contentMatches); + } + if (! $section->hasChildren()) { + $this->content[] = '
  • '; + } + } + 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 @@ +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 '
    ' . highlight_string(htmlspecialchars_decode($match[1]), true) . '
    '; + } + + /** + * 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 removeChild($doc->doctype); + // Remove and + 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( + '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( + '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( + '%3$s. %4$s', + static::encodeAnchor($section->getId()), + $section->getLevel(), + $number, + $title + ); + $html = $this->parsedown->text(implode('', $section->getContent())); + if (empty($html)) { + continue; + } + $html = preg_replace_callback( + '#
    (.*?)
    #s', + array($this, 'highlightPhp'), + $html + ); + $html = preg_replace_callback( + '/]+>/', + array($this, 'replaceImg'), + $html + ); + $html = preg_replace_callback( + '#
    .+?
    #ms', + array($this, 'markupNotes'), + $html + ); + $html = preg_replace_callback( + '/[^>]*?\s+)?href="(?:(?!http:\/\/)[^"#]*)#(?P
    [^"]+)"/', + array($this, 'replaceSectionLink'), + $html + ); + $html = preg_replace_callback( + '/[^>]*?\s+)?href="(?:\d+-)?(?P[^\/"#]+).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 @@ +content[] = sprintf('', 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('', static::HTML_LIST_TAG); + } + + public function render() + { + if ($this->getInnerIterator()->isEmpty()) { + return '

    ' . mt('doc', 'Documentation is empty.') . '

    '; + } + $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[] = '
  • ' . $this->getView()->qlink( + $section->getTitle(), + $url->getAbsoluteUrl(), + null, + $urlAttributes + ); + if (! $section->hasChildren()) { + $this->content[] = '
  • '; + } + } + return implode("\n", $this->content); + } +} -- cgit v1.2.3