-
-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathDocumentationRepository.php
More file actions
353 lines (310 loc) · 11.5 KB
/
Copy pathDocumentationRepository.php
File metadata and controls
353 lines (310 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
<?php
/*
* UserFrosting Learn (http://www.userfrosting.com)
*
* @link https://github.com/userfrosting/Learn
* @copyright Copyright (c) 2025 Alexander Weissman & Louis Charette
* @license https://github.com/userfrosting/Learn/blob/main/LICENSE.md (MIT License)
*/
namespace UserFrosting\Learn\Documentation;
use Illuminate\Cache\Repository as Cache;
use UserFrosting\Config\Config;
use UserFrosting\Sprinkle\Core\Util\RouteParserInterface;
use UserFrosting\UniformResourceLocator\ResourceInterface;
use UserFrosting\UniformResourceLocator\ResourceLocatorInterface;
/**
* Repository for managing documentation pages across different versions.
*
* Handles retrieval, caching, and organization of documentation content
* from various sources using the resource locator system.
*/
class DocumentationRepository
{
public function __construct(
protected ResourceLocatorInterface $locator,
protected VersionValidator $versionValidator,
protected PageFactory $pageFactory,
protected Config $config,
protected Cache $cache,
protected RouteParserInterface $router,
) {
}
/**
* Return the documentation tree for a given version, using cache if enabled.
*
* @param string|null $version
*
* @return PageResource[]
*/
public function getTree(?string $version = null): array
{
return $this->cache->remember(
$this->getCacheKey('tree', $version ?? 'latest'),
$this->getCacheTtl(),
function () use ($version) {
// Get version object (throws exception if invalid)
$versionObj = $this->versionValidator->getVersion($version);
// Get all files for the version
$pages = $this->getPages($versionObj);
// Sort the files by relative path
usort($pages, fn ($a, $b) => strcmp($a->getBasePath(), $b->getBasePath()));
// Transform the flat list of pages into a tree structure
return $this->getPagesChildren($pages);
}
);
}
/**
* Return the list of all variables version, with the link to the provided
* page alternate versions. Uses cache if enabled.
*
* @param string $path
*
* @return array<string, string> Array containing Label => Route
*/
public function getAlternateVersions(string $path): array
{
return $this->cache->remember(
$this->getCacheKey('versions', $path),
$this->getCacheTtl(),
function () use ($path) {
$available = $this->config->get('learn.versions.available', []);
// $available contains a list of version => name
// We need for the dropdown to have name => path
return array_map(
fn ($v) => $this->router->urlFor('documentation.versioned', [
'version' => $v,
'path' => $path,
]),
array_flip($available)
);
}
);
}
/**
* Return children pages for a given parent slug.
*
* @param PageResource[] $pages
* @param string $parentSlug
*
* @return PageResource[]
*/
protected function getPagesChildren(array $pages, string $parentSlug = ''): array
{
// Get all pages that have this parent
$children = array_filter($pages, fn ($p) => $p->getParentSlug() === $parentSlug);
// For each child, get its own children
foreach ($children as $child) {
$child->setChildren($this->getPagesChildren($pages, $child->getSlug()));
}
// Reset keys
$children = array_values($children);
return $children;
}
/**
* Get a single page by version and slug.
*
* @param string $slug
* @param string|null $version
*
*
* @throws PageNotFoundException
* @return PageResource
*/
public function getPage(string $slug, ?string $version = null): PageResource
{
return $this->cache->remember(
$this->getCacheKey('page', $slug . ($version ?? 'latest')),
$this->getCacheTtl(),
function () use ($slug, $version) {
// Get version object (throws exception if invalid)
$versionObj = $this->versionValidator->getVersion($version);
// Get all pages for the version
$pages = $this->getPages($versionObj);
// If page slug is empty, we want the "home" page, aka the first page found
if ($slug === '') {
return array_key_first($pages) !== null
? $pages[array_key_first($pages)]
: throw new PageNotFoundException("Page not found: (version: {$versionObj->id})");
}
// Find the page with the matching slug
foreach ($pages as $page) {
if ($page->getSlug() === $slug) {
return $page;
}
}
throw new PageNotFoundException("Page not found: {$slug} (version: {$versionObj->id})");
}
);
}
/**
* Get a list of all the files found across all active sprinkles
*
* @return PageResource[] An array of absolute paths
*/
protected function getPages(Version $version): array
{
return $this->cache->remember(
$this->getCacheKey('pages', $version->id),
$this->getCacheTtl(),
function () use ($version) {
// Get all pages
$resources = $this->locator->listResources("pages://{$version->id}/");
// Keep only markdown files
$resources = array_filter(
$resources,
fn (ResourceInterface $res) => $res->getExtension() === 'md'
);
// Convert each to our custom "PageResource" objects using the factory
return array_map(
fn (ResourceInterface $res) => $this->pageFactory->createFromResource($version, $res),
$resources
);
}
);
}
/**
* Get the breadcrumbs for a given page.
*
* @param PageResource $page
*
* @return array<int, array{label: string, url: string}>
*/
public function getBreadcrumbsForPage(PageResource $page): array
{
$breadcrumbs = [];
$current = $page;
// Build breadcrumbs from current page up to root
while ($current !== null) {
array_unshift($breadcrumbs, [
'label' => $current->getTitle(),
'url' => $current->getRoute(),
]);
$parentSlug = $current->getParentSlug();
$current = $parentSlug === '' ? null : $this->getPage($parentSlug, $current->getVersion()->id);
}
// Add home link at the start
array_unshift($breadcrumbs, [
'label' => 'Home',
'url' => $page->getVersion()->latest ?
$this->router->urlFor('documentation', [
'path' => ''
]) :
$this->router->urlFor('documentation.versioned', [
'path' => '',
'version' => $page->getVersion()->id,
]),
]);
return $breadcrumbs;
}
/**
* Get the next page in the documentation tree after the given page.
*
* Traverses the documentation tree in depth-first order to determine
* the next page. Returns null if the given page is the last page.
*
* @param PageResource $page The current page
*
* @return PageResource|null The next page, or null if at the end
*/
public function getNextPageForPage(PageResource $page): ?PageResource
{
return $this->getAdjacentPage($page, 1);
}
/**
* Get the previous page in the documentation tree before the given page.
*
* Traverses the documentation tree in depth-first order to determine
* the previous page. Returns null if the given page is the first page.
*
* @param PageResource $page The current page
*
* @return PageResource|null The previous page, or null if at the beginning
*/
public function getPreviousPageForPage(PageResource $page): ?PageResource
{
return $this->getAdjacentPage($page, -1);
}
/**
* Get an adjacent page in the documentation tree relative to the given page.
*
* @param PageResource $page The current page
* @param int $offset The offset (+1 for next, -1 for previous)
*
* @return PageResource|null The adjacent page, or null if not found
*/
protected function getAdjacentPage(PageResource $page, int $offset): ?PageResource
{
$flatPages = $this->getFlattenedTree($page->getVersion()->id);
$slugs = array_keys($flatPages);
$currentIndex = array_search($page->getSlug(), $slugs, true);
if ($currentIndex === false || !isset($slugs[$currentIndex + $offset])) {
return null;
}
return $flatPages[$slugs[$currentIndex + $offset]];
}
/**
* Get a flattened list of pages in tree order (depth-first traversal).
* Uses page slugs as array keys for efficient lookups.
*
* @param string|null $version
*
* @return array<string, PageResource> Array keyed by page slug
*/
public function getFlattenedTree(?string $version = null): array
{
$tree = $this->getTree($version);
$flat = [];
$flatten = function (array $pages) use (&$flatten, &$flat) {
foreach ($pages as $page) {
$flat[$page->getSlug()] = $page;
if ($page->getChildren()) {
$flatten($page->getChildren());
}
}
};
$flatten($tree);
return $flat;
}
/**
* Get a versioned image resource by path and version.
*
* @param string $version The version (empty string for latest)
* @param string $path The image path
*
* @throws PageNotFoundException If the image is not found
* @return ResourceInterface
*/
public function getVersionedImage(string $version, string $path): ResourceInterface
{
// Get version object (throws exception if invalid)
$versionObj = $this->versionValidator->getVersion($version === '' ? null : $version);
// Try to get the versioned image resource first
$resource = $this->locator->getResource("pages://{$versionObj->id}/images/{$path}");
if ($resource === null) {
throw new PageNotFoundException("Image not found: {$path} (version: {$versionObj->id})");
}
return $resource;
}
/**
* Get the cache key for documentation items.
*
* @param string $type The type of item (e.g., 'tree', 'page', 'versions', 'pages')
* @param string $identifier The identifier for the item (e.g., version, slug)
*
* @return string The cache key
*/
protected function getCacheKey(string $type, string $identifier): string
{
$keyFormat = $this->config->get('learn.cache.key', '%s.%s');
return sprintf($keyFormat, $type, $identifier);
}
/**
* Get the cache TTL for documentation items.
*
* @return int The cache TTL in seconds
*/
protected function getCacheTtl(): int
{
return $this->config->get('learn.cache.ttl', 3600);
}
}