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-06-05 02:10:49 +02:00
|
|
|
require_once 'IcingaVendor/Parsedown/Parsedown.php';
|
2014-05-23 09:36:14 +02:00
|
|
|
|
2014-03-21 20:05:00 +01:00
|
|
|
use Parsedown;
|
2014-06-06 14:10:13 +02:00
|
|
|
use Icinga\Data\Tree\Node;
|
2014-05-27 14:31:17 +02:00
|
|
|
use Icinga\Exception\NotReadableError;
|
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
|
|
|
|
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-02-11 15:27:42 +01:00
|
|
|
* @throws DocException
|
2014-05-27 14:31:17 +02:00
|
|
|
* @throws NotReadableError
|
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-06-06 14:10:13 +02:00
|
|
|
throw new DocException('Doc directory `' . $path . '\' does not exist');
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
2014-05-27 14:31:17 +02:00
|
|
|
if (! is_readable($path)) {
|
2014-06-06 14:10:13 +02:00
|
|
|
throw new NotReadableError('Doc directory `' . $path . '\' is not readable');
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
2014-05-27 14:31:17 +02:00
|
|
|
$this->path = $path;
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
|
|
|
|
2014-06-03 15:23:59 +02:00
|
|
|
/**
|
|
|
|
* Retrieve the table of contents
|
|
|
|
*
|
2014-06-06 14:10:13 +02:00
|
|
|
* @return Node
|
2014-06-03 15:23:59 +02:00
|
|
|
*/
|
|
|
|
public function getToc()
|
|
|
|
{
|
|
|
|
$tocStack = array((object) array(
|
|
|
|
'level' => 0,
|
2014-06-06 14:10:13 +02:00
|
|
|
'node' => new Node()
|
2014-06-03 15:23:59 +02:00
|
|
|
));
|
|
|
|
foreach (new DocIterator($this->path) as $fileObject) {
|
|
|
|
$line = null;
|
2014-06-06 14:10:13 +02:00
|
|
|
$currentChapterName = null;
|
2014-06-03 15:23:59 +02:00
|
|
|
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)));
|
2014-06-06 14:10:13 +02:00
|
|
|
if ($currentChapterName === null) {
|
|
|
|
// The first header is the chapter's name
|
|
|
|
$currentChapterName = $id;
|
|
|
|
$id = null;
|
|
|
|
}
|
2014-06-03 15:23:59 +02:00
|
|
|
$node = end($tocStack)->node->appendChild(
|
|
|
|
(object) array(
|
2014-06-06 14:10:13 +02:00
|
|
|
'id' => $id,
|
|
|
|
'title' => $header,
|
|
|
|
'nofollow' => $nofollow,
|
|
|
|
'chapterName' => $currentChapterName
|
2014-06-03 15:23:59 +02:00
|
|
|
)
|
|
|
|
);
|
|
|
|
$tocStack[] = (object) array(
|
|
|
|
'level' => $level,
|
|
|
|
'node' => $node
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-06-06 14:10:13 +02:00
|
|
|
return $tocStack[0]->node;
|
2014-06-03 15:23:59 +02:00
|
|
|
}
|
|
|
|
|
2014-02-11 15:27:42 +01:00
|
|
|
/**
|
2014-06-06 14:10:13 +02:00
|
|
|
* Retrieve a chapter
|
2013-10-16 14:45:23 +02:00
|
|
|
*
|
2014-06-06 14:10:13 +02:00
|
|
|
* @param string $chapterName
|
|
|
|
*
|
|
|
|
* @return string
|
2013-10-16 14:45:23 +02:00
|
|
|
*/
|
2014-06-06 14:10:13 +02:00
|
|
|
public function getChapter($chapterName)
|
2013-10-16 14:45:23 +02:00
|
|
|
{
|
2014-05-28 17:18:07 +02:00
|
|
|
$cat = array();
|
|
|
|
$tocStack = array((object) array(
|
2014-02-11 15:27:42 +01:00
|
|
|
'level' => 0,
|
2014-06-06 14:10:13 +02:00
|
|
|
'node' => new Node()
|
2014-02-11 15:27:42 +01:00
|
|
|
));
|
2014-06-06 14:10:13 +02:00
|
|
|
$chapterFound = false;
|
2014-06-03 15:23:59 +02:00
|
|
|
foreach (new DocIterator($this->path) as $fileObject) {
|
2014-02-11 16:35:36 +01:00
|
|
|
$line = null;
|
2014-06-06 14:10:13 +02:00
|
|
|
$currentChapterName = null;
|
|
|
|
$chapter = array();
|
2014-05-23 14:16:58 +02:00
|
|
|
while (! $fileObject->eof()) {
|
2014-02-11 15:27:42 +01:00
|
|
|
// Save last line for setext-style headers
|
2014-06-06 14:10:13 +02:00
|
|
|
$lastLine = $line;
|
|
|
|
$line = $fileObject->fgets();
|
|
|
|
$header = $this->extractHeader($line, $lastLine);
|
2014-02-11 15:27:42 +01:00
|
|
|
if ($header !== null) {
|
2014-06-03 14:56:44 +02:00
|
|
|
list($header, $level) = $header;
|
2014-06-06 14:10:13 +02:00
|
|
|
$id = $this->extractHeaderId($header);
|
2014-05-28 17:18:07 +02:00
|
|
|
$this->reduceToc($tocStack, $level);
|
2014-02-11 15:27:42 +01:00
|
|
|
if ($id === null) {
|
|
|
|
$path = array();
|
2014-05-28 17:18:07 +02:00
|
|
|
foreach (array_slice($tocStack, 1) as $entity) {
|
|
|
|
$path[] = $entity->node->getValue()->title;
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
2014-06-06 14:10:13 +02:00
|
|
|
$path[] = $header;
|
|
|
|
$id = implode('-', $path);
|
|
|
|
}
|
|
|
|
$id = urlencode(str_replace('.', '.', strip_tags($id)));
|
|
|
|
if ($currentChapterName === null) {
|
|
|
|
$currentChapterName = $id;
|
|
|
|
$id = null;
|
2014-02-05 12:35:44 +01:00
|
|
|
}
|
2014-06-06 14:10:13 +02:00
|
|
|
$node = end($tocStack)->node->appendChild(
|
2014-05-28 17:18:07 +02:00
|
|
|
(object) array(
|
2014-06-06 14:10:13 +02:00
|
|
|
'title' => $header
|
2014-02-11 15:27:42 +01:00
|
|
|
)
|
|
|
|
);
|
2014-06-06 14:10:13 +02:00
|
|
|
$tocStack[] = (object) array(
|
2014-02-05 12:35:44 +01:00
|
|
|
'level' => $level,
|
2014-05-28 17:18:07 +02:00
|
|
|
'node' => $node
|
|
|
|
);
|
2014-03-21 20:05:00 +01:00
|
|
|
$line = '<a name="' . $id . '"></a>' . PHP_EOL . $line;
|
2013-10-16 14:45:23 +02:00
|
|
|
}
|
2014-06-06 14:10:13 +02:00
|
|
|
$chapter[] = $line;
|
|
|
|
}
|
|
|
|
if ($currentChapterName === $chapterName) {
|
|
|
|
$chapterFound = true;
|
|
|
|
$cat = $chapter;
|
|
|
|
}
|
|
|
|
if (! $chapterFound) {
|
|
|
|
$cat = array_merge($cat, $chapter);
|
2013-10-16 14:45:23 +02:00
|
|
|
}
|
|
|
|
}
|
2014-02-11 17:04:58 +01:00
|
|
|
$html = preg_replace_callback(
|
|
|
|
'#<pre><code class="language-php">(.*?)\</code></pre>#s',
|
|
|
|
array($this, 'highlight'),
|
2014-06-06 14:10:13 +02:00
|
|
|
Parsedown::instance()->text(implode('', $cat))
|
2014-02-11 17:04:58 +01:00
|
|
|
);
|
2014-06-06 14:10:13 +02:00
|
|
|
return $html;
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
|
|
|
|
2014-02-11 17:04:58 +01:00
|
|
|
/**
|
|
|
|
* Syntax highlighting for PHP code
|
|
|
|
*
|
|
|
|
* @param $match
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function highlight($match)
|
|
|
|
{
|
|
|
|
return highlight_string(htmlspecialchars_decode($match[1]), true);
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
if ($line &&
|
|
|
|
$line[0] === '#' &&
|
|
|
|
preg_match('/^#+/', $line, $match) === 1
|
|
|
|
) {
|
|
|
|
// Atx-style
|
|
|
|
$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 (
|
|
|
|
$line &&
|
|
|
|
($line[0] === '=' || $line[0] === '-') &&
|
|
|
|
preg_match('/^[=-]+\s*$/', $line, $match) === 1
|
|
|
|
) {
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
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('#(?:<(?P<tag>a|span) id="(?P<id>.+)"></(?P=tag)>)#u', $header, $match)
|
|
|
|
) {
|
|
|
|
$header = str_replace($match[0], '', $header);
|
|
|
|
return $match['id'];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2014-05-28 17:18:07 +02:00
|
|
|
* Reduce the toc stack to the given level
|
2014-02-11 15:27:42 +01:00
|
|
|
*
|
2014-05-28 17:18:07 +02:00
|
|
|
* @param array &$tocStack
|
2014-02-11 15:27:42 +01:00
|
|
|
* @param int $level
|
|
|
|
*/
|
2014-05-28 17:18:07 +02:00
|
|
|
protected function reduceToc(array &$tocStack, $level) {
|
|
|
|
while (end($tocStack)->level >= $level) {
|
|
|
|
array_pop($tocStack);
|
2014-02-11 15:27:42 +01:00
|
|
|
}
|
2013-10-16 14:45:23 +02:00
|
|
|
}
|
|
|
|
}
|