Skip to content

Commit 98cad17

Browse files
committed
Add sorting hook for dw2pdf plugin
1 parent 5113845 commit 98cad17

File tree

3 files changed

+379
-0
lines changed

3 files changed

+379
-0
lines changed

_test/Dw2PdfTest.php

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<?php
2+
3+
namespace dokuwiki\plugin\indexmenu\test;
4+
5+
use DokuWikiTest;
6+
7+
/**
8+
* dw2pdf tests for the indexmenu plugin
9+
*
10+
* @group plugin_indexmenu
11+
* @group plugins
12+
*/
13+
class Dw2PdfTest extends DokuWikiTest
14+
{
15+
/** @inheritdoc **/
16+
protected $pluginsEnabled = ['indexmenu'];
17+
18+
/**
19+
* Test sorting of pages in dw2pdf export
20+
*
21+
* @return void
22+
* @throws \ReflectionException
23+
*/
24+
public function testSorting()
25+
{
26+
$pages = [
27+
[
28+
'id' => 'en:admin-guides:latest_release:getting_started:actions:start',
29+
'rev' => 1769524502,
30+
'mtime' => 1769524502,
31+
'size' => 2930,
32+
],
33+
[
34+
'id' => 'en:admin-guides:latest_release:getting_started:display_the_id_of_column_and_set:start',
35+
'rev' => 1769524502,
36+
'mtime' => 1769524502,
37+
'size' => 809,
38+
],
39+
[
40+
'id' => 'en:admin-guides:latest_release:getting_started:help_search:start',
41+
'rev' => 1769524502,
42+
'mtime' => 1769524502,
43+
'size' => 404,
44+
],
45+
[
46+
'id' => 'en:admin-guides:latest_release:getting_started:hot_keys:start',
47+
'rev' => 1769524502,
48+
'mtime' => 1769524502,
49+
'size' => 2717,
50+
],
51+
[
52+
'id' => 'en:admin-guides:latest_release:getting_started:how_to_navigate_in_datawalk_system:external_links:start',
53+
'rev' => 1769524502,
54+
'mtime' => 1769524502,
55+
'size' => 986,
56+
],
57+
[
58+
'id' => 'en:admin-guides:latest_release:getting_started:how_to_navigate_in_datawalk_system:start_page:start',
59+
'rev' => 1769524502,
60+
'mtime' => 1769524502,
61+
'size' => 978,
62+
],
63+
[
64+
'id' => 'en:admin-guides:latest_release:getting_started:how_to_navigate_in_datawalk_system:start',
65+
'rev' => 1769524502,
66+
'mtime' => 1769524502,
67+
'size' => 958,
68+
],
69+
[
70+
'id' => 'en:admin-guides:latest_release:getting_started:integration-with-external-systems-by-injecting-javascript:start',
71+
'rev' => 1769524502,
72+
'mtime' => 1769524502,
73+
'size' => 1766,
74+
],
75+
[
76+
'id' => 'en:admin-guides:latest_release:getting_started:system_architecture:production_sizing_page',
77+
'rev' => 1769524502,
78+
'mtime' => 1769524502,
79+
'size' => 9805,
80+
],
81+
[
82+
'id' => 'en:admin-guides:latest_release:getting_started:system_architecture:start',
83+
'rev' => 1769524502,
84+
'mtime' => 1769524502,
85+
'size' => 14474,
86+
],
87+
[
88+
'id' => 'en:admin-guides:latest_release:getting_started:workstation:start',
89+
'rev' => 1769524502,
90+
'mtime' => 1769524502,
91+
'size' => 1381,
92+
],
93+
[
94+
'id' => 'en:admin-guides:latest_release:getting_started:start',
95+
'rev' => 1769524502,
96+
'mtime' => 1769524502,
97+
'size' => 4469,
98+
],
99+
];
100+
101+
$tags = [
102+
'en:admin-guides:latest_release:getting_started:actions:start' => 10,
103+
'en:admin-guides:latest_release:getting_started:display_the_id_of_column_and_set:start' => 9,
104+
'en:admin-guides:latest_release:getting_started:help_search:start' => 3,
105+
'en:admin-guides:latest_release:getting_started:hot_keys:start' => 9,
106+
'en:admin-guides:latest_release:getting_started:how_to_navigate_in_datawalk_system:external_links:start' => 2,
107+
'en:admin-guides:latest_release:getting_started:how_to_navigate_in_datawalk_system:start_page:start' => 1,
108+
'en:admin-guides:latest_release:getting_started:how_to_navigate_in_datawalk_system:start' => 2,
109+
'en:admin-guides:latest_release:getting_started:integration-with-external-systems-by-injecting-javascript:start' => 210,
110+
'en:admin-guides:latest_release:getting_started:system_architecture:production_sizing_page' => 1,
111+
'en:admin-guides:latest_release:getting_started:system_architecture:start' => 6,
112+
'en:admin-guides:latest_release:getting_started:workstation:start' => 1,
113+
'en:admin-guides:latest_release:getting_started:start' => 1,
114+
];
115+
116+
$expected = [
117+
[
118+
'id' => 'en:admin-guides:latest_release:getting_started:start',
119+
'rev' => 1769524502,
120+
'mtime' => 1769524502,
121+
'size' => 4469,
122+
],
123+
[
124+
'id' => 'en:admin-guides:latest_release:getting_started:workstation:start',
125+
'rev' => 1769524502,
126+
'mtime' => 1769524502,
127+
'size' => 1381,
128+
],
129+
[
130+
'id' => 'en:admin-guides:latest_release:getting_started:how_to_navigate_in_datawalk_system:start',
131+
'rev' => 1769524502,
132+
'mtime' => 1769524502,
133+
'size' => 958,
134+
],
135+
[
136+
'id' => 'en:admin-guides:latest_release:getting_started:how_to_navigate_in_datawalk_system:start_page:start',
137+
'rev' => 1769524502,
138+
'mtime' => 1769524502,
139+
'size' => 978,
140+
],
141+
[
142+
'id' => 'en:admin-guides:latest_release:getting_started:how_to_navigate_in_datawalk_system:external_links:start',
143+
'rev' => 1769524502,
144+
'mtime' => 1769524502,
145+
'size' => 986,
146+
],
147+
[
148+
'id' => 'en:admin-guides:latest_release:getting_started:help_search:start',
149+
'rev' => 1769524502,
150+
'mtime' => 1769524502,
151+
'size' => 404,
152+
],
153+
[
154+
'id' => 'en:admin-guides:latest_release:getting_started:system_architecture:start',
155+
'rev' => 1769524502,
156+
'mtime' => 1769524502,
157+
'size' => 14474,
158+
],
159+
[
160+
'id' => 'en:admin-guides:latest_release:getting_started:system_architecture:production_sizing_page',
161+
'rev' => 1769524502,
162+
'mtime' => 1769524502,
163+
'size' => 9805,
164+
],
165+
[
166+
'id' => 'en:admin-guides:latest_release:getting_started:display_the_id_of_column_and_set:start',
167+
'rev' => 1769524502,
168+
'mtime' => 1769524502,
169+
'size' => 809,
170+
],
171+
[
172+
'id' => 'en:admin-guides:latest_release:getting_started:hot_keys:start',
173+
'rev' => 1769524502,
174+
'mtime' => 1769524502,
175+
'size' => 2717,
176+
],
177+
[
178+
'id' => 'en:admin-guides:latest_release:getting_started:actions:start',
179+
'rev' => 1769524502,
180+
'mtime' => 1769524502,
181+
'size' => 2930,
182+
],
183+
[
184+
'id' => 'en:admin-guides:latest_release:getting_started:integration-with-external-systems-by-injecting-javascript:start',
185+
'rev' => 1769524502,
186+
'mtime' => 1769524502,
187+
'size' => 1766,
188+
],
189+
];
190+
191+
$action = plugin_load('action', 'indexmenu_dw2pdf');
192+
self::setInaccessibleProperty($action, 'tags', $tags);
193+
194+
195+
usort($pages, [$action, 'cbIndexmenuSort']);
196+
197+
$this->assertEquals($expected, $pages);
198+
}
199+
}

action/dw2pdf.php

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
use dokuwiki\Extension\ActionPlugin;
4+
use dokuwiki\Extension\Event;
5+
use dokuwiki\Extension\EventHandler;
6+
7+
/**
8+
* Class action_plugin_indexmenu_dw2pdf
9+
*/
10+
class action_plugin_indexmenu_dw2pdf extends ActionPlugin
11+
{
12+
/**
13+
* @var bool Strict mode requires a tag for all sorted pages
14+
*/
15+
protected bool $isStrictMode;
16+
17+
/**
18+
* @var array Page ids and their corresponding ordering tags
19+
*/
20+
protected array $tags = [];
21+
22+
/**
23+
* @param EventHandler $controller DokuWiki's event controller object.
24+
*/
25+
public function register(EventHandler $controller)
26+
{
27+
$controller->register_hook('DW2PDF_NAMESPACEEXPORT_SORT', 'BEFORE', $this, 'sortPages2Pdf');
28+
}
29+
30+
/**
31+
* Triggered by dw2pdf plugin.
32+
* Custom sorting of pages if "book_order" parameter was set to "indexmenu" or "indexmenu_strict"
33+
*
34+
* @param Event $event
35+
* @return true
36+
*/
37+
public function sortPages2Pdf(Event $event)
38+
{
39+
$sort = $event->data['sort'];
40+
if ($sort === 'indexmenu' || $sort === 'indexmenu_strict') {
41+
$this->isStrictMode = str_contains($sort, 'strict');
42+
$event->preventDefault();
43+
44+
$pages =& $event->data['pages'];
45+
46+
// extract tags for easier testing
47+
foreach ($pages as $page) {
48+
$this->tags[$page['id']] = p_get_metadata($page['id']);
49+
// break early in strict mode if tags are missing
50+
if ($this->isStrictMode && is_null($this->tags[$page['id']])) {
51+
throw new Exception('Page ' . $page['id'] . ' does not exist or does not have an indexmenu tag!');
52+
}
53+
}
54+
55+
usort($pages, [$this, 'cbIndexmenuSort']);
56+
}
57+
58+
return true;
59+
}
60+
61+
/**
62+
* usort callback to sort by indexmenu tag.
63+
* Whole namespaces are sorted: start pages define the sort order on the same ns level.
64+
*
65+
* @param array $a Page info in event data
66+
* @param array $b Page info in event data
67+
* @return int
68+
*/
69+
public function cbIndexmenuSort($a, $b)
70+
{
71+
global $conf;
72+
73+
$partsA = explode(':', $a['id']);
74+
$partsB = explode(':', $b['id']);
75+
76+
//find where the namespaces diverge
77+
[$nsA, $nsB] = $this->getFirstDifferentNs($partsA, $partsB);
78+
79+
// pages in the same namespace
80+
if (is_null($nsA) && is_null($nsB)) {
81+
// start page always has priority
82+
if ($partsA[count($partsA) - 1] === $conf['start']) return -1;
83+
if ($partsB[count($partsB) - 1] === $conf['start']) return 1;
84+
85+
// otherwise compare via indexmenu tag
86+
return $this->tagCompare($a['id'], $b['id']);
87+
}
88+
89+
// one of the pages is in a sub-namespace
90+
if (is_null($nsA)) return -1;
91+
if (is_null($nsB)) return 1;
92+
93+
// different namespaces, so first resolve the page holding the actual sorting order for this level
94+
$idA = $this->resolveSortingAnchor($partsA, $nsA);
95+
$idB = $this->resolveSortingAnchor($partsB, $nsB);
96+
97+
return $this->tagCompare($idA, $idB);
98+
}
99+
100+
/**
101+
* Compare ids based on indexmenu tag. If tags are missing or equal, do string comparison.
102+
*
103+
* @param string $idA
104+
* @param string $idB
105+
* @return int
106+
*/
107+
public function tagCompare($idA, $idB)
108+
{
109+
$indexA = $this->tags[$idA];
110+
$indexB = $this->tags[$idB];
111+
112+
if (is_null($indexA) || is_null($indexB) || $indexA === $indexB) {
113+
return strnatcmp($idA, $idB);
114+
}
115+
116+
return $indexA <=> $indexB;
117+
}
118+
119+
/**
120+
* Returns first different namespaces when comparing two arrays of namespace parts
121+
*
122+
* @param array $a Full id exploded by ":"
123+
* @param array $b Full id exploded by ":"
124+
* @return array
125+
*/
126+
public function getFirstDifferentNs($a, $b)
127+
{
128+
$countA = count($a);
129+
$countB = count($b);
130+
$max = max($countA, $countB);
131+
132+
for ($i = 0; $i < $max - 1; $i++) {
133+
$partA = $a[$i] ?: null;
134+
$partB = $b[$i] ?: null;
135+
136+
if ($i === $countA - 1) {
137+
return [null, $partB];
138+
}
139+
140+
if ($i === $countB - 1) {
141+
return [$partA, null];
142+
}
143+
144+
if ($partA !== $partB) {
145+
return [$partA, $partB];
146+
}
147+
}
148+
149+
return [null, null];
150+
}
151+
152+
/**
153+
* Resolve the id of the page that is relevant for sorting (anchor).
154+
* When comparing pages in different namespaces, it is necessary to reference the start page,
155+
* because tags are used for sorting ACROSS and WITHIN namespaces.
156+
*
157+
* @param array $parts Full id exploded by ":"
158+
* @param string $ns
159+
* @return string
160+
*/
161+
public function resolveSortingAnchor($parts, $ns)
162+
{
163+
global $conf;
164+
165+
$path_parts = [];
166+
foreach ($parts as $part) {
167+
$path_parts[] = $part;
168+
169+
// we hit the target namespace, append the start page and return immediately
170+
if ($part === $ns) {
171+
$path_parts[] = $conf["start"];
172+
return implode(':', $path_parts);
173+
}
174+
}
175+
176+
// fallback
177+
return implode(':', $parts);
178+
}
179+
}

lang/en/lang.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
$lang['noupdates'] = 'Indexmenu does not need to be update. You have already the last release:';
2727
$lang['infos'] = 'You can create your theme following the instructions at the <a href="https://www.dokuwiki.org/plugin:indexmenu#theme_tutorial">Theme Tutorial</a> page. <br />Then you could make more people happy :-) sending it to the public indexmenu repository, with the "share" button under that theme.';
2828
$lang['showsort'] = 'Indexmenu sort number: ';
29+
$lang['pdf_sort_err'] = 'Page %s does not have a required indexmenu tag';
2930
$lang['donation_text'] = 'The indexmenu plugin is not sponsored by anyone but i develop and support it for free during my spare time. If you gain something thanks to it or you want to support its development, you can consider to make a donation.';
3031
$lang['js']['indexmenuwizard'] = 'Indexmenu Wizard';
3132
$lang['js']['index'] = 'Index';

0 commit comments

Comments
 (0)