plugins: Manage our navigation through a plugin not manually

This commit is contained in:
Andy Williams 2017-11-29 11:52:30 +00:00
parent 1148adb6bf
commit e727f07ce5
12 changed files with 667 additions and 0 deletions

View File

@ -0,0 +1,16 @@
# Config file for travis-ci.org
language: php
php:
- "7.0"
- "5.6"
- "5.5"
- "5.4"
- "5.3"
env:
matrix:
- DOKUWIKI=master
- DOKUWIKI=stable
before_install: wget https://raw.github.com/splitbrain/dokuwiki-travis/master/travis.sh
install: sh travis.sh
script: cd _test && phpunit --stderr --group plugin_navi

View File

@ -0,0 +1,97 @@
<?php
/**
* Tests for functionality of the navi plugin
*
* @group plugin_navi
* @group plugins
*
*/
class basic_plugin_navi_test extends DokuWikiTest {
protected $pluginsEnabled = array('navi');
public function setUp() {
parent::setUp();
}
public function tearDown() {
parent::tearDown();
}
public function test_controlpage_simple() {
// arrange
$controlpage = " * [[a]]\n * [[b]]\n * [[c]]";
saveWikiText('controlpage', $controlpage, '');
saveWikiText('navi', '{{navi>controlpage}}', '');
// act
$info = array();
$actualHTML = p_render('xhtml', p_get_instructions('{{navi>controlpage}}'), $info);
// assert
$expectedHTML = '<div class="plugin__navi left"><ul>
<li class="level1 "><div class="li"><a href="/./doku.php?id=a" class="wikilink2" title="a" rel="nofollow">a</a></div>
</li>
<li class="level1 close"><div class="li"><a href="/./doku.php?id=b" class="wikilink2" title="b" rel="nofollow">b</a></div>
</li>
</ul>
</div>';
$this->assertEquals($expectedHTML, $actualHTML);
}
public function test_controlpage_complex() {
// arrange
$controlpage = "
* [[en:products:a:start|BasePage]]
* [[en:products:b:d:start|2nd-level Page with hidden child]]
* [[en:products:c:projects|hidden 3rd-level page]]
* [[en:products:b:archive:start|2nd-level pape]]
* [[en:products:c:start|current 2nd-level page with visible child]]
* [[en:products:d:start|visible 3rd-level page]]
";
saveWikiText('controlpage', $controlpage, '');
saveWikiText('navi', '{{navi>controlpage}}', '');
global $ID, $INFO;
// act
$info = array();
$ID = 'en:products:c:start';
$INFO['id'] = 'en:products:c:start';
$actualHTML = p_render('xhtml', p_get_instructions('{{navi>controlpage}}'), $info);
$pq = phpQuery::newDocumentXHTML($actualHTML);
$actualPages = array();
foreach ($pq->find('a') as $page) {
$actualPages[] = $page->getAttribute('title');
}
$actualLiOpen = array();
foreach ($pq->find('li.open > div > a, li.open > div > span > a') as $page) {
$actualLiOpen[] = $page->getAttribute('title');
}
$actualLiClose = array();
foreach ($pq->find('li.close > div > a, li.close > div > span > a') as $page) {
$actualLiClose[] = $page->getAttribute('title');
}
$this->assertEquals(array(
0 => 'en:products:a:start',
1 => 'en:products:b:d:start',
2 => 'en:products:b:archive:start',
3 => 'en:products:c:start',
4 => 'en:products:d:start',
), $actualPages, 'the correct pages in the correct order');
$this->assertEquals(array(
0 => 'en:products:a:start',
1 => 'en:products:c:start',
), $actualLiOpen, 'the pages which have have children and are open should have the "open" class');
$this->assertEquals(array(
0 => 'en:products:b:d:start',
), $actualLiClose, 'the pages which have have children, but are closed should have the "close" class');
}
}

View File

@ -0,0 +1,76 @@
<?php
/**
* Tests for functionality of the navi plugin
*
* @group plugin_navi
* @group plugins
*
*/
class external_plugin_navi_test extends DokuWikiTest {
protected $pluginsEnabled = array('navi');
public function setUp() {
parent::setUp();
}
public function tearDown() {
parent::tearDown();
}
public function test_controlpage_with_external_link() {
// arrange
$controlpage = "
* [[en:products:a:start|BasePage]]
* [[en:products:b:d:start|2nd-level Page with hidden child]]
* [[en:products:c:projects|hidden 3rd-level page]]
* [[en:products:b:archive:start|2nd-level pape]]
* [[en:products:c:start|current 2nd-level page with visible child]]
* [[https://www.example.org|Example Page]]
";
saveWikiText('controlpage', $controlpage, '');
saveWikiText('navi', '{{navi>controlpage}}', '');
global $ID, $INFO;
// act
$info = array();
$ID = 'en:products:c:start';
$INFO['id'] = 'en:products:c:start';
$actualHTML = p_render('xhtml', p_get_instructions('{{navi>controlpage}}'), $info);
// print_r($actualHTML);
$pq = phpQuery::newDocumentXHTML($actualHTML);
$actualPages = array();
foreach ($pq->find('a') as $page) {
$actualPages[] = $page->getAttribute('title');
}
$actualLiOpen = array();
foreach ($pq->find('li.open > div > a, li.open > div > span > a') as $page) {
$actualLiOpen[] = $page->getAttribute('title');
}
$actualLiClose = array();
foreach ($pq->find('li.close > div > a, li.close > div > span > a') as $page) {
$actualLiClose[] = $page->getAttribute('title');
}
$this->assertEquals(array(
0 => 'en:products:a:start',
1 => 'en:products:b:d:start',
2 => 'en:products:b:archive:start',
3 => 'en:products:c:start',
4 => 'https://www.example.org',
), $actualPages, 'the correct pages in the correct order');
$this->assertEquals(array(
0 => 'en:products:a:start',
1 => 'en:products:c:start',
), $actualLiOpen, 'the pages which have have children and are open should have the "open" class');
$this->assertEquals(array(
0 => 'en:products:b:d:start',
), $actualLiClose, 'the pages which have have children, but are closed should have the "close" class');
}
}

View File

@ -0,0 +1,108 @@
<?php
/**
* General tests for the navi plugin
*
* @group plugin_navi
* @group plugins
*/
class general_plugin_navi_test extends DokuWikiTest {
/**
* Simple test to make sure the plugin.info.txt is in correct format
*/
public function test_plugininfo() {
$file = __DIR__.'/../plugin.info.txt';
$this->assertFileExists($file);
$info = confToHash($file);
$this->assertArrayHasKey('base', $info);
$this->assertArrayHasKey('author', $info);
$this->assertArrayHasKey('email', $info);
$this->assertArrayHasKey('date', $info);
$this->assertArrayHasKey('name', $info);
$this->assertArrayHasKey('desc', $info);
$this->assertArrayHasKey('url', $info);
$this->assertEquals('navi', $info['base']);
$this->assertRegExp('/^https?:\/\//', $info['url']);
$this->assertTrue(mail_isvalid($info['email']));
$this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
$this->assertTrue(false !== strtotime($info['date']));
}
/**
* Test to ensure that every conf['...'] entry in conf/default.php has a corresponding meta['...'] entry in
* conf/metadata.php.
*/
public function test_plugin_conf() {
$conf_file = __DIR__.'/../conf/default.php';
if (file_exists($conf_file)){
include($conf_file);
}
$meta_file = __DIR__.'/../conf/metadata.php';
if (file_exists($meta_file)) {
include($meta_file);
}
$this->assertEquals(gettype($conf), gettype($meta),'Both ' . DOKU_PLUGIN . 'navi/conf/default.php and ' . DOKU_PLUGIN . 'navi/conf/metadata.php have to exist and contain the same keys.');
if (gettype($conf) != 'NULL' && gettype($meta) != 'NULL') {
foreach($conf as $key => $value) {
$this->assertArrayHasKey($key, $meta, 'Key $meta[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'navi/conf/metadata.php');
}
foreach($meta as $key => $value) {
$this->assertArrayHasKey($key, $conf, 'Key $conf[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'navi/conf/default.php');
}
}
}
/*Test that the levels have the right classes
public function render_test() {
$data = array(
0 => '/home/michael/public_html/dokuwiki/data/pages/plugins/navi.txt',
1 => array(
'lvl1' => array(
'parents' => array(),
'page' => 'lvl1:start',
'title' => '',
'lvl' => 1,
),
'lvl2' => array(
'parents' => Array(
0 => 'lvl1',
),
'page' => 'lvl2:start',
'title' => '',
'lvl' => 2,
),
'lvl3' => array(
'parents' => Array(
0 => 'lvl1',
1 => 'lvl2',
),
'page' => 'lvl3:start',
'title' => '',
'lvl' => 3,
),
'lvl4' => array(
'parents' => Array(
0 => 'lvl1',
1 => 'lvl2',
2 => 'lvl3',
),
'page' => 'lvl4:start',
'title' => '',
'lvl' => 4,
),
),
2 => '',
);
render('xhtml',$data);
}*/
}

View File

@ -0,0 +1,39 @@
<?php
/**
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Andreas Gohr <gohr@cosmocode.de>
*/
if(!defined('DOKU_INC')) die();
if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
require_once(DOKU_PLUGIN.'action.php');
/**
* All DokuWiki plugins to extend the parser/rendering mechanism
* need to inherit from this class
*/
class action_plugin_navi extends DokuWiki_Action_Plugin {
/**
* plugin should use this method to register its handlers with the dokuwiki's event controller
*/
function register(Doku_Event_Handler $controller) {
$controller->register_hook('PARSER_CACHE_USE','BEFORE', $this, 'handle_cache_prepare');
}
/**
* prepare the cache object for default _useCache action
*/
function handle_cache_prepare(&$event, $param) {
$cache =& $event->data;
// we're only interested in wiki pages
if (!isset($cache->page)) return;
if ($cache->mode != 'i') return;
// get meta data
$depends = p_get_metadata($cache->page, 'relation naviplugin');
if(!is_array($depends) || !count($depends)) return; // nothing to do
$cache->depends['files'] = !empty($cache->depends['files']) ? array_merge($cache->depends['files'], $depends) : $depends;
}
}

View File

@ -0,0 +1,3 @@
<?php
$conf['arrow'] = 'left';

View File

@ -0,0 +1,3 @@
<?php
$meta['arrow'] = array('multichoice','_choices' => array('left', 'right', 'none'));

View File

@ -0,0 +1,8 @@
base navi
author Andreas Gohr
email dokuwiki@cosmocode.de
date 2016-10-12
name Navigation Plugin
desc Build a navigation menu from a list
url http://www.dokuwiki.org/plugin:navi

View File

@ -0,0 +1,12 @@
jQuery(function() {
'use strict';
jQuery('li.open, li.close').find('> div.li').each(function (index, element){
var link = jQuery(element).find('a').attr('href');
var $arrowSpan = jQuery('<span></span>').click(function (event) {
window.location = link;
});
$arrowSpan.addClass('arrowUnderlay');
jQuery(element).append($arrowSpan);
});
});

View File

@ -0,0 +1,45 @@
#dokuwiki__aside, .sidebar_box {
div.plugin__navi {
span.arrowUnderlay {
padding: 4px;
cursor: pointer;
background: no-repeat top right/100%;
}
li.open {
span.arrowUnderlay {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAMAAAC6V+0/AAAANlBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3dmhyAAAAEXRSTlMAAQcJCgsUIDZGW2Jjmt75/Rmge4gAAABISURBVBjTY2CgJWDmwBRj4RPkxiImiC4KEuNHEwWJcbPyoYiyQLgsyKJwDpIoNiY2TUyYxnMxsAlgOISXgYGdE827PIy0CEUAdlAEbg1NcZ0AAAAASUVORK5CYII=);
}
}
li.close {
span.arrowUnderlay {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUBAMAAAB/pwA+AAAAMFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABaPxwLAAAAD3RSTlMAAQIECiMlYWJ3psPc3v2CpcpnAAAARklEQVQIW2NgIAgUoLQBg9QVCIvjGUP8Xwcws/83A8d/sDCY6gcLg0kwHyoHEoBIgUSggiDh+VBjgMIwQZghYMB+hIF8AABJKyB+OnNcUQAAAABJRU5ErkJggg==);
}
}
&.none {
span.arrowUnderlay {
display: none;
}
}
&.left {
li {
list-style-type: none;
}
span.arrowUnderlay {
position: absolute;
left: 1.3em;
}
}
&.right {
span.arrowUnderlay {
float: right;
}
}
}
}

View File

@ -0,0 +1,241 @@
<?php
/**
* Build a navigation menu from a list
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Andreas Gohr <gohr@cosmocode.de>
*/
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();
if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
require_once(DOKU_PLUGIN.'syntax.php');
class syntax_plugin_navi extends DokuWiki_Syntax_Plugin {
/**
* What kind of syntax are we?
*/
function getType(){
return 'substition';
}
/**
* What about paragraphs?
*/
function getPType(){
return 'block';
}
/**
* Where to sort in?
*/
function getSort(){
return 155;
}
/**
* Connect pattern to lexer
*/
function connectTo($mode) {
$this->Lexer->addSpecialPattern('{{navi>[^}]+}}',$mode,'plugin_navi');
}
/**
* Handle the match
*/
function handle($match, $state, $pos, Doku_Handler $handler){
global $ID;
$id = substr($match,7,-2);
list($id,$opt) = explode('?',$id,2);
$id = cleanID($id);
// fetch the instructions of the control page
$instructions = p_cached_instructions(wikiFN($id),false,$id);
// prepare some vars
$max = count($instructions);
$pre = true;
$lvl = 0;
$parents = array();
$page = '';
$cnt = 0;
// build a lookup table
for($i=0; $i<$max; $i++){
if($instructions[$i][0] == 'listu_open'){
$pre = false;
$lvl++;
if($page) array_push($parents,$page);
}elseif($instructions[$i][0] == 'listu_close'){
$lvl--;
array_pop($parents);
}elseif($pre || $lvl == 0){
unset($instructions[$i]);
}elseif($instructions[$i][0] == 'listitem_close'){
$cnt++;
}elseif($instructions[$i][0] == 'internallink'){
$foo = true;
$page = $instructions[$i][1][0];
resolve_pageid(getNS($ID),$page,$foo); // resolve relative to sidebar ID
$list[$page] = array(
'parents' => $parents,
'page' => $page,
'title' => $instructions[$i][1][1],
'lvl' => $lvl
);
} elseif ($instructions[$i][0] == 'externallink') {
$url = $instructions[$i][1][0];
$list['_'.$page] = array(
'parents' => $parents,
'page' => $url,
'title' => $instructions[$i][1][1],
'lvl' => $lvl
);
}
}
return array(wikiFN($id),$list,$opt);
}
/**
* Create output
*
* We handle all modes (except meta) because we pass all output creation back to the parent
*/
function render($format, Doku_Renderer $R, $data) {
$fn = $data[0];
$opt = $data[2];
$data = $data[1];
if($format == 'metadata'){
$R->meta['relation']['naviplugin'][] = $fn;
return true;
}
$R->info['cache'] = false; // no cache please
$path = $this->getOpenPath($data, $opt);
$arrowLocation = $this->getConf('arrow');
$R->doc .= '<div class="plugin__navi ' . $arrowLocation . '">';
$this->renderTree($data, $path, $R);
$R->doc .= '</div>';
return true;
}
public function getOpenPath($data, $opt) {
global $INFO;
$openPath = array();
if(isset($data[$INFO['id']])){
$openPath = (array) $data[$INFO['id']]['parents']; // get the "path" of the page we're on currently
array_push($openPath,$INFO['id']);
}elseif($opt == 'ns'){
$ns = $INFO['id'];
// traverse up for matching namespaces
if($data) do {
$ns = getNS($ns);
$try = "$ns:";
resolve_pageid('',$try,$foo);
if(isset($data[$try])){
// got a start page
$openPath = (array) $data[$try]['parents'];
array_push($openPath,$try);
break;
}else{
// search for the first page matching the namespace
foreach($data as $key => $junk){
if(getNS($key) == $ns){
$openPath = (array) $data[$key]['parents'];
array_push($openPath,$key);
break 2;
}
}
}
}while($ns);
}
return $openPath;
}
/**
* @param $data
* @param $parent
* @param Doku_Renderer $R
*/
public function renderTree($data, $parent, Doku_Renderer $R) {
// create a correctly nested list (or so I hope)
$open = false;
$lvl = 1;
$R->listu_open();
// read if item has childs and if it is open or closed
$upper = array();
foreach ((array)$data as $pid => $info) {
$state = (array_diff($info['parents'], $parent)) ? 'close' : '';
$countparents = count($info['parents']);
if ($countparents > '0') {
for ($i = 0; $i < $countparents; $i++) {
$upperlevel = $countparents - 1;
$upper[$info['parents'][$upperlevel]] = ($state == 'close') ? 'close' : 'open';
}
}
}
unset($pid);
foreach ((array)$data as $pid => $info) {
// only show if we are in the "path"
if (array_diff($info['parents'], $parent)) {
continue;
}
if ($upper[$pid]) {
$menuitem = ($upper[$pid] == 'open') ? 'open' : 'close';
} else {
$menuitem = '';
}
// skip every non readable page
if (auth_quickaclcheck(cleanID($info['page'])) < AUTH_READ) {
continue;
}
if ($info['lvl'] == $lvl) {
if ($open) {
$R->listitem_close();
}
$R->listitem_open($lvl . ' ' . $menuitem);
$open = true;
} elseif ($lvl > $info['lvl']) {
for ($lvl; $lvl > $info['lvl']; --$lvl) {
$R->listitem_close();
$R->listu_close();
}
$R->listitem_close();
$R->listitem_open($lvl . ' ' . $menuitem);
} elseif ($lvl < $info['lvl']) {
// more than one run is bad nesting!
for ($lvl; $lvl < $info['lvl']; ++$lvl) {
$R->listu_open();
$R->listitem_open($lvl + 1 . ' ' . $menuitem);
$open = true;
}
}
$R->listcontent_open();
if (substr($pid, 0, 1) != '_') {
$R->internallink(':' . $info['page'], $info['title']);
} else {
$R->externallink($info['page'], $info['title']);
}
$R->listcontent_close();
}
while ($lvl > 0) {
$R->listitem_close();
$R->listu_close();
--$lvl;
}
}
}

View File

@ -106,6 +106,25 @@ h1, h2, h3, h4, h5, h6,
display: block;
}
.nav .close {
float: none;
font-size: inherit;
font-weight: normal;
line-height: inherit;
color: inherit;
text-shadow: inherit;
opacity: 1;
filter: none;
}
.nav .open {
font-weight: bold;
}
.nav .open ul {
font-weight: normal;
}
@media (min-width: 768px) and (max-width: 993px) {
.bs-sidebar {
margin-top: 50px;