2013-10-16 14:45:23 +02:00
|
|
|
<?php
|
2015-02-04 10:46:36 +01:00
|
|
|
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
|
2013-10-16 14:45:23 +02:00
|
|
|
|
|
|
|
namespace Icinga\Module\Doc;
|
|
|
|
|
2015-02-11 15:51:31 +01:00
|
|
|
use LogicException;
|
2015-02-10 17:04:27 +01:00
|
|
|
use SplStack;
|
|
|
|
use Icinga\Data\Tree\SimpleTree;
|
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
|
|
|
|
*
|
2015-03-12 13:39:17 +01:00
|
|
|
* @var string
|
2014-05-27 14:31:17 +02:00
|
|
|
*/
|
|
|
|
protected $path;
|
2014-02-11 15:27:42 +01:00
|
|
|
|
2014-06-30 15:24:40 +02:00
|
|
|
/**
|
|
|
|
* Iterator over documentation files
|
|
|
|
*
|
2015-03-12 13:39:17 +01:00
|
|
|
* @var DocIterator
|
2014-06-30 15:24:40 +02:00
|
|
|
*/
|
|
|
|
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-12-09 14:24:11 +01: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(
|
2014-08-19 16:22:22 +02:00
|
|
|
sprintf(mt('doc', 'Documentation directory \'%s\' does not exist'), $path)
|
2014-07-28 19:09:04 +02:00
|
|
|
);
|
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(
|
2014-08-19 16:22:22 +02:00
|
|
|
sprintf(mt('doc', 'Documentation directory \'%s\' is not readable'), $path)
|
2014-07-28 19:09:04 +02:00
|
|
|
);
|
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(
|
2014-08-19 16:22:22 +02:00
|
|
|
sprintf(
|
|
|
|
mt(
|
|
|
|
'doc',
|
|
|
|
'Documentation directory \'%s\' does not contain any non-empty Markdown file (\'.md\' suffix)'
|
|
|
|
),
|
|
|
|
$path
|
2014-08-19 13:20:46 +02:00
|
|
|
)
|
2014-07-28 19:09:04 +02:00
|
|
|
);
|
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
|
|
|
*
|
2015-02-10 17:04:27 +01:00
|
|
|
* @return SimpleTree
|
2014-02-11 15:27:42 +01:00
|
|
|
*/
|
2014-07-28 19:09:04 +02:00
|
|
|
public function getDocTree()
|
|
|
|
{
|
2015-02-10 17:04:27 +01:00
|
|
|
$tree = new SimpleTree();
|
|
|
|
$stack = new SplStack();
|
2014-07-28 19:09:04 +02:00
|
|
|
foreach ($this->docIterator as $fileInfo) {
|
2015-03-12 13:39:17 +01:00
|
|
|
/** @var $fileInfo \SplFileInfo */
|
2014-07-28 19:09:04 +02:00
|
|
|
$file = $fileInfo->openFile();
|
|
|
|
$lastLine = null;
|
|
|
|
foreach ($file as $line) {
|
|
|
|
$header = $this->extractHeader($line, $lastLine);
|
|
|
|
if ($header !== null) {
|
2014-07-29 11:12:06 +02:00
|
|
|
list($title, $id, $level) = $header;
|
2014-07-28 19:09:04 +02:00
|
|
|
while (! $stack->isEmpty() && $stack->top()->getLevel() >= $level) {
|
|
|
|
$stack->pop();
|
|
|
|
}
|
|
|
|
if ($id === null) {
|
|
|
|
$path = array();
|
|
|
|
foreach ($stack as $section) {
|
2015-03-12 13:39:17 +01:00
|
|
|
/** @var $section DocSection */
|
2014-07-28 19:09:04 +02:00
|
|
|
$path[] = $section->getTitle();
|
|
|
|
}
|
2014-07-29 11:12:06 +02:00
|
|
|
$path[] = $title;
|
2014-07-28 19:09:04 +02:00
|
|
|
$id = implode('-', $path);
|
2014-08-19 09:57:22 +02:00
|
|
|
$noFollow = true;
|
2014-07-28 19:09:04 +02:00
|
|
|
} else {
|
2014-08-19 09:57:22 +02:00
|
|
|
$noFollow = false;
|
2014-07-28 19:09:04 +02:00
|
|
|
}
|
2015-02-10 17:04:27 +01:00
|
|
|
if ($tree->getNode($id) !== null) {
|
|
|
|
$id = uniqid($id);
|
|
|
|
}
|
|
|
|
$section = new DocSection();
|
|
|
|
$section
|
|
|
|
->setId($id)
|
|
|
|
->setTitle($title)
|
|
|
|
->setLevel($level)
|
|
|
|
->setNoFollow($noFollow);
|
2014-07-28 19:09:04 +02:00
|
|
|
if ($stack->isEmpty()) {
|
2015-02-10 17:04:27 +01:00
|
|
|
$section->setChapter($section);
|
|
|
|
$tree->addChild($section);
|
2014-07-28 19:09:04 +02:00
|
|
|
} else {
|
2015-02-10 17:04:27 +01:00
|
|
|
$section->setChapter($stack->bottom());
|
2014-07-28 19:09:04 +02:00
|
|
|
$tree->addChild($section, $stack->top());
|
|
|
|
}
|
|
|
|
$stack->push($section);
|
|
|
|
} else {
|
2015-02-11 15:51:31 +01:00
|
|
|
if ($stack->isEmpty()) {
|
|
|
|
throw new LogicException('Heading required');
|
|
|
|
}
|
2014-07-28 19:09:04 +02:00
|
|
|
$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
|
|
|
}
|
|
|
|
}
|