Repository navigation
Expand file tree
/
Copy pathNavigationGraph.cs
More file actions
146 lines (129 loc) · 5.25 KB
/
Copy pathNavigationGraph.cs
File metadata and controls
146 lines (129 loc) · 5.25 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
namespace ShellDocs.Core;
public class NavigationGraph
{
public NavigationNode Root { get; }
private readonly Dictionary<string, NavigationNode> _byUrl;
private readonly List<NavigationNode> _flatPages;
private readonly List<NavigationNode> _hiddenRoots = new();
public NavigationGraph(NavigationNode root, IEnumerable<NavigationNode>? hiddenPages = null)
{
Root = root;
_byUrl = new Dictionary<string, NavigationNode>(StringComparer.OrdinalIgnoreCase);
_flatPages = new List<NavigationNode>();
Index(root);
// Hidden pages resolve by URL but stay out of _flatPages, so prev/next skips them.
if (hiddenPages is not null)
{
foreach (var page in hiddenPages)
{
_hiddenRoots.Add(page);
IndexHidden(page);
}
}
}
private void IndexHidden(NavigationNode node)
{
if (node.Kind == NodeKind.Page && !string.IsNullOrEmpty(node.Url))
{
_byUrl[Normalize(node.Url)] = node;
}
foreach (var child in node.Children) IndexHidden(child);
}
public NavigationNode? ResolveByUrl(string url)
{
var key = Normalize(url);
return _byUrl.TryGetValue(key, out var node) ? node : null;
}
public (NavigationNode? Prev, NavigationNode? Next) GetPrevNext(NavigationNode node)
{
var i = _flatPages.IndexOf(node);
if (i < 0) return (null, null);
var prev = i > 0 ? _flatPages[i - 1] : null;
var next = i < _flatPages.Count - 1 ? _flatPages[i + 1] : null;
return (prev, next);
}
// Nearest neighbours `inScope` accepts, skipping the rest (keeps prev/next inside a version).
public (NavigationNode? Prev, NavigationNode? Next) GetPrevNext(NavigationNode node, Func<NavigationNode, bool> inScope)
{
var i = _flatPages.IndexOf(node);
if (i < 0) return (null, null);
NavigationNode? prev = null, next = null;
for (var j = i - 1; j >= 0; j--)
{
if (inScope(_flatPages[j])) { prev = _flatPages[j]; break; }
}
for (var j = i + 1; j < _flatPages.Count; j++)
{
if (inScope(_flatPages[j])) { next = _flatPages[j]; break; }
}
return (prev, next);
}
// Section node for the content folder at `folderUrl`, searching hidden folders too.
// Null for graphs built without on-disk paths.
public NavigationNode? FindFolder(string folderUrl)
{
if (string.IsNullOrEmpty(Root.Path)) return null;
var target = UrlPath.Normalize(folderUrl);
foreach (var n in FlattenFrom(Root).Concat(_hiddenRoots.SelectMany(FlattenFrom)))
{
if (n.Kind != NodeKind.Section || n == Root || string.IsNullOrEmpty(n.Path)) continue;
var rel = System.IO.Path.GetRelativePath(Root.Path, n.Path).Replace('\\', '/');
if (rel.StartsWith("..", StringComparison.Ordinal)) continue;
if (string.Equals(UrlPath.Normalize(rel), target, StringComparison.OrdinalIgnoreCase)) return n;
}
return null;
}
// First visible page (tree order) whose URL is `urlPrefix` or below it.
public NavigationNode? FirstPageUnder(string urlPrefix)
=> _flatPages.FirstOrDefault(p => UrlPath.IsUnder(p.Url, urlPrefix));
public NavigationNode? FirstPageUnder(string urlPrefix, Func<NavigationNode, bool> predicate)
=> _flatPages.FirstOrDefault(p => UrlPath.IsUnder(p.Url, urlPrefix) && predicate(p));
// URL of every visible content folder below the root. Empty for graphs built without on-disk paths.
public IEnumerable<(string Url, NavigationNode Node)> Folders()
{
if (string.IsNullOrEmpty(Root.Path)) yield break;
foreach (var n in FlattenFrom(Root))
{
if (n.Kind != NodeKind.Section || n == Root || string.IsNullOrEmpty(n.Path)) continue;
var rel = System.IO.Path.GetRelativePath(Root.Path, n.Path).Replace('\\', '/');
if (rel.StartsWith("..", StringComparison.Ordinal)) continue;
yield return (UrlPath.Normalize(rel), n);
}
}
public IReadOnlyList<NavigationNode> GetBreadcrumb(NavigationNode node)
{
var chain = new List<NavigationNode>();
var current = node;
while (current is not null && current != Root)
{
chain.Add(current);
current = current.Parent;
}
chain.Reverse();
return chain;
}
public IEnumerable<NavigationNode> Flatten()
{
return FlattenFrom(Root);
}
// Visible + hidden pages; static prerender needs both.
public IEnumerable<string> AllUrls => _byUrl.Keys;
private static IEnumerable<NavigationNode> FlattenFrom(NavigationNode node)
{
yield return node;
foreach (var child in node.Children)
{
foreach (var n in FlattenFrom(child)) yield return n;
}
}
private void Index(NavigationNode node)
{
if (node.Kind == NodeKind.Page && !string.IsNullOrEmpty(node.Url))
{
_byUrl[Normalize(node.Url)] = node;
_flatPages.Add(node);
}
foreach (var child in node.Children) Index(child);
}
private static string Normalize(string url) => UrlPath.Normalize(url);
}