Modules/Doc: Set rel="nofollow" on header links missing a user-specified id
refs #4820
This commit is contained in:
parent
f96974fc79
commit
1caacbf20f
|
@ -12,7 +12,7 @@
|
|||
?>
|
||||
<li <?php if (!empty($itemClass)): ?>class="<?= $itemClass ?>"<?php endif ?>>
|
||||
<?php if($item->getUrl()): ?>
|
||||
<a href="<?= $this->href($item->getUrl()); ?>">
|
||||
<a href="<?= $this->href($item->getUrl()); ?>" <?php foreach($item->getAttribs() as $attrib => $value): ?> <?= $attrib ?>="<?= $value ?>"<?php endforeach?>>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
if ($icon = $item->getIcon()) {
|
||||
|
|
|
@ -61,6 +61,8 @@ class MenuItem
|
|||
*/
|
||||
private $children = array();
|
||||
|
||||
private $attribs = array();
|
||||
|
||||
|
||||
/**
|
||||
* Create a new MenuItem
|
||||
|
@ -308,6 +310,28 @@ class MenuItem
|
|||
throw new ProgrammingError(sprintf('Trying to get invalid Menu child "%s"', $id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set HTML a tag attributes
|
||||
*
|
||||
* @param array $attribs
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setAttribs(array $attribs)
|
||||
{
|
||||
$this->attribs = $attribs;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTML a tag attributes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAttribs()
|
||||
{
|
||||
return $this->attribs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare children based on priority and title
|
||||
|
|
|
@ -358,11 +358,14 @@ class Url
|
|||
/**
|
||||
* Set the url anchor-part
|
||||
*
|
||||
* @param $anchor The site's anchor string without the '#'
|
||||
* @param $anchor The site's anchor string without the '#'
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setAnchor($anchor)
|
||||
{
|
||||
$this->anchor = '#' . $anchor;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
use Icinga\Application\Icinga;
|
||||
use Icinga\Module\Doc\Controller as DocController;
|
||||
|
||||
class Doc_IndexController extends DocController
|
||||
|
@ -13,7 +12,7 @@ class Doc_IndexController extends DocController
|
|||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$this->populateViewFromDocDirectory(Icinga::app()->getApplicationDir('/../doc'));
|
||||
$this->populateView();
|
||||
}
|
||||
}
|
||||
// @codingStandardsIgnoreEnd
|
||||
|
|
|
@ -21,9 +21,7 @@ class Doc_ModuleController extends DocController
|
|||
*/
|
||||
public function viewAction()
|
||||
{
|
||||
$this->populateViewFromDocDirectory(
|
||||
Icinga::app()->getModuleManager()->getModuleDir($this->getParam('name'), '/doc')
|
||||
);
|
||||
$this->populateView($this->getParam('name'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?php if ($html === null): ?>
|
||||
<p>Module is not documented.</p>
|
||||
<p>No documentation available.</p>
|
||||
<?php else: ?>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-xs-12 col-md-2 col-lg-2">
|
||||
|
|
|
@ -4,26 +4,20 @@
|
|||
|
||||
namespace Icinga\Module\Doc;
|
||||
|
||||
use Icinga\Module\Doc\DocParser;
|
||||
use Icinga\Web\Controller\ActionController;
|
||||
use Icinga\Web\Menu;
|
||||
|
||||
class Controller extends ActionController
|
||||
{
|
||||
/**
|
||||
* Set HTML and toc
|
||||
*
|
||||
* @param string $dir
|
||||
* @param string $module
|
||||
*/
|
||||
protected function populateViewFromDocDirectory($dir)
|
||||
protected function populateView($module = null)
|
||||
{
|
||||
if (!@is_dir($dir)) {
|
||||
$this->view->html = null;
|
||||
} else {
|
||||
$parser = new DocParser();
|
||||
list($html, $toc) = $parser->parseDirectory($dir);
|
||||
$this->view->html = $html;
|
||||
$this->view->toc = $toc;
|
||||
}
|
||||
$parser = new DocParser($module);
|
||||
list($html, $toc) = $parser->getDocumentation();
|
||||
$this->view->html = $html;
|
||||
$this->view->toc = $toc;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
// {{{ICINGA_LICENSE_HEADER}}}
|
||||
|
||||
namespace Icinga\Module\Doc;
|
||||
|
||||
use \Exception;
|
||||
|
||||
class DocException extends Exception
|
||||
{
|
||||
}
|
|
@ -6,110 +6,202 @@ namespace Icinga\Module\Doc;
|
|||
|
||||
require_once 'vendor/Parsedown/Parsedown.php';
|
||||
|
||||
use \Exception;
|
||||
use \SplStack;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Retrieve table of contents and HTML converted from all Markdown files in the given directory sorted by filename
|
||||
* Create a new documentation parser for the given module or the application
|
||||
*
|
||||
* @param $dir
|
||||
* @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 table of contents and HTML converted from markdown files sorted by filename
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception
|
||||
* @throws DocException
|
||||
*/
|
||||
public function parseDirectory($dir)
|
||||
public function getDocumentation()
|
||||
{
|
||||
$iter = new RecursiveIteratorIterator(
|
||||
new MarkdownFileIterator(
|
||||
new RecursiveDirectoryIterator($dir)
|
||||
new RecursiveDirectoryIterator($this->dir)
|
||||
)
|
||||
);
|
||||
$fileInfos = iterator_to_array($iter);
|
||||
natcasesort($fileInfos);
|
||||
$cat = array();
|
||||
$toc = new Menu('doc');
|
||||
$stack = new SplStack();
|
||||
$toc = array((object) array(
|
||||
'level' => 0,
|
||||
'item' => new Menu('doc')
|
||||
));
|
||||
foreach ($fileInfos as $fileInfo) {
|
||||
try {
|
||||
$fileObject = $fileInfo->openFile();
|
||||
} catch (RuntimeException $e) {
|
||||
throw new Exception($e->getMessage());
|
||||
throw new DocException($e->getMessage());
|
||||
}
|
||||
if ($fileObject->flock(LOCK_SH) === false) {
|
||||
throw new Exception('Couldn\'t get the lock');
|
||||
throw new DocException('Couldn\'t get the lock');
|
||||
}
|
||||
$itemPriority = 1;
|
||||
$line = null;
|
||||
while (!$fileObject->eof()) {
|
||||
$line = $fileObject->fgets();
|
||||
if ($line &&
|
||||
$line[0] === '#' &&
|
||||
preg_match('/^#+/', $line, $match) === 1
|
||||
) {
|
||||
// Atx-style
|
||||
$level = strlen($match[0]);
|
||||
$heading = str_replace('.', '.', trim(strip_tags(substr($line, $level))));
|
||||
$fragment = urlencode($heading);
|
||||
$line = '<span id="' . $fragment . '">' . "\n" . $line;
|
||||
$stack->rewind();
|
||||
while ($stack->valid() && $stack->current()->level >= $level) {
|
||||
$stack->pop();
|
||||
$stack->next();
|
||||
// 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';
|
||||
}
|
||||
$parent = $stack->current();
|
||||
if ($parent === null) {
|
||||
$item = $toc->addChild($heading, array('url' => '#' . $fragment));
|
||||
} else {
|
||||
$item = $parent->item->addChild($heading, array('url' => '#' . $fragment));
|
||||
}
|
||||
$stack->push((object) array(
|
||||
'level' => $level,
|
||||
'item' => $item
|
||||
));
|
||||
} elseif (
|
||||
$line &&
|
||||
($line[0] === '=' || $line[0] === '-') &&
|
||||
preg_match('/^[=-]+\s*$/', $line, $match) === 1
|
||||
) {
|
||||
// Setext
|
||||
if ($match[0][0] === '=') {
|
||||
// H1
|
||||
$level = 1;
|
||||
} else {
|
||||
// H
|
||||
$level = 2;
|
||||
}
|
||||
$heading = trim(strip_tags(end($cat)));
|
||||
$fragment = urlencode($heading);
|
||||
$line = '<span id="' . $fragment . '">' . "\n" . $line;
|
||||
$stack->rewind();
|
||||
while ($stack->valid() && $stack->current()->level >= $level) {
|
||||
$stack->pop();
|
||||
$stack->next();
|
||||
}
|
||||
$parent = $stack->current();
|
||||
if ($parent === null) {
|
||||
$item = $toc->addChild($heading, array('url' => '#' . $fragment));
|
||||
} else {
|
||||
$item = $parent->item->addChild($heading, array('url' => '#' . $fragment));
|
||||
}
|
||||
$stack->push((object) array(
|
||||
$id = str_replace('.', '.', strip_tags($id));
|
||||
$item = end($toc)->item->addChild(
|
||||
$id,
|
||||
array(
|
||||
'url' => Url::fromPath(
|
||||
'doc/module/view',
|
||||
array(
|
||||
'name' => $this->module
|
||||
)
|
||||
)->setAnchor(urlencode($id))->getRelativeUrl(),
|
||||
'title' => htmlspecialchars($header),
|
||||
'priority' => $itemPriority++,
|
||||
'attribs' => $attribs
|
||||
)
|
||||
);
|
||||
$toc[] = ((object) array(
|
||||
'level' => $level,
|
||||
'item' => $item
|
||||
));
|
||||
$line = '<span id="' . $id . '"></span>' . PHP_EOL . $line;
|
||||
}
|
||||
$cat[] = $line;
|
||||
}
|
||||
$fileObject->flock(LOCK_UN);
|
||||
}
|
||||
$html = Parsedown::instance()->parse(implode('', $cat));
|
||||
return array($html, $toc);
|
||||
return array($html, $toc[0]->item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# The instance.ini configuration file
|
||||
# <a id="instances"></a> The instance.ini configuration file
|
||||
|
||||
## Abstract
|
||||
|
||||
|
|
Loading…
Reference in New Issue