xref: /template/navigator/navigator/autolistNavigator.php (revision 448bae0e7f8fbee3d233094e5c4034b30f5f025b)
1<?php
2/**
3 * Navigator — Auto-Index Renderer
4 * v20260317a
5 * ------------------------------------------------------------
6 * Generates a namespace index on start pages using DokuWiki’s
7 * page index, with sorting options and filesystem sanity checks.
8 *
9 * Purpose
10 * -------
11 * • Provide an automatic list of pages within the same namespace.
12 * • Support multiple sort orders (latest, oldest, alphabetical).
13 * • Avoid stale index entries by verifying pages exist on disk.
14 * • Respect the opt-in marker (~~AUTOINDEX~~ or configured value).
15 *
16 * This script is intentionally self-contained and theme-agnostic.
17 */
18/**
19 * License: MIT (see LICENSE.txt)
20 *
21 * Maintainers are encouraged to document significant changes in
22 * the theme’s README to preserve clarity and continuity.
23 */
24
25
26if (!defined('DOKU_INC')) die();
27global $INFO;
28
29/* -------------------------------------------------------------
30 * Title normalization helper for alphabetical sorting
31 * Removes leading articles (EN/PT) and strips accents.
32 * ----------------------------------------------------------- */
33
34function navigator_normalize_title($title) {
35    // Load article list from plugin configuration
36    $nav = plugin_load('helper', 'navigatorlabels');
37    $articleList = $nav ? $nav->getConf('articles') : '';
38
39    // Soft max length guard (prevents accidental pasting of large text)
40    if (strlen($articleList) > 500) {
41        $articleList = substr($articleList, 0, 500);
42    }
43
44    // Normalize and split into individual articles
45    $articles = array_filter(array_map('trim', explode(',', mb_strtolower($articleList))));
46
47    // Normalize title for comparison
48    $lower = mb_strtolower($title, 'UTF-8');
49
50    // Remove leading whitespace (including NBSP)
51    $lower = preg_replace('/^\s+/u', '', $lower);
52    $title = preg_replace('/^\s+/u', '', $title);
53
54    // Remove leading articles (Unicode-aware)
55    foreach ($articles as $article) {
56        if ($article === '') continue;
57
58        // French elision support: l’, d’, qu’, etc.
59        if (preg_match('/[\'’]$/u', $article)) {
60            // Match article + ANY letter (elision)
61            $pattern = '/^' . preg_quote($article, '/') . '\p{L}/u';
62            if (preg_match($pattern, $lower)) {
63                $title = mb_substr($title, mb_strlen($article), null, 'UTF-8');
64                break;
65            }
66        }
67
68        // Normal article: match article + whitespace or punctuation
69        $pattern = '/^' . preg_quote($article, '/') . '[\s\p{P}]/u';
70        if (preg_match($pattern, $lower)) {
71            $title = mb_substr($title, mb_strlen($article) + 1, null, 'UTF-8');
72            break;
73        }
74    }
75
76    // Transliterate accents for natural sorting
77    $normalized = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $title);
78    return ($normalized !== false) ? $normalized : $title;
79}
80
81
82
83/* -------------------------------------------------------------
84 * Only run on namespace start pages
85 * ----------------------------------------------------------- */
86$cleanID = $INFO['id'];
87if (!preg_match('/\:start$/', $cleanID)) return;
88
89/* -------------------------------------------------------------
90 * Load page content and detect opt-in marker
91 * ----------------------------------------------------------- */
92$pageContent = rawWiki($cleanID);
93$trimmed = trim($pageContent);
94
95// Load marker from helper plugin (Navigator Labels)
96$nav = plugin_load('helper', 'navigatorlabels');
97$marker = $nav ? $nav->getConf('marker_autoindex') : '~~AUTOINDEX~~';
98$hasAutoIndexMarker = (strpos($pageContent, $marker) !== false);
99
100// Placeholder detection (//)
101$isPlaceholder = ($trimmed === '//');
102
103// Determine whether to show auto-index
104$shouldShowAutoIndex =
105    ($trimmed === '') ||
106    $isPlaceholder ||
107    ($trimmed !== '' && $hasAutoIndexMarker);
108
109if (!$shouldShowAutoIndex) return;
110
111/* -------------------------------------------------------------
112 * Determine namespace
113 * ----------------------------------------------------------- */
114$ns = getNS($cleanID);
115
116/* -------------------------------------------------------------
117 * Sort order detection (from query string)
118 * ----------------------------------------------------------- */
119$query = html_entity_decode($_SERVER['QUERY_STRING'] ?? '');
120$order = 'latest';
121
122if (preg_match('/(?:^|&)order=oldest(?:&|$)/', $query)) {
123    $order = 'oldest';
124} elseif (preg_match('/(?:^|&)order=alpha(?:&|$)/', $query)) {
125    $order = 'alpha';
126}
127
128/* -------------------------------------------------------------
129 * Collect pages using DokuWiki’s index
130 * ----------------------------------------------------------- */
131$allPages = idx_getIndex('page', '');
132$entries = [];
133
134foreach ($allPages as $id) {
135    $id = trim($id);
136
137    // Same namespace, but not the start page itself
138    if (getNS($id) === $ns && noNS($id) !== noNS($cleanID)) {
139
140        // Filesystem mtime for robust date sorting
141        $mtime = @filemtime(wikiFN($id));
142
143        // Title for alphabetical sorting
144        $title = p_get_first_heading($id);
145        if (!$title) $title = noNS($id);
146
147        $entries[] = [
148            'id'    => $id,
149            'mtime' => $mtime,
150            'title' => $title
151        ];
152    }
153}
154
155/* -------------------------------------------------------------
156 * Filesystem sanity filter
157 * -------------------------------------------------------------
158 * DokuWiki’s index may contain stale entries for deleted pages.
159 * We ensure only pages that physically exist on disk are listed.
160 * ----------------------------------------------------------- */
161$entries = array_filter($entries, function($entry) {
162    return file_exists(wikiFN($entry['id']));
163});
164
165/* -------------------------------------------------------------
166 * Sorting logic
167 * ----------------------------------------------------------- */
168if ($order === 'latest') {
169    usort($entries, function($a, $b) {
170        return $b['mtime'] <=> $a['mtime']; // newest first
171    });
172} elseif ($order === 'oldest') {
173    usort($entries, function($a, $b) {
174        return $a['mtime'] <=> $b['mtime']; // oldest first
175    });
176} elseif ($order === 'alpha') {
177    usort($entries, function($a, $b) {
178        $ta = navigator_normalize_title($a['title']);
179        $tb = navigator_normalize_title($b['title']);
180        return strnatcasecmp($ta, $tb);
181    });
182}
183
184/* -------------------------------------------------------------
185 * Render list
186 * ----------------------------------------------------------- */
187echo '<ul class="navigator-autolist-list">';
188
189foreach ($entries as $entry) {
190    $id = $entry['id'];
191    $title = $entry['title'];
192    $url = wl($id);
193
194    echo '<li class="navigator-autolist-item">';
195    echo '<a href="' . $url . '">' . hsc($title) . '</a>';
196    echo '</li>';
197}
198
199echo '</ul>';
200?>
201