mirror of
				https://github.com/Icinga/icingaweb2.git
				synced 2025-11-04 05:05:01 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			225 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			225 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?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\Application\Icinga;
 | 
						|
use Icinga\Web\Menu;
 | 
						|
use Icinga\Web\Url;
 | 
						|
 | 
						|
/**
 | 
						|
 * Parser for documentation written in Markdown
 | 
						|
 */
 | 
						|
class DocParser
 | 
						|
{
 | 
						|
    protected $dir;
 | 
						|
 | 
						|
    protected $module;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Create a new documentation parser for the given module or the application
 | 
						|
     *
 | 
						|
     * @param   string $module
 | 
						|
     *
 | 
						|
     * @throws  DocException
 | 
						|
     */
 | 
						|
    public function __construct($module = null)
 | 
						|
    {
 | 
						|
        if ($module === null) {
 | 
						|
            $dir = Icinga::app()->getApplicationDir('/../doc');
 | 
						|
        } else {
 | 
						|
            $mm = Icinga::app()->getModuleManager();
 | 
						|
            if (!$mm->hasInstalled($module)) {
 | 
						|
                throw new DocException('Module is not installed');
 | 
						|
            }
 | 
						|
            if (!$mm->hasEnabled($module)) {
 | 
						|
                throw new DocException('Module is not enabled');
 | 
						|
            }
 | 
						|
            $dir = $mm->getModuleDir($module, '/doc');
 | 
						|
        }
 | 
						|
        if (!is_dir($dir)) {
 | 
						|
            throw new DocException('Doc directory does not exist');
 | 
						|
        }
 | 
						|
        $this->dir      = $dir;
 | 
						|
        $this->module   = $module;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Retrieve doc as HTML converted from markdown files sorted by filename and the table of contents
 | 
						|
     *
 | 
						|
     * @return  array
 | 
						|
     * @throws  DocException
 | 
						|
     */
 | 
						|
    public function getDocAndToc()
 | 
						|
    {
 | 
						|
        $iter = new RecursiveIteratorIterator(
 | 
						|
            new MarkdownFileIterator(
 | 
						|
                new RecursiveDirectoryIterator($this->dir)
 | 
						|
            )
 | 
						|
        );
 | 
						|
        $fileInfos = iterator_to_array($iter);
 | 
						|
        natcasesort($fileInfos);
 | 
						|
        $cat    = array();
 | 
						|
        $toc    = array((object) array(
 | 
						|
            'level' => 0,
 | 
						|
            'item'  => new Menu('doc')
 | 
						|
        ));
 | 
						|
        $itemPriority = 1;
 | 
						|
        foreach ($fileInfos as $fileInfo) {
 | 
						|
            try {
 | 
						|
                $fileObject = $fileInfo->openFile();
 | 
						|
            } catch (RuntimeException $e) {
 | 
						|
                throw new DocException($e->getMessage());
 | 
						|
            }
 | 
						|
            if ($fileObject->flock(LOCK_SH) === false) {
 | 
						|
                throw new DocException('Couldn\'t get the lock');
 | 
						|
            }
 | 
						|
            $line = 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);
 | 
						|
                    $attribs                = array();
 | 
						|
                    $this->reduceToc($toc, $level);
 | 
						|
                    if ($id === null) {
 | 
						|
                        $path = array();
 | 
						|
                        foreach (array_slice($toc, 1) as $entry) {
 | 
						|
                            $path[] = $entry->item->getTitle();
 | 
						|
                        }
 | 
						|
                        $path[]         = $header;
 | 
						|
                        $id             = implode('-', $path);
 | 
						|
                        $attribs['rel'] = 'nofollow';
 | 
						|
                    }
 | 
						|
                    $id     = urlencode(str_replace('.', '.', strip_tags($id)));
 | 
						|
                    $item   = end($toc)->item->addChild(
 | 
						|
                        $id,
 | 
						|
                        array(
 | 
						|
                            'url' => Url::fromPath(
 | 
						|
                                'doc/module/view',
 | 
						|
                                array(
 | 
						|
                                    'name' => $this->module
 | 
						|
                                )
 | 
						|
                            )->setAnchor($id)->getRelativeUrl(),
 | 
						|
                            'title'     => htmlspecialchars($header),
 | 
						|
                            'priority'  => $itemPriority++,
 | 
						|
                            'attribs'   => $attribs
 | 
						|
                        )
 | 
						|
                    );
 | 
						|
                    $toc[]  = ((object) array(
 | 
						|
                        'level' => $level,
 | 
						|
                        'item'  => $item
 | 
						|
                    ));
 | 
						|
                    $line = '<a name="' . $id . '"></a>' . PHP_EOL . $line;
 | 
						|
                }
 | 
						|
                $cat[] = $line;
 | 
						|
            }
 | 
						|
            $fileObject->flock(LOCK_UN);
 | 
						|
        }
 | 
						|
        $html = Parsedown::instance()->parse(implode('', $cat));
 | 
						|
        $html = preg_replace_callback(
 | 
						|
            '#<pre><code class="language-php">(.*?)\</code></pre>#s',
 | 
						|
            array($this, 'highlight'),
 | 
						|
            $html
 | 
						|
        );
 | 
						|
        return array($html, $toc[0]->item);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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 to the given level
 | 
						|
     *
 | 
						|
     * @param array &$toc
 | 
						|
     * @param int   $level
 | 
						|
     */
 | 
						|
    protected function reduceToc(array &$toc, $level) {
 | 
						|
        while (end($toc)->level >= $level) {
 | 
						|
            array_pop($toc);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |