doc/DocParser: Replace `getDoc()' and `getToc()' with `getDocTree()'

refs #4820
This commit is contained in:
Eric Lippmann 2014-07-28 19:09:04 +02:00
parent e26d360561
commit 134db3fc66
1 changed files with 66 additions and 163 deletions

View File

@ -4,12 +4,8 @@
namespace Icinga\Module\Doc; namespace Icinga\Module\Doc;
require_once 'IcingaVendor/Parsedown/Parsedown.php'; use SplDoublyLinkedList;
use Parsedown;
use Icinga\Data\Tree\Node;
use Icinga\Exception\NotReadableError; use Icinga\Exception\NotReadableError;
use Icinga\Module\Doc\Exception\ChapterNotFoundException;
use Icinga\Module\Doc\Exception\DocEmptyException; use Icinga\Module\Doc\Exception\DocEmptyException;
use Icinga\Module\Doc\Exception\DocException; use Icinga\Module\Doc\Exception\DocException;
@ -44,152 +40,26 @@ class DocParser
public function __construct($path) public function __construct($path)
{ {
if (! is_dir($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)) { 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); $docIterator = new DocIterator($path);
if ($docIterator->count() === 0) { 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->path = $path;
$this->docIterator = $docIterator; $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 = '<a name="' . $id . '"></a>' . PHP_EOL . $line;
}
$chapter[] = $line;
}
if ($currentChapterName === $chapterName) {
return preg_replace_callback(
'#<pre><code class="language-php">(.*?)\</code></pre>#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 * Extract atx- or setext-style headers from the given lines
* *
@ -208,7 +78,7 @@ class DocParser
&& $line[0] === '#' && $line[0] === '#'
&& preg_match('/^#+/', $line, $match) === 1 && preg_match('/^#+/', $line, $match) === 1
) { ) {
// Atx-style // Atx
$level = strlen($match[0]); $level = strlen($match[0]);
$header = trim(substr($line, $level)); $header = trim(substr($line, $level));
if (! $header) { if (! $header) {
@ -233,36 +103,69 @@ class DocParser
if ($header === null) { if ($header === null) {
return 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] === '<' if ($header[0] === '<'
&& preg_match('#(?:<(?P<tag>a|span) id="(?P<id>.+)"></(?P=tag)>)#u', $header, $match) && preg_match('#(?:<(?P<tag>a|span) (?:id|name)="(?P<id>.+)"></(?P=tag)>)\s*#u', $header, $match)
) { ) {
$header = str_replace($match[0], '', $header); $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 * @return DocTree
* @param int $level
*/ */
protected function reduceToc(array &$tocStack, $level) { public function getDocTree()
while (end($tocStack)->level >= $level) { {
array_pop($tocStack); $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;
} }
} }