@page "/docs" @page "/docs/{*Page}" @inject HttpClient Http @inject NavigationManager Nav @inject IJSRuntime JS @using Markdig @implements IDisposable Docs@(!string.IsNullOrWhiteSpace(_activeTitle) ? $": {_activeTitle}" : "")
Knowledge Base

Docs @if (!string.IsNullOrWhiteSpace(_activeTitle)) { : @_activeTitle }

→ Overview
@if (_isLoading) {
loading documentation…
} else if (_notFound) {
document not found
Tried: @_lastFetchUrl
} else {
@((MarkupString)_htmlContent!)
}
@code { [Parameter] public string? Page { get; set; } private string? _htmlContent; private bool _isLoading = true; private bool _notFound; private bool _pendingScroll; private string _filter = ""; private string _activeTitle = ""; private string? _lastFetchUrl; private bool _indexLoading = true; private bool _indexNotFound; private List _docsIndex = new(); private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder() .UseAdvancedExtensions() .UseAutoIdentifiers() .Build(); protected override void OnInitialized() { Nav.LocationChanged += HandleLocationChanged; } protected override async Task OnInitializedAsync() { await LoadIndexAsync(); } protected override async Task OnParametersSetAsync() { _isLoading = true; _notFound = false; _pendingScroll = true; _activeTitle = ""; try { // DEFAULT TO overview.md var target = string.IsNullOrWhiteSpace(Page) ? "overview" : Page; var decoded = Uri.UnescapeDataString(target); if (!decoded.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) decoded += ".md"; var url = $"_content/Shared.Rcl/raw_docs/{decoded}"; _lastFetchUrl = url; var markdown = await Http.GetStringAsync(url); _htmlContent = Markdown.ToHtml(markdown, Pipeline); _activeTitle = DisplayName(decoded); } catch { _notFound = true; _htmlContent = null; _activeTitle = DisplayName(Page ?? "overview"); } finally { _isLoading = false; } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (_pendingScroll && !_isLoading && !_notFound) { _pendingScroll = false; await ScrollToFragmentAsync(); } } private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) { _pendingScroll = true; InvokeAsync(StateHasChanged); } private async Task ScrollToFragmentAsync() { var uri = new Uri(Nav.Uri); var fragment = uri.Fragment; if (!string.IsNullOrWhiteSpace(fragment)) { var anchor = fragment.TrimStart('#'); await JS.InvokeVoidAsync("scrollToAnchor", anchor); } } private async Task LoadIndexAsync() { _indexLoading = true; _indexNotFound = false; try { var url = "_content/Shared.Rcl/raw_docs/docs-index.json"; var items = await Http.GetFromJsonAsync>(url); _docsIndex = (items ?? new List()) .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Replace('\\', '/').Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList(); } catch { _indexNotFound = true; _docsIndex = new List(); } finally { _indexLoading = false; } } private IEnumerable FilteredIndex() { if (string.IsNullOrWhiteSpace(_filter)) return _docsIndex; return _docsIndex.Where(x => x.Contains(_filter, StringComparison.OrdinalIgnoreCase)); } private static string ToRouteSlug(string path) { return path.EndsWith(".md", StringComparison.OrdinalIgnoreCase) ? path[..^3] : path; } private static string DisplayName(string path) { var p = path.Replace('\\', '/').Trim(); if (p.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) p = p[..^3]; return p.Split('/', StringSplitOptions.RemoveEmptyEntries) .LastOrDefault() ?? p; } private static string Sanitize(string value) { return value.Replace(" ", "-") .Replace("/", "-") .Replace("\\", "-") .Replace(".", "-"); } public void Dispose() { Nav.LocationChanged -= HandleLocationChanged; } }