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