*/
class syntax_plugin_simplenavi extends SyntaxPlugin
{
private $startpages = [];
/** @inheritdoc */
public function getType()
{
return 'substition';
}
/** @inheritdoc */
public function getPType()
{
return 'block';
}
/** @inheritdoc */
public function getSort()
{
return 155;
}
/** @inheritdoc */
public function connectTo($mode)
{
$this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi');
}
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
return explode(' ', substr($match, 13, -2));
}
/** @inheritdoc */
public function render($format, Doku_Renderer $renderer, $data)
{
if ($format != 'xhtml') return false;
global $INFO;
$renderer->nocache();
// first data is namespace, rest is options
$ns = array_shift($data);
if ($ns && $ns[0] === '.') {
// resolve relative to current page
$ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx"));
} else {
$ns = cleanID($ns);
}
$items = $this->getSortedItems(
$ns,
$INFO['id'],
$this->getConf('usetitle'),
$this->getConf('natsort'),
$this->getConf('nsfirst'),
in_array('home', $data)
);
$class = 'plugin__simplenavi';
if (in_array('filter', $data)) $class .= ' plugin__simplenavi_filter';
$renderer->doc .= '
';
$renderer->doc .= html_buildlist($items, 'idx', [$this, 'cbList'], [$this, 'cbListItem']);
$renderer->doc .= '
';
return true;
}
/**
* Fetch the items to display
*
* This returns a flat list suitable for html_buildlist()
*
* @param string $ns the namespace to search in
* @param string $current the current page, the tree will be expanded to this
* @param bool $useTitle Sort by the title instead of the ID?
* @param bool $useNatSort Use natural sorting or just sort by ASCII?
* @param bool $nsFirst Sort namespaces before pages?
* @param bool $home Add namespace's start page as top level item?
* @return array
*/
public function getSortedItems($ns, $current, $useTitle, $useNatSort, $nsFirst, $home)
{
global $conf;
// convert to path
$nspath = utf8_encodeFN(str_replace(':', '/', $ns));
// get the start page of the main namespace, this adds it to the list of seen pages in $this->startpages
// and will skip it by default in the search callback
$startPage = $this->getMainStartPage($ns, $useTitle);
$items = [];
if ($home) {
// when home is requested, add the start page as top level item
$items[$startPage['id']] = $startPage;
$minlevel = 0;
} else {
$minlevel = 1;
}
// execute search using our own callback
search(
$items,
$conf['datadir'],
[$this, 'cbSearch'],
[
'currentID' => $current,
'usetitle' => $useTitle,
],
$nspath,
1,
'' // no sorting, we do ourselves
);
if (!$items) return [];
// split into separate levels
$parents = [];
$levels = [];
$curLevel = $minlevel;
foreach ($items as $idx => $item) {
if ($curLevel < $item['level']) {
// previous item was the parent
$parents[] = array_key_last($levels[$curLevel]);
}
$curLevel = $item['level'];
$levels[$item['level']][$idx] = $item;
}
// sort each level separately
foreach ($levels as $level => $items) {
uasort($items, function ($a, $b) use ($useNatSort, $nsFirst) {
return $this->itemComparator($a, $b, $useNatSort, $nsFirst);
});
$levels[$level] = $items;
}
// merge levels into a flat list again
$levels = array_reverse($levels, true);
foreach (array_keys($levels) as $level) {
if ($level == $minlevel) break;
$parent = array_pop($parents);
$pos = array_search($parent, array_keys($levels[$level - 1])) + 1;
/** @noinspection PhpArrayAccessCanBeReplacedWithForeachValueInspection */
$levels[$level - 1] = array_slice($levels[$level - 1], 0, $pos, true) +
$levels[$level] +
array_slice($levels[$level - 1], $pos, null, true);
}
return $levels[$minlevel];
}
/**
* Compare two items
*
* @param array $a
* @param array $b
* @param bool $useNatSort
* @param bool $nsFirst
* @return int
*/
public function itemComparator($a, $b, $useNatSort, $nsFirst)
{
if ($nsFirst && $a['type'] != $b['type']) {
return $a['type'] == 'd' ? -1 : 1;
}
if ($useNatSort) {
return Sort::strcmp($a['title'], $b['title']);
} else {
return strcmp($a['title'], $b['title']);
}
}
/**
* Create a list openening
*
* @param array $item
* @return string
* @see html_buildlist()
*/
public function cbList($item)
{
global $INFO;
if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) {
return '' . html_wikilink(':' . $item['id'], $item['title']) . '';
} else {
return html_wikilink(':' . $item['id'], $item['title']);
}
}
/**
* Create a list item
*
* @param array $item
* @return string
* @see html_buildlist()
*/
public function cbListItem($item)
{
if ($item['type'] == "f") {
return '';
} elseif ($item['open']) {
return '';
} else {
return '';
}
}
/**
* Custom search callback
*
* @param $data
* @param $base
* @param $file
* @param $type
* @param $lvl
* @param array $opts - currentID is the currently shown page
* @return bool
*/
public function cbSearch(&$data, $base, $file, $type, $lvl, $opts)
{
global $conf;
$return = true;
$id = pathID($file);
if (
$type == 'd' &&
(
!preg_match('#^' . $id . '(:|$)#', $opts['currentID']) &&
!preg_match('#^' . $id . '(:|$)#', getNS($opts['currentID']))
)
) {
//add but don't recurse
$return = false;
} elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) {
//don't add
return false;
}
// for sneaky index, check access to the namespace's start page
if ($type == 'd' && $conf['sneaky_index']) {
$sp = (new PageResolver(''))->resolveId($id . ':');
if (auth_quickaclcheck($sp) < AUTH_READ) {
return false;
}
}
if ($type == 'd') {
// link directories to their start pages
$original = $id;
$id = "$id:";
$id = (new PageResolver(''))->resolveId($id);
$this->startpages[$id] = 1;
// if the resolve id is in the same namespace as the original it's a start page named like the dir
if (getNS($original) === getNS($id)) {
$useNS = $original;
}
} elseif (!empty($this->startpages[$id])) {
// skip already shown start pages
return false;
}
//check hidden
if (isHiddenPage($id)) {
return false;
}
//check ACL
if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) {
return false;
}
$data[$id] = [
'id' => $id,
'type' => $type,
'level' => $lvl,
'open' => $return,
'title' => $this->getTitle($id, $opts['usetitle']),
'ns' => $useNS ?? (string)getNS($id),
];
return $return;
}
/**
* @param string $id
* @param bool $useTitle
* @return array
*/
protected function getMainStartPage($ns, $useTitle)
{
$resolver = new PageResolver('');
$id = $resolver->resolveId($ns . ':');
$item = [
'id' => $id,
'type' => 'd',
'level' => 0,
'open' => true,
'title' => $this->getTitle($id, $useTitle),
'ns' => $ns,
];
$this->startpages[$id] = 1;
return $item;
}
/**
* Get the title for the given page ID
*
* @param string $id
* @param bool $usetitle - use the first heading as title
* @return string
*/
protected function getTitle($id, $usetitle)
{
global $conf;
if ($usetitle) {
$p = p_get_first_heading($id);
if (!empty($p)) return $p;
}
$p = noNS($id);
if ($p == $conf['start'] || !$p) {
$p = noNS(getNS($id));
if (!$p) {
return $conf['start'];
}
}
return $p;
}
}