2013-10-16 14:45:23 +02:00
|
|
|
<?php
|
|
|
|
// {{{ICINGA_LICENSE_HEADER}}}
|
2014-06-06 14:10:13 +02:00
|
|
|
// {{{ICINGA_LICENSE_HEADER}}}
|
2013-10-16 14:45:23 +02:00
|
|
|
|
|
|
|
namespace Icinga\Module\Doc;
|
|
|
|
|
2014-07-28 19:09:04 +02:00
|
|
|
use SplDoublyLinkedList;
|
2014-05-27 14:31:17 +02:00
|
|
|
use Icinga\Exception\NotReadableError;
|
2014-06-30 15:24:40 +02:00
|
|
|
use Icinga\Module\Doc\Exception\DocEmptyException;
|
|
|
|
use Icinga\Module\Doc\Exception\DocException;
|
2013-10-16 14:45:23 +02:00
|
|
|
|
|
|
|
/**
|
2014-01-24 16:41:37 +01:00
|
|
|
* Parser for documentation written in Markdown
|
2013-10-16 14:45:23 +02:00
|
|
|
*/
|
2014-02-03 15:39:53 +01:00
|
|
|
class DocParser
|
2013-10-16 14:45:23 +02:00
|
|
|
{
|
2014-05-27 14:31:17 +02:00
|
|
|
/**
|
|
|
|
* Path to the documentation
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $path;
|
2014-02-11 15:27:42 +01:00
|
|
|
|
2014-06-30 15:24:40 +02:00
|
|
|
/**
|
|
|
|
* Iterator over documentation files
|
|
|
|
*
|
|
|
|
* @var DocIterator
|
|
|
|
*/
|
|
|
|
protected $docIterator;
|
|
|
|
|
2013-10-16 14:45:23 +02:00
|
|
|
/**
|
2014-05-27 14:31:17 +02:00
|
|
|
* Create a new documentation parser for the given path
|
2014-02-11 15:27:42 +01:00
|
|
|
*
|
2014-05-27 14:31:17 +02:00
|
|
|
* @param string $path Path to the documentation
|
2013-10-16 14:45:23 +02:00
|
|
|
*
|
2014-06-30 15:24:40 +02:00
|
|
|
* @throws DocException If the documentation directory does not exist
|
|
|
|
* @throws NotReadableError If the documentation directory is not readable
|
|
|
|
* @throws DocEmptyException If the documentation directory is empty
|
2014-02-11 15:27:42 +01:00
|
|
|
*/
|
2014-05-27 14:31:17 +02:00
|
|
|
public function __construct($path)
|
2014-02-11 15:27:42 +01:00
|
|
|
{
|
2014-05-27 14:31:17 +02:00
|
|
|
if (! is_dir($path)) {
|
2014-07-28 19:09:04 +02:00
|
|
|
throw new DocException(
|
|
|
|
mt('doc', 'Documentation directory') . ' \'' . $path . '\' ' . mt('doc', 'does not exist')
|
|
|
|
);
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
2014-05-27 14:31:17 +02:00
|
|
|
if (! is_readable($path)) {
|
2014-07-28 19:09:04 +02:00
|
|
|
throw new DocException(
|
|
|
|
mt('doc', 'Documentation directory') . ' \'' . $path . '\' ' . mt('doc', 'is not readable')
|
|
|
|
);
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
2014-06-30 15:24:40 +02:00
|
|
|
$docIterator = new DocIterator($path);
|
|
|
|
if ($docIterator->count() === 0) {
|
2014-07-28 19:09:04 +02:00
|
|
|
throw new DocEmptyException(
|
|
|
|
mt('doc', 'Documentation directory') . ' \'' . $path . '\' '
|
|
|
|
. mt('doc', 'does not contain any non-empty Markdown file (\'.md\' suffix')
|
|
|
|
);
|
2014-06-30 15:24:40 +02:00
|
|
|
}
|
2014-05-27 14:31:17 +02:00
|
|
|
$this->path = $path;
|
2014-06-30 15:24:40 +02:00
|
|
|
$this->docIterator = $docIterator;
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract atx- or setext-style headers from the given lines
|
|
|
|
*
|
|
|
|
* @param string $line
|
|
|
|
* @param string $lastLine
|
|
|
|
*
|
|
|
|
* @return array|null An array containing the header and the header level or null if there's nothing to extract
|
|
|
|
*/
|
|
|
|
protected function extractHeader($line, $lastLine)
|
|
|
|
{
|
2014-05-23 14:16:58 +02:00
|
|
|
if (! $line) {
|
2014-02-11 15:27:42 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$header = null;
|
2014-06-13 17:23:20 +02:00
|
|
|
if ($line
|
|
|
|
&& $line[0] === '#'
|
|
|
|
&& preg_match('/^#+/', $line, $match) === 1
|
2014-02-11 15:27:42 +01:00
|
|
|
) {
|
2014-07-28 19:09:04 +02:00
|
|
|
// Atx
|
2014-02-11 15:27:42 +01:00
|
|
|
$level = strlen($match[0]);
|
|
|
|
$header = trim(substr($line, $level));
|
2014-05-23 14:16:58 +02:00
|
|
|
if (! $header) {
|
2014-02-11 15:27:42 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
} elseif (
|
2014-06-13 17:23:20 +02:00
|
|
|
$line
|
|
|
|
&& ($line[0] === '=' || $line[0] === '-')
|
|
|
|
&& preg_match('/^[=-]+\s*$/', $line, $match) === 1
|
2014-02-11 15:27:42 +01:00
|
|
|
) {
|
|
|
|
// Setext
|
|
|
|
$header = trim($lastLine);
|
2014-05-23 14:16:58 +02:00
|
|
|
if (! $header) {
|
2014-02-11 15:27:42 +01:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if ($match[0][0] === '=') {
|
|
|
|
$level = 1;
|
|
|
|
} else {
|
|
|
|
$level = 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($header === null) {
|
|
|
|
return null;
|
|
|
|
}
|
2014-06-13 17:23:20 +02:00
|
|
|
if ($header[0] === '<'
|
2014-07-28 19:09:04 +02:00
|
|
|
&& preg_match('#(?:<(?P<tag>a|span) (?:id|name)="(?P<id>.+)"></(?P=tag)>)\s*#u', $header, $match)
|
2014-02-11 15:27:42 +01:00
|
|
|
) {
|
|
|
|
$header = str_replace($match[0], '', $header);
|
2014-07-28 19:09:04 +02:00
|
|
|
$id = $match['id'];
|
|
|
|
} else {
|
|
|
|
$id = null;
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
2014-07-28 19:09:04 +02:00
|
|
|
return array($header, $id, $level);
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2014-07-28 19:09:04 +02:00
|
|
|
* Get the documentation tree
|
2014-02-11 15:27:42 +01:00
|
|
|
*
|
2014-07-28 19:09:04 +02:00
|
|
|
* @return DocTree
|
2014-02-11 15:27:42 +01:00
|
|
|
*/
|
2014-07-28 19:09:04 +02:00
|
|
|
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()) {
|
2014-07-29 11:10:49 +02:00
|
|
|
$chapterTitle = $header;
|
|
|
|
$section = new Section($id, $header, $level, $nofollow, $chapterTitle);
|
2014-07-28 19:09:04 +02:00
|
|
|
$tree->addRoot($section);
|
|
|
|
} else {
|
2014-07-29 11:10:49 +02:00
|
|
|
$chapterTitle = $stack->bottom()->getTitle();
|
|
|
|
$section = new Section($id, $header, $level, $nofollow, $chapterTitle);
|
2014-07-28 19:09:04 +02:00
|
|
|
$tree->addChild($section, $stack->top());
|
|
|
|
}
|
|
|
|
$stack->push($section);
|
|
|
|
} else {
|
|
|
|
$stack->top()->appendContent($line);
|
|
|
|
}
|
|
|
|
// Save last line for setext-style headers
|
|
|
|
$lastLine = $line;
|
|
|
|
}
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
2014-07-28 19:09:04 +02:00
|
|
|
return $tree;
|
2013-10-16 14:45:23 +02:00
|
|
|
}
|
|
|
|
}
|