icingaweb2/modules/doc/library/Doc/DocParser.php

214 lines
6.3 KiB
PHP
Raw Normal View History

2013-10-16 14:45:23 +02:00
<?php
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}
namespace Icinga\Module\Doc;
require_once 'vendor/Parsedown/Parsedown.php';
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use Parsedown;
use Icinga\Exception\NotReadableError;
2013-10-16 14:45:23 +02:00
/**
* Parser for documentation written in Markdown
2013-10-16 14:45:23 +02:00
*/
class DocParser
2013-10-16 14:45:23 +02:00
{
/**
* Path to the documentation
*
* @var string
*/
protected $path;
2013-10-16 14:45:23 +02:00
/**
* Create a new documentation parser for the given path
*
* @param string $path Path to the documentation
2013-10-16 14:45:23 +02:00
*
* @throws DocException
* @throws NotReadableError
*/
public function __construct($path)
{
if (! is_dir($path)) {
throw new DocException('Doc directory `' . $path .'\' does not exist');
}
if (! is_readable($path)) {
throw new NotReadableError('Doc directory `' . $path .'\' is not readable');
}
$this->path = $path;
}
/**
* Retrieve doc as HTML converted from markdown files sorted by filename and the table of contents
2013-10-16 14:45:23 +02:00
*
* @return array
* @throws DocException
2013-10-16 14:45:23 +02:00
*/
public function getDocAndToc()
2013-10-16 14:45:23 +02:00
{
$iter = new RecursiveIteratorIterator(
new MarkdownFileIterator(
new RecursiveDirectoryIterator($this->path)
2013-10-16 14:45:23 +02:00
)
);
$fileInfos = iterator_to_array($iter);
natcasesort($fileInfos);
$cat = array();
$tocStack = array((object) array(
'level' => 0,
'node' => new DocToc()
));
2013-10-16 14:45:23 +02:00
foreach ($fileInfos as $fileInfo) {
try {
$fileObject = $fileInfo->openFile();
} catch (RuntimeException $e) {
throw new DocException($e->getMessage());
2013-10-16 14:45:23 +02:00
}
if ($fileObject->flock(LOCK_SH) === false) {
throw new DocException('Couldn\'t get the lock');
2013-10-16 14:45:23 +02:00
}
2014-02-11 16:35:36 +01:00
$line = null;
$sectionTitle = 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;
if ($sectionTitle === null) {
// The first header is the section's title
$sectionTitle = $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;
}
2014-02-11 16:35:36 +01:00
$id = urlencode(str_replace('.', '&#46;', strip_tags($id)));
$node = end($tocStack)->node->appendChild(
(object) array(
'id' => $id,
'title' => $header,
'nofollow' => $nofollow
)
);
$tocStack[] = (object) array(
'level' => $level,
'node' => $node
);
$line = '<a name="' . $id . '"></a>' . PHP_EOL . $line;
2013-10-16 14:45:23 +02:00
}
$cat[] = $line;
}
$fileObject->flock(LOCK_UN);
}
$html = Parsedown::instance()->text(implode('', $cat));
$html = preg_replace_callback(
'#<pre><code class="language-php">(.*?)\</code></pre>#s',
array($this, 'highlight'),
$html
);
return array($html, new DocTocHtmlRenderer($tocStack[0]->node));
}
/**
* 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
*
* @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)
{
if (! $line) {
return null;
}
$header = null;
if ($line &&
$line[0] === '#' &&
preg_match('/^#+/', $line, $match) === 1
) {
// Atx-style
$level = strlen($match[0]);
$header = trim(substr($line, $level));
if (! $header) {
return null;
}
} elseif (
$line &&
($line[0] === '=' || $line[0] === '-') &&
preg_match('/^[=-]+\s*$/', $line, $match) === 1
) {
// Setext
$header = trim($lastLine);
if (! $header) {
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;
}
/**
* Reduce the toc stack to the given level
*
* @param array &$tocStack
* @param int $level
*/
protected function reduceToc(array &$tocStack, $level) {
while (end($tocStack)->level >= $level) {
array_pop($tocStack);
}
2013-10-16 14:45:23 +02:00
}
}