zapatista.kompot.si/plugins/PicoPagesList/PicoPagesList.php

246 lines
8.0 KiB
PHP

<?php
/**
* Flat and nested pages list navigation for Pico CMS.
*
* - Adds twig global `{{ nested_pages }}` in addition to `{{ pages }}`
* - Render flat or nested HTML navigation tree with `navigation` twig filter
* - Filter pages and nested pages by paths with `exclude()` and `only()` twig filters
*
* Examples :
*
* {{ pages | navigation }} // output a flat pages list
* {{ nested_pages | navigation }} // output a nested pages list
* {{ nested_pages | exclude('sub/page') | navigation }} // filtered nested pages list
* {% assign filtered = pages | only('sub/path/') %} // get filtered flat pages array
*
* @author Nicolas Liautaud
* @link https://github.com/nliautaud/pico-pages-list
* @link http://picocms.org
* @license http://opensource.org/licenses/MIT The MIT License
*/
class PicoPagesList extends AbstractPicoPlugin
{
const API_VERSION = 3;
protected $items;
/**
* Construct the nested pages array.
*
* Triggered after Pico has read all known pages
*
* See {@link DummyPlugin::onSinglePageLoaded()} for details about the
* structure of the page data.
*
* @see Pico::getPages()
* @param array[] &$pages data of all known pages
* @return void
*/
public function onPagesLoaded(array &$pages)
{
$this->items = $this->nestedPages($pages);
}
/**
* Register `$this` in the Twig `{{ PagesList }}` variable.
*
*
* @see Pico::getTwig()
* @param Twig_Environment &$twig Twig instance
* @return void
*/
public function onTwigRegistered(Twig_Environment &$twig)
{
$twig->addFilter(new Twig_SimpleFilter('navigation', function($pages) {
return $this->output($pages);
}, array('is_safe' => array('html'))));
$twig->addFilter(new Twig_SimpleFilter('exclude', function($pages, array $paths = array()) {
return $this->filterPages($pages, $paths);
}, array('is_variadic' => true)));
$twig->addFilter(new Twig_SimpleFilter('only', function($pages, array $paths = array()) {
return $this->filterPages($pages, $paths, true);
}, array('is_variadic' => true)));
}
/**
* Register `$this` in the Twig `{{ PagesList }}` variable.
*
* Triggered before Pico renders the page
*
* @see Pico::getTwig()
* @see DummyPlugin::onPageRendered()
* @param string &$templateName file name of the template
* @param array &$twigVariables template variables
* @return void
*/
public function onPageRendering(string &$templateName, array &$twigVariables)
{
$twigVariables['nested_pages'] = $this->items;
}
/**
* Create the nested pages array according to the pages paths.
*
* @see nested_path
* @param array $pages Pico pages flat array
*/
private function nestedPages($pages)
{
$this->items = array();
foreach ($pages as $page) {
$nested_path = $this->nested_path($page);
$this->items = array_replace_recursive($this->items, $nested_path);
}
return $this->items['_childs'];
}
/**
* Create a nested array of a given path, with the page at the end.
* Each path fragment is in "_childs" of the parent.
*
* @param array $page the page array
* @return array the nested path
*/
private function nested_path($page)
{
$path = self::rtrim($page['id'], '/index');
$parts = explode('/', $path);
$count = count($parts);
$arr = array();
$parent = &$arr;
foreach($parts as $id => $part) {
$value = [];
if(!$part || $id == $count-1) {
$value = $page;
} else {
$currpath = implode('/', array_slice($parts, 0, $id+1));
$value['id'] = $currpath;
}
if($path && !$part) {
$parent = $value;
break;
}
$parent['_childs'][$part] = $value;
$parent = &$parent['_childs'][$part];
}
return $arr;
}
/**
* Strip a substring from the end of a string
*
* @param array $str The input string.
* @param array $substr The substring to remove.
* @return string The modified string.
*/
private static function rtrim($str, $substr)
{
$length = strlen($substr);
return (substr($str, -$length) === $substr) ? substr($str, 0, -$length) : $str;
}
/**
* Filter the pages array according to given paths, as exclusive or inclusive.
*
* @param array $pages The flat or nested pages array.
* @param array $filteredPaths The paths to filter.
* @param boolean $isInclusive If `true` only corresponding paths are kept.
* @return array The filtered array of pages.
*/
public static function filterPages(
$pages,
$filteredPaths,
$isInclusive = false,
$inclusiveOutput = []
) {
foreach($pages as $i => $page) {
if (!isset($page['id'])) return;
$path = self::rtrim($page['id'], '/index');
$isSubPath = self::isSubPath($path, $filteredPaths);
if ($isSubPath) {
if ($isInclusive) $inclusiveOutput[$i] = $page;
else unset($pages[$i]);
continue;
}
if (isset($page['_childs'])) {
$childs = self::filterPages($page['_childs'], $filteredPaths, $isInclusive, $inclusiveOutput);
if ($isInclusive) $inclusiveOutput = $childs;
else $pages[$i]['_childs'] = $childs;
}
}
return $isInclusive ? $inclusiveOutput : $pages;
}
/**
* Return if the given path is a subpath of the given parent path(s)
*
* @param string $path
* @param array $parentPaths array of paths
* @return boolean
*/
private static function isSubPath($path, $parentPaths)
{
foreach($parentPaths as $p) {
if (!is_string($p)) continue;
if ($path == $p) return true;
if (strncmp($path, $p, strlen($p)) === 0)
return true;
}
return false;
}
/**
* Return an html nested list based on a nested pages array.
*
* @param array $pages a nested pages array
* @return string the html list
*/
private function output($pages)
{
if (!is_array($pages)) return;
$html = '<ul>';
foreach ($pages as $pageID => $page)
{
if (!empty($page['hidden'])) continue;
$childsOutput = '';
if(isset($page['_childs'])) {
$childsOutput = $this->output($page['_childs']);
}
$url = isset($page['url']) ? $page['url'] : false;
// use title if the page has one and make a link if the page exists.
if(!$url) $item = "<span>$pageID</span>";
else {
$name = !empty($page['title']) ? $page['title'] : $pageID;
$item = "<a href=\"$url\">$name</a>";
}
// add the pageID in class and indicate if it is the current or parent of the current page.
$class = $pageID;
$class .= $url ? ' is-page' : ' is-directory';
if ($childsOutput) $class .= ' has-childs';
$currentPage = $this->getPico()->getCurrentPage();
if ($currentPage && $currentPage['id']) {
if ($currentPage['id'] === $page['id']) {
$class .= ' is-current is-active';
} elseif ($page['id'] !== 'index') {
$pagePath = (basename($page['id']) === 'index') ? dirname($page['id']) . '/' : $page['id'] . '/';
if (substr_compare($pagePath, $currentPage['id'], 0, strlen($pagePath)) === 0) $class .= ' is-active';
}
}
$html .= "<li class=\"$class\">$item$childsOutput</li>";
}
$html .= '</ul>';
return $html;
}
}