|
| 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 | +} |
0 commit comments