Skip to content

Commit eb735f8

Browse files
committed
fix test
1 parent 298b6af commit eb735f8

File tree

3 files changed

+309
-2
lines changed

3 files changed

+309
-2
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Functional Principles
2+
3+
These principles define what the navigation system does and why.
4+
5+
> **Also see:** [Technical Principles](technical-principles.md) for implementation details.
6+
7+
## 1. Two-Phase Loading
8+
9+
Navigation construction follows a strict two-phase approach:
10+
11+
**Phase 1: Configuration Resolution** (`Elastic.Documentation.Configuration`)
12+
- Parse YAML files (`docset.yml`, `toc.yml`, `navigation.yml`)
13+
- Resolve all file references to **full paths** relative to documentation set root
14+
- Validate configuration structure and relationships
15+
- Output: Fully resolved configuration objects with complete file paths
16+
17+
**Phase 2: Navigation Construction** (`Elastic.Documentation.Navigation`)
18+
- Consume resolved configuration from Phase 1
19+
- Build navigation tree with **full URLs**
20+
- Create node relationships (parent/child/root)
21+
- Set up home providers for URL calculation
22+
- Output: Complete navigation tree with calculated URLs
23+
24+
**Why Two Phases?**
25+
- **Separation of Concerns**: Configuration parsing is independent of navigation structure
26+
- **Validation**: Catch file/structure errors before building expensive navigation trees
27+
- **Reusability**: Same configuration can build different navigation structures (isolated vs assembler)
28+
- **Performance**: Resolve file system operations once, reuse for navigation
29+
30+
> See [Two-Phase Loading](two-phase-loading.md) for detailed explanation.
31+
32+
## 2. Single Documentation Source
33+
34+
URLs are always built relative to the documentation set's source directory:
35+
- Files referenced in `docset.yml` are relative to the docset root
36+
- Files referenced in nested `toc.yml` are relative to the toc directory
37+
- During Phase 1, all paths are resolved to be relative to the docset root
38+
- During Phase 2, URLs are calculated from these resolved paths
39+
40+
**Example:**
41+
```
42+
docs/
43+
├── docset.yml # Root
44+
├── index.md
45+
└── api/
46+
├── toc.yml # Nested TOC
47+
└── rest.md
48+
```
49+
50+
Phase 1 resolves `api/toc.yml` reference to `rest.md` as: `api/rest.md` (relative to docset root)
51+
Phase 2 builds URL as: `/api/rest/`
52+
53+
## 3. URL Building is Dynamic and Cheap
54+
55+
URLs are **calculated on-demand**, not stored:
56+
- Nodes don't store their final URL
57+
- URLs are computed from `HomeProvider.PathPrefix` + relative path
58+
- Changing a `HomeProvider` instantly updates all descendant URLs
59+
- No tree traversal needed to update URLs
60+
61+
**Why Dynamic?**
62+
- **Re-homing**: Same subtree can have different URLs in different contexts
63+
- **Memory Efficient**: Don't store redundant URL strings
64+
- **Consistency**: URLs always reflect current home provider state
65+
66+
> See [Home Provider Architecture](home-provider-architecture.md) for implementation details.
67+
68+
## 4. Navigation Roots Can Be Re-homed
69+
70+
A key design feature that enables assembler builds:
71+
- **Isolated Build**: Each `DocumentationSetNavigation` is its own root
72+
- **Assembler Build**: `SiteNavigation` becomes the root, docsets are "re-homed"
73+
- **Re-homing**: Replace a subtree's `HomeProvider` to change its URL prefix
74+
- **Cheap Operation**: O(1) - just replace the provider reference
75+
76+
**Example:**
77+
```csharp
78+
// Isolated: URLs start at /
79+
homeProvider.PathPrefix = "";
80+
// → /api/rest/
81+
82+
// Assembled: Re-home to /guide
83+
homeProvider = new NavigationHomeProvider("/guide", siteNav);
84+
// → /guide/api/rest/
85+
```
86+
87+
> See [Assembler Process](assembler-process.md) for how re-homing works in practice.
88+
89+
## 5. Navigation Scope via HomeProvider
90+
91+
`INavigationHomeProvider` creates navigation scopes:
92+
- **Provider**: Defines `PathPrefix` and `NavigationRoot` for a scope
93+
- **Accessor**: Children use `INavigationHomeAccessor` to access their scope
94+
- **Inheritance**: Child nodes inherit their parent's accessor
95+
- **Isolation**: Changes to a provider only affect its scope
96+
97+
**Scope Creators:**
98+
- `DocumentationSetNavigation` - Creates scope for entire docset
99+
- `TableOfContentsNavigation` - Creates scope for TOC subtree (enables re-homing)
100+
101+
**Scope Consumers:**
102+
- `FileNavigationLeaf` - Uses accessor to calculate URL
103+
- `FolderNavigation` - Passes accessor to children
104+
- `VirtualFileNavigation` - Passes accessor to children
105+
106+
## 6. Index Files Determine Folder URLs
107+
108+
Every folder/node navigation has an **Index**:
109+
- Index is either `index.md` or the first file
110+
- The node's URL is the same as its Index's URL
111+
- Children appear "under" the index in navigation
112+
- Index files map to folder paths: `/api/index.md``/api/`
113+
114+
**Why?**
115+
- **Consistent URL Structure**: Folders and their indexes share the same URL
116+
- **Natural Navigation**: Index represents the folder's landing page
117+
- **Hierarchical**: Clear parent-child URL relationships
118+
119+
## 7. File Structure Should Mirror Navigation
120+
121+
Best practices for maintainability:
122+
- Navigation structure should follow file system structure
123+
- Avoid deep-linking files from different directories
124+
- Use `folder:` references when possible
125+
- Virtual files should group sibling files, not restructure the tree
126+
127+
**Rationale:**
128+
- **Discoverability**: Developers can find files by following navigation
129+
- **Predictability**: URL structure matches file structure
130+
- **Maintainability**: Moving files in navigation matches moving them on disk
131+
132+
## 8. Acyclic Graph Structure
133+
134+
The navigation forms a **directed acyclic graph (DAG)**:
135+
- **Tree Structure**: Each node has exactly one parent (except root)
136+
- **No Cycles**: Following parent pointers always terminates at root
137+
- **Single Root**: Every node has a `NavigationRoot` pointing to the ultimate ancestor
138+
- **Predictable Traversal**: Tree structure enables efficient queries and traversal
139+
140+
**Why This Matters:**
141+
- **URL Uniqueness**: Tree structure ensures each file has one canonical URL
142+
- **Consistent Hierarchy**: Clear parent-child relationships for breadcrumbs and navigation
143+
- **Efficient Queries**: Can traverse up (to root) or down (to leaves) without cycle detection
144+
- **Re-homing Safety**: Replacing a subtree's root doesn't create cycles
145+
146+
**Invariants:**
147+
1. Following `.Parent` chain always reaches root (or null for root)
148+
2. Following `.NavigationRoot` immediately reaches ultimate root
149+
3. No node can be its own ancestor
150+
4. Every node appears exactly once in the tree
151+
152+
## 9. Phantom Nodes for Incomplete Navigation
153+
154+
`navigation.yml` can declare phantoms:
155+
```yaml
156+
phantoms:
157+
- source: plugins://
158+
```
159+
160+
**Purpose:**
161+
- Reference nodes that exist but aren't included in site navigation
162+
- Prevent "undeclared navigation" warnings
163+
- Document intentionally excluded content
164+
- Enable validation of cross-links
165+
166+
---
167+
168+
## Key Invariants
169+
170+
1. **Phase Order**: Configuration must be fully resolved before navigation construction
171+
2. **Path Resolution**: All paths in configuration are relative to docset root after Phase 1
172+
3. **URL Uniqueness**: Every navigation item must have a unique URL within its site
173+
4. **Root Consistency**: All nodes in a subtree point to the same `NavigationRoot`
174+
5. **Provider Validity**: A node's `HomeProvider` must be an ancestor in the tree
175+
6. **Index Requirement**: All node navigations (folder/toc/docset) must have an Index
176+
7. **Path Prefix Uniqueness**: In assembler builds, all `path_prefix` values must be unique
177+
178+
## Performance Characteristics
179+
180+
- **Tree Construction**: O(n) where n = number of files
181+
- **URL Calculation**: O(depth) for first access, O(1) with caching
182+
- **Re-homing**: O(1) - just replace HomeProvider reference
183+
- **Tree Traversal**: O(n) for full tree, but rarely needed
184+
- **Memory**: O(n) for nodes, URLs computed on-demand
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Technical Principles
2+
3+
These principles define how the navigation system is implemented.
4+
5+
> **Prerequisites:** Read [Functional Principles](functional-principles.md) first to understand what the system does and why.
6+
7+
## 1. Generic Type System for Covariance
8+
9+
Navigation classes are generic over `TModel`:
10+
```csharp
11+
public class DocumentationSetNavigation<TModel>
12+
where TModel : class, IDocumentationFile
13+
```
14+
15+
**Why Generic?**
16+
17+
**Covariance Enables Static Typing:**
18+
```csharp
19+
// Without covariance: always get base interface, requires runtime casts
20+
INodeNavigationItem<INavigationModel, INavigationItem> node = GetNode();
21+
if (node.Model is MarkdownFile markdown) // Runtime check required
22+
{
23+
var content = markdown.Content;
24+
}
25+
26+
// With covariance: query for specific type statically
27+
INodeNavigationItem<MarkdownFile, INavigationItem> node = QueryForMarkdownNodes();
28+
var content = node.Model.Content; // ✓ No cast needed! Static type safety
29+
```
30+
31+
**Benefits:**
32+
- **Type Safety**: Query methods can return specific types like `INodeNavigationItem<MarkdownFile, INavigationItem>`
33+
- **No Runtime Casts**: Access `.Model.Content` directly without casting
34+
- **Compile-Time Errors**: Type mismatches caught during compilation, not runtime
35+
- **Better IntelliSense**: IDEs show correct members for specific model types
36+
- **Flexibility**: Same navigation code works with different file models (MarkdownFile, ApiDocFile, etc.)
37+
38+
**Example:**
39+
```csharp
40+
// Query for nodes with specific model type
41+
var markdownNodes = navigation.NavigationItems
42+
.OfType<INodeNavigationItem<MarkdownFile, INavigationItem>>();
43+
44+
foreach (var node in markdownNodes)
45+
{
46+
// No cast needed! Static typing
47+
Console.WriteLine(node.Model.FrontMatter);
48+
Console.WriteLine(node.Model.Content);
49+
}
50+
```
51+
52+
## 2. Provider Pattern for URL Context
53+
54+
`INavigationHomeProvider` / `INavigationHomeAccessor`:
55+
- **Providers** define context (PathPrefix, NavigationRoot)
56+
- **Accessors** reference providers
57+
- Decouples URL calculation from tree structure
58+
- Enables context switching (re-homing)
59+
60+
**Why This Enables Re-homing:**
61+
```csharp
62+
// Isolated build
63+
node.HomeProvider = new NavigationHomeProvider("", docsetRoot);
64+
// URLs: /api/rest/
65+
66+
// Assembler build - O(1) operation!
67+
node.HomeProvider = new NavigationHomeProvider("/guide", siteRoot);
68+
// URLs: /guide/api/rest/
69+
```
70+
71+
Single reference change updates all descendant URLs.
72+
73+
> See [Home Provider Architecture](home-provider-architecture.md) for complete explanation.
74+
75+
## 3. Lazy URL Calculation with Caching
76+
77+
`FileNavigationLeaf` implements smart URL caching:
78+
```csharp
79+
private string? _homeProviderCache;
80+
private string? _urlCache;
81+
82+
public string Url
83+
{
84+
get
85+
{
86+
if (_homeProviderCache == HomeProvider.Id && _urlCache != null)
87+
return _urlCache;
88+
89+
_urlCache = CalculateUrl();
90+
_homeProviderCache = HomeProvider.Id;
91+
return _urlCache;
92+
}
93+
}
94+
```
95+
96+
**Strategy:**
97+
- Cache URL along with HomeProvider ID
98+
- Invalidate cache when HomeProvider changes
99+
- Recalculate only when needed
100+
- O(1) for repeated access, O(depth) for calculation
101+
102+
**Why HomeProvider.Id?**
103+
- Each HomeProvider has a unique ID
104+
- Comparing IDs is cheaper than deep equality checks
105+
- ID changes when provider is replaced during re-homing
106+
- Automatic cache invalidation without explicit cache clearing
107+
108+
---
109+
110+
## Performance Characteristics
111+
112+
- **Tree Construction**: O(n) where n = number of files
113+
- **URL Calculation**: O(depth) for first access, O(1) with caching
114+
- **Re-homing**: O(1) - just replace HomeProvider reference
115+
- **Tree Traversal**: O(n) for full tree, but rarely needed
116+
- **Memory**: O(n) for nodes, URLs computed on-demand
117+
118+
**Why Re-homing is O(1):**
119+
1. Replace single HomeProvider reference
120+
2. No tree traversal required
121+
3. URLs lazy-calculated on next access
122+
4. Cache invalidation via ID comparison
123+
5. All descendants automatically use new provider

tests/Navigation.Tests/Isolation/PhysicalDocsetTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ public async Task PhysicalDocsetNavigationIncludesNestedTocs()
122122
var developmentToc = tocNavs.FirstOrDefault(t => t.Url == "/development/");
123123
developmentToc.Should().NotBeNull();
124124

125-
developmentToc.NavigationItems.Should().HaveCount(2);
125+
developmentToc.NavigationItems.Should().HaveCount(3);
126126
developmentToc.Index.Should().NotBeNull();
127127
developmentToc.NavigationItems.OfType<FileNavigationLeaf<TestDocumentationFile>>().Should().HaveCount(0);
128-
developmentToc.NavigationItems.OfType<FolderNavigation<TestDocumentationFile>>().Should().HaveCount(1);
128+
developmentToc.NavigationItems.OfType<FolderNavigation<TestDocumentationFile>>().Should().HaveCount(2);
129129
developmentToc.NavigationItems.OfType<TableOfContentsNavigation<TestDocumentationFile>>().Should().HaveCount(1);
130130

131131
var developmentIndex = developmentToc.Index as FileNavigationLeaf<TestDocumentationFile>;

0 commit comments

Comments
 (0)