User:Nigelk/Nav
See also now the concat-pages special page that works with NavMap.
Version 0.6, 2006-01-10
[edit]en:User:Phil Boswell requested that i set a page up at meta for easier discussion about the [Nav Extension] i wrote. I am regurgitating much of the text from the original sandbox wiki where the extension is actually installed, but to see it you'll still need to [go there] to look.
Introduction
[edit]I came to this pass because I have a largish set of family memorabilia (notebooks, letters, other documents) I am digitizing and putting into a mediawiki instance. Some of these documents are large, comprising many digitized objects in sequence. In the model I want to use, each digitized item or side gets its own wiki page. Sequencing them tractably came to be a conundrum. I wanted prev-up-next links, but i wanted the maintenance of them to be straight-forward.
Between not wanting to hard-code numerous links inside lots of individual pages that I wanted to link together in a particular, static way (so, several dozen or even hundreds of pages with a natural sequence to start with), and not seeing how i could easily accomplish it with Categories, neither, and influence from [METS]' [structMap], I propose Nav Maps as described here.
The NavMap Extension understands the <nav> tag primitively: inside the navmap tag is the name of some page in the wiki (I suppose it wouldn't have to be in the same wiki...). This named page has a list of pages in a wiki list, with optionally a first page not in the list.
The extension tries to locate CURRENTPAGE in the contents of that page, either as the first, not-in-list page, or as one of the pages given in the list.
- If the former, a box with the full list as links is rendered out where the nav tag was.
- if the latter, some navigational links of "Previous", "Up", and "Next" are output, with adjacent or the not-in-list page, as appropriate.
See the demonstrations [at the original location].
To my pleasant surprise that the What links here link picked up the links made by Nav, I hadn't been expecting that they would be noticed. Obviously I ain't no MediaWiki expert (and, fwiw, I'm not much of a fist at php, neither).
Comment: We could certainly imagine this having more features: understanding more senses of 'up' than just the Toc page, parameters in the nav tag directing how the links and toc should display, &c.
Question: the PurgeCache extension still often not picking up when i want it to: if the navmap page itself changes, any referenced page also needs to change. thoughts?
Comment: at least one commentor suggested moving this all up into article rendition. that's certainly still a possibility, but that was a bigger task than i wanted to engage in just at the moment. Anyone with interest, do feel free.
NavMap.php, the Nav Extension
[edit]<?php /** Nav Map 0.6 2006-01-10 06:17:28 nigel kerr, nigelk at-sign nigelk period org Used to produce next/up/forward links from any page in a specified tree of pages (a navmap). Includes some options for how the links should display, how the data about the tree should display. when one of the three tags nav, navmap, or navmap-data are found in a currently-viewed page, this extension is set to parse them. navmap (syn. nav) are expected to contain a name of a page. This named page, the nav map itself, is expected to itself contain a navmap-data tag. the data in this navmap-data tag describe a tree of pages, and the extension attempts to locate the currently-viewed page in that tree. The very top of the tree, and the immediate neighbors of the currently-viewed page are used as targets for navigational links, that are then inserted back into the currently-viewed page. when a navmap or nav tag is found on a currently-viewed page that proves to be the top page of the indicated navmap, a table-of-contents tree with live links is inserted instead, and optionally a link for the page to Start with. ordinarily a navmap-data tag is only encountered by this extension when trying to look up a page. when a nav map page is itself visited, the navmap-data tag is rendered much like the table-of-contents tree in the preceding paragraph. the navmap (or nav) tag itself may have the following attributes: suppress="true", causes NavMap to swallow the tag and output nothing. suppress="tocbox", NavMap won't output the tocbox from the toc page (but continues to see if the "Start" link is to be output) navboxstyle="[css properties text]", applied to the div tag surrounding the links block prev="[text for Previous link]" next="[text for Next link]" up="[text for Up link]" start="[text for the Start link]" tocboxstyle="[css properties text]", applied to the div tag surrounding the toc box block startlink="true", from the toc page, output the start link in a navbox div. label="true", include a label in the nav box of p-u-n that is a link to toc page (useful for situations when a given page is in two different navmaps). the navmap-data tag itself may have the following attributes: tocboxstyle="[css properties text]", applied to the div tag surrounding the toc box block the navmap-data tag contains lines of this pattern: optionally: 1 line with no * or # at the beginning, and a page name, optionally followed by a | and link text. This is understood to be the TOC page or top of the nav map, when present. one or more lines of bulleted- or numbered-list style (start with one or more * or #), each of which has a page name, optionally followed by a | and link text. Note that this extension is not too choosy about the structure of the list, it could mix and match bullets and numbers, and even have very odd structure. Lines that do not match the above patterns inside the navmap-data tag, however, are discarded silently. The more well-formed this list, the better to drive other applications (such as collating pages of a section or subsection into a single body). Can be used in conjunction with Special:ConcatPages, which see (meta [[User:Nigelk/ConcatPages]]) Future Plans: the nav map and the rendering of the nav map need to be prised apart in some places. the rendering wants to be in a class of its own (ideally this extension has the bare minimum for setting up and calling an extension, and two classes). */ $wgExtensionFunctions[] = "wfNavMap"; $navmapVersion = "0.5"; $navmapDefaultNavBoxStyle = "text-align: right;"; $navmapDefaultTocBoxStyle = "padding: 1em; margin: 1em; border: thin black solid;"; $navmapDefaultLinkTextPrev = "Previous"; $navmapDefaultLinkTextUp = "Up"; $navmapDefaultLinkTextNext = "Next"; $navmapDefaultLinkTextStart = "Start"; function wfNavMap() { global $wgParser; $wgParser->setHook( "nav", "renderNavMap" ); /* backwards compatible. */ $wgParser->setHook( "navmap", "renderNavMap" ); $wgParser->setHook( "navmap-data", "renderNavMapData" ); } function getTocBoxTags( $argv ) { global $navmapDefaultTocBoxStyle; $style = $navmapDefaultTocBoxStyle; if ( $argv["tocboxstyle"] ) { $style = $argv["tocboxstyle"]; } return array( "<div class='navmap-tocbox' style='$style'>", "</div>" ); } function getNavBoxTags( $argv ) { global $navmapDefaultNavBoxStyle; $style = $navmapDefaultNavBoxStyle; if ( $argv["navboxstyle"] ) { $style = $argv["navboxstyle"]; } return array( "<div class='navmap-navbox' style='$style'>", "</div>" ); } function renderLinkLinkText( $l, $lt ) { if ( (! $l && ! $lt) || ! $l ) { return ''; } if ( ! $lt ) { return "[[$l]]"; } return "[[$l|$lt]]"; } function renderNavLinks( $p, $u, $n, $navLinkTexts = array("Previous", "Up", "Next") ) { $retval = ''; if ( $p ) { $retval .= renderLinkLinkText($p, $navLinkTexts[0]); } if ( $u ) { if ( $retval ) { $retval .= ' - '; } $retval .= renderLinkLinkText($u, $navLinkTexts[1]); } if ( $n ) { if ( $retval ) { $retval .= ' - '; } $retval .= renderLinkLinkText($n, $navLinkTexts[2]); } return $retval; } function getBadMapMessage( $input ) { return "<p style='color: red; font-size: 1.5em;'>Failed to make valid map from '$input' !</p>"; } class NavMap { /*private*/ var $mapPage; /*private*/ var $maplines; /*private*/ var $map = array(); /*private*/ var $mapReverse = array(); /*private*/ var $toc; /*private*/ var $mapcount; /*private*/ var $valid = 1; /*public*/ function NavMap( $navMapPage, $navMapData = NULL ) { $this->mapPage = $navMapPage; if ( is_null($navMapData) ) { $this->parseMapPage($this->mapPage); } else { $this->parseMapData($navMapData); } } /*private*/ function parseMapData( $data ) { $this->maplines = $this->getMapLinesFromData($data); $this->parseMapLines(); } /*private*/ function parseMapPage($page) { $this->maplines = $this->getMapLines($page); $this->parseMapLines(); } function parseMapLines() { if ( ! $this->valid || ! $this->maplines ) { return NULL; } $this->toc = $this->parseMapTocLine( $this->maplines[0] ); if ( $this->toc ) { array_shift($this->maplines); } $flatmap = array_map(array($this, 'parseMapBodyLine'), $this->maplines); $index = 1; foreach ( $flatmap as $key => $value ) { if ( ! is_null($value) ) { array_push($this->map, $value); /* ignore all mentions of a page after the first! */ if ( ! $this->mapReverse[$value[1]] ) { $this->mapReverse[$value[1]] = $index; $index++; } } } $this->mapcount = count($this->map); } /*private*/ function parseMapTocLine( $line ) { $line = trim($line); $pattern = '/^([^\\*\\#][^\\|]+)(\\|([^\\|]+))?/'; preg_match($pattern, $line, $matches ); /* no link text, just a link. */ if ( count($matches) == 2 ) { return array( trim($matches[1]), trim($matches[1]) ); } /* link text also provided. */ if ( count($matches) == 4 ) { return array( trim($matches[1]), trim($matches[3]) ); } /* cheerfully ignore bad lines. */ return NULL; } /*private*/ function parseMapBodyLine( $line ) { $line = trim($line); $pattern = '/^([\\*\\#]+)\\s*([^\\|]+)(\\|([^\\|]+))?/'; preg_match($pattern, $line, $matches); if ( count($matches) == 3 ) { $ret = array ( trim($matches[1]), trim($matches[2]), trim($matches[2]) ); return $ret; } if ( count($matches) == 5 ) { $ret = array ( trim($matches[1]), trim($matches[2]), trim($matches[4]) ); return $ret; } /* cheerfully ignore bad lines. */ return NULL; } /*private*/ function getMapLines($page) { global $wgTitle; global $wgArticle; /* remove these when we go non-globals... */ $oldTitle = $wgTitle; $oldArticle = $wgArticle; $title = trim($page); $article = new Article( Title::newFromText( $title ) ); $content = $article->getContent(false); $wgTitle = $oldTitle; $wgArticle = $oldArticle; /* end global shuffling */ return $this->getMapLinesFromData($content); } /*private*/ function getMapLinesFromData($data) { /* we probably have the new navmap-data style... but check anyways, we might have old style with no tagging. */ if (! $data ) { $this->valid = 0; return NULL; } $pattern = '/.*<navmap-data>(.+)<\\/navmap-data>.*/s'; $matchReturned = preg_match( $pattern, $data, $matches ); if ( $matchReturned == 1 ) { $data = $matches[1]; } $data = trim($data); $maplines = split("\n", str_replace("\n\n", "\n", str_replace("\r","\n",$data))); return $maplines; } /*public*/ function isValid() { return $this->valid; } /*public*/ function getStartPage() { return $this->map[0][1]; } /*public*/ function isTocPage( $page ) { return ( $this->toc[0] && $this->toc[0] === $page ); } /*public*/ function getLinksForPage( $page ) { $prev = NULL; $up = NULL; $next = NULL; if ( $this->mapReverse[$page] ) { $i = $this->mapReverse[$page] - 1; if ( $i > 0 ) { $prev = $this->map[$i-1][1]; } if ( $i < ($this->mapcount - 1) ) { $next = $this->map[$i+1][1]; } if ( $this->toc ) { $up = $this->toc[0]; } } return array( $prev, $up, $next ); } /*public*/ function getFullTocForm() { $retval = ''; if ( $this->toc ) { $retval .= $this->toc[0] . "\n"; } foreach ( $this->map as $k => $v ) { $retval .= $v[0] . " [[" . $v[1] . "|" . $v[2] . "]]\n"; } return $retval; } /*public*/ function getTocLink() { $retval = ''; if ( $this->toc ) { if ( $this->toc[0] && $this->toc[1] ) { return "[[" . $this->toc[0] . "|" . $this->toc[1] . "]]"; } else { return "[[" . $this->toc[0] . "]]"; } } } /*public*/ function getRenderedNavMapData() { $retval = ''; if ( $this->toc ) { $retval .= "'''[[" . $this->toc[0] . "|" . $this->toc[1] . "]]'''\n"; } foreach ( $this->map as $k => $v ) { $retval .= $v[0] . " [[" . $v[1] . "|" . $v[2] . "]]\n"; } return $retval; } /*public*/ function howManyChildren( $page ) { $idx = $this->mapReverse[$page] - 1; if ( $idx >= 0 && $idx < $this->mapcount ) { $depth = strlen($this->map[$idx][0]); $childrenCount = 0; for ( $i = ($idx+1); $i < $this->mapcount; $i++ ) { if ( strlen($this->map[$i][0]) > $depth ) { $childrenCount++; } else { break; } } return $childrenCount; } return NULL; } /*public*/ function getPageSequence( $start, $end ) { $start_idx = $this->mapReverse[$start] - 1; $end_idx = $this->mapReverse[$end] - 1; if ( $start_idx >= 0 && $start_idx < $this->mapcount && $end_idx > $start_idx && $end_idx < $this->mapcount ) { $retval = array(); $further = $this->howManyChildren($end); for ( $i = $start_idx; $i <= $end_idx + $further; $i++ ) { array_push( $retval, $this->map[$i][1] ); } return $retval; } return NULL; } } function renderNavMap($input, $argv) { global $wgTitle; global $wgOut; global $navmapDefaultLinkTextPrev; global $navmapDefaultLinkTextUp; global $navmapDefaultLinkTextNext; purgePage(); if ( (! trim($input)) || ($argv["suppress"] === "true") ) { return ''; } $currentPage = trim($wgTitle->getFullText()); $mapper = new NavMap(trim($input), NULL); if ( ! $mapper->isValid() ) { return getBadMapMessage($input); } if ( $mapper->isTocPage($currentPage) ) { $torender = ''; if ( ! ($argv["suppress"] === "tocbox") ) { $unrenderedToc = $mapper->getFullTocForm(); list($tocBoxStart, $tocBoxEnd) = getTocBoxTags($argv); $torender .= $tocBoxStart . $unrenderedToc . $tocBoxEnd . "\n"; } if ( $argv["startlink"] === "true" ) { list($navStart, $navEnd) = getNavBoxTags($argv); $startlink = renderLinkLinkText( $mapper->getStartPage(), ($argv["start"] ? $argv["start"] : $navmapDefaultLinkTextStart )); $torender .= $navStart . $startlink . $navEnd . "\n"; } return $wgOut->parse($torender); } else { list($prev, $up, $next) = $mapper->getLinksForPage($currentPage); $linktexts = array( ($argv["prev"] ? $argv["prev"] : $navmapDefaultLinkTextPrev ), ($argv["up"] ? $argv["up"] : $navmapDefaultLinkTextUp ), ($argv["next"] ? $argv["next"] : $navmapDefaultLinkTextNext ), ); $fmtLinks = renderNavLinks($prev, $up, $next, $linktexts); list($navStart, $navEnd) = getNavBoxTags($argv); $label = ''; if ( $argv["label"] === "true" ) { $toclink = $mapper->getTocLink(); if ( $toclink != '' ) { $label = "<small>" . $toclink . "</small><br/>"; } } return $wgOut->parse($navStart . $label . $fmtLinks . $navEnd . "\n"); } } function renderNavMapData($input, $argv) { global $wgOut; purgePage(); if ( ! trim($input) ) { return ''; } $mapper = new NavMap( NULL, $input ); if ( ! $mapper->isValid() ) { return getBadMapMessage($input); } $rendered = $mapper->getRenderedNavMapData(); if ( $rendered ) { list($tocBoxStart, $tocBoxEnd) = getTocBoxTags($argv); return $wgOut->parse( $tocBoxStart . $rendered . $tocBoxEnd . "\n" ); } return $input; } ?>
NavMapTest.php
[edit]For general interest, i also include here unit tests. a brief google shewed that there are indeed php unit-test-y things out there, but rather than spend time learning about them, i did the following crufty testing. i shan't pretend this is 100% coverage, but it was a useful way to test and write. how do others here in mediawiki land test/unit-test?
i do also assert that this ran for both php 4.3.10 and php 5.0.4 from the command line. if the tests passed, the onliest output i expected to see was the http headers. if a test failed, then there was a message.
<?php /** Tests for NavMap.php mocks up many mediawiki objects and globals, so this set of tests doesnt really tell one anything about those mocked up things, just this author's understanding of how they seem to behave. */ $failures = 0; function assertTrue( $message, $trueOrFalse ) { if ( ! $trueOrFalse ) { print( $message . "\n" ); $failures++; } } function endTests() { if ( $failures > 0 ) { print ("!!! there were exactly $failures failures !!!\n\n"); } } # Mock classes class MockWgOut { /*public*/ function parse( $input ) { return $input; } } class MockWgTitle { /*private*/ var $fullTexts = array(); /*public*/ function addFullTexts( $returns ) { array_push($this->fullTexts, $returns ); } /*public*/ function getFullText() { return array_shift($this->fullTexts); } } class MockWgParser { /*private*/ var $tag = array(); /*private*/ var $func = array(); /*public*/ function setHook( $tag, $func ) { array_push($this->tag, $tag); array_push($this->func, $func); } /*public*/ function getTags() { return $this->tag; } /*public*/ function getFuncs() { return $this->func; } /*public*/ function reset() { $this->tag = array(); $this->func = array(); } } $articleContent; class Article { /*public*/ function __construct( $title ) { } /*static*/ function setContent( $content ) { global $articleContent; $articleContent = $content; } /*public*/ function getContent( $trueOrFalse ) { global $articleContent; return $articleContent; } } class Title { function newFromText( $text, $defaultNamespace = "DEFAULT" ) { return "newFromText"; } } function purgePage() { } $wgParser; $wgTitle; $wgOut; $wgParser = new MockWgParser(); $wgOut = new MockWgOut(); # tests require_once("NavMap.php"); assertTrue( "00 wrong navmapVersion", $navmapVersion === "0.5" ); wfNavMap(); $tags = $wgParser->getTags(); $funcs = $wgParser->getFuncs(); assertTrue( "01 wrong hooks set in wfNavMap()", ( count($tags) == 3 && count($funcs) == 3 && $tags[0] === "nav" && $funcs[0] === "renderNavMap" && $tags[1] === "navmap" && $funcs[1] === "renderNavMap" && $tags[2] === "navmap-data" && $funcs[2] === "renderNavMapData" ) ); $tocBoxArgv["nostyle"] = 1; $tocBoxReturns = getTocBoxTags( $tocBoxArgv ); assertTrue( "02 wrong tags for toc box", (count($tocBoxReturns) === 2 && $tocBoxReturns[0] === "<div class='navmap-tocbox' style='padding: 1em; margin: 1em; border: thin black solid;'>" && $tocBoxReturns[1] === "</div>" ) ); $tocBoxArgv["tocboxstyle"] = "foobar"; $tocBoxReturns = getTocBoxTags( $tocBoxArgv ); assertTrue( "03 wrong tags for toc box", (count($tocBoxReturns) === 2 && $tocBoxReturns[0] === "<div class='navmap-tocbox' style='foobar'>" && $tocBoxReturns[1] === "</div>" ) ); $navBoxArgv["nostyle"] = 1; $navBoxReturns = getNavBoxTags( $navBoxArgv ); assertTrue( "04 wrong tags for nav box", (count($navBoxReturns) === 2 && $navBoxReturns[0] === "<div class='navmap-navbox' style='text-align: right;'>" && $navBoxReturns[1] === "</div>" ) ); $navBoxArgv["navboxstyle"] = "foobar"; $navBoxReturns = getNavBoxTags( $navBoxArgv ); assertTrue( "05 wrong tags for nav box", (count($navBoxReturns) === 2 && $navBoxReturns[0] === "<div class='navmap-navbox' style='foobar'>" && $navBoxReturns[1] === "</div>" ) ); # rendering tests $renderInput = 'Nav:test1'; $renderArgv = array(); $renderArgv["suppress"] = "true"; $render1 = renderNavMap( $renderInput, $renderArgv ); assertTrue( "06 renderNavMap didnt suppress", ($render1 === '') ); $renderInput = ''; $renderArgv = array(); $render2 = renderNavMap( $renderInput, $renderArgv ); assertTrue( "07 empty input didnt return empty string", ($render2 === '') ); $renderInput = 'Nav:test3'; $renderArgv = array(); $wgTitle = new MockWgTitle(); $wgTitle->addFullTexts( "Middle page" ); Article::setContent("<navmap-data>TOC page\n* First page\n* Middle page\n* Last page</navmap-data>"); $render3 = renderNavMap( $renderInput, $renderArgv ); $expected3 = "<div class='navmap-navbox' style='text-align: right;'>[[First page|Previous]] - [[TOC page|Up]] - [[Last page|Next]]</div>\n"; assertTrue( "08 TOC page did not return nav box", ($render3 === $expected3) ); $renderInput = 'Nav:test3'; $renderArgv = array(); $renderArgv["prev"] = "Earlier"; $renderArgv["up"] = "Topper"; $renderArgv["next"] = "Later"; $wgTitle = new MockWgTitle(); $wgTitle->addFullTexts( "Middle page" ); Article::setContent("<navmap-data>TOC page\n* First page\n* Middle page\n* Last page</navmap-data>"); $render3 = renderNavMap( $renderInput, $renderArgv ); $expected3 = "<div class='navmap-navbox' style='text-align: right;'>[[First page|Earlier]] - [[TOC page|Topper]] - [[Last page|Later]]</div>\n"; assertTrue( "08a TOC page did not return nav box", ($render3 === $expected3) ); $renderInput = 'Nav:test4'; $renderArgv = array(); $renderArgv["tocboxstyle"] = 'foo'; $wgTitle = new MockWgTitle(); $wgTitle->addFullTexts( "TOC page" ); Article::setContent("<navmap-data>TOC page\n* First page\n* Middle page\n* Last page</navmap-data>"); $render4 = renderNavMap( $renderInput, $renderArgv ); $expected4 = "<div class='navmap-tocbox' style='foo'>TOC page\n* [[First page|First page]]\n* [[Middle page|Middle page]]\n* [[Last page|Last page]]\n</div>\n"; assertTrue( "09 TOC page did not return toc box", ($render4 === $expected4) ); $linktexts = array("p", "u", "n"); assertTrue("10 renderNavLinks wasnt right", ("[[1|p]] - [[2|u]] - [[3|n]]" === renderNavLinks("1","2","3", $linktexts) ) ); $linktexts = array("p", "u", "n"); assertTrue("11 renderNavLinks wasnt right", ("[[1|p]] - [[3|n]]" === renderNavLinks("1", NULL,"3", $linktexts) ) ); $renderInput = "TOC page|blah\n* First page|1\n* Middle page|2\n* Last page|3"; $renderArgv = array(); $renderArgv["tocboxstyle"] = 'foo'; $render5 = renderNavMapData( $renderInput, $renderArgv ); $expected5 = "<div class='navmap-tocbox' style='foo'>'''[[TOC page|blah]]'''\n* [[First page|1]]\n* [[Middle page|2]]\n* [[Last page|3]]\n</div>\n"; assertTrue( "12 TOC page did not return toc box", ($render5 === $expected5) ); $renderInput = 'Nav:test3'; Article::setContent("<navmap-data>TOC page\n* First page\n* Middle page\n* Last page</navmap-data>"); $navmap = new NavMap($renderInput); assertTrue( "13 navmapp didnt think it was valid", ($navmap->isValid()) ); $renderInput = 'Nav:test14'; Article::setContent(''); $navmap = new NavMap($renderInput); assertTrue( "14 navmapp didnt think it was invalid", (! $navmap->isValid()) ); $renderInput = 'Nav:test15'; $renderArgv = array(); Article::setContent(''); $wgTitle = new MockWgTitle(); $wgTitle->addFullTexts( "Fluggo page" ); $render15 = renderNavMap( $renderInput, $renderArgv ); $expected15 = "<p style='color: red; font-size: 1.5em;'>Failed to make valid map from 'Nav:test15' !</p>"; assertTrue( "15 bad return for invalid navmap", ($render15 == $expected15) ); $renderInput = 'Nav:test16'; $renderArgv = array(); $renderArgv["label"] = "true"; $wgTitle = new MockWgTitle(); $wgTitle->addFullTexts( "Middle page" ); Article::setContent("<navmap-data>TOC page|hiya\n* First page\n* Middle page\n* Last page</navmap-data>"); $render16 = renderNavMap( $renderInput, $renderArgv ); $expected16 = "<div class='navmap-navbox' style='text-align: right;'><small>[[TOC page|hiya]]</small><br/>[[First page|Previous]] - [[TOC page|Up]] - [[Last page|Next]]</div>\n"; assertTrue( "16 TOC page did not return labeled nav box", ($render16 === $expected16) ); # end tests endTests(); ?>