From 45cef8625650064bc7cd9ee2aec6a4f81c899fe2 Mon Sep 17 00:00:00 2001 From: Wil Stead Date: Thu, 21 Nov 2024 08:26:39 -0500 Subject: [PATCH] - Update to .NET 9 - Update dependencies - Fix talk page --- .github/workflows/build.yml | 4 +- .github/workflows/publish.yml | 6 +- docs/CHANGELOG.md | 7 + .../Tavenem.Wiki.Blazor.Example.Client.csproj | 4 +- .../Tavenem.Wiki.Blazor.Example/Program.cs | 2 + .../Tavenem.Wiki.Blazor.Example.csproj | 4 +- .../Tavenem.Wiki.Blazor.Sample.csproj | 10 +- .../InteractiveRenderSettings.cs | 0 .../{ => Configuration}/ServiceExtensions.cs | 0 .../WikiBlazorJsonSerializerContext.cs | 0 .../{ => Configuration}/WikiBlazorOptions.cs | 0 src/Client/{ => Internal}/Extensions.cs | 0 src/Client/Internal/WikiState.cs | 20 + src/Client/LinkerConfig.xml | 1 - src/Client/Pages/ArticleView.cs | 95 ++++ src/Client/Pages/ArticleView.razor | 18 - src/Client/Pages/ArticleView.razor.cs | 73 --- src/Client/Pages/Talk.razor.cs | 47 +- src/Client/Pages/WikiPage.razor | 342 ++++++++++++++ .../WikiPage.razor.cs} | 356 +++------------ src/Client/Services/ClientWikiDataService.cs | 21 - src/Client/Shared/DynamicWikiComponent.cs | 54 --- .../{WikiLayout.razor.cs => WikiLayout.cs} | 21 +- src/Client/Shared/WikiLayout.razor | 14 - src/Client/Tavenem.Wiki.Blazor.Client.csproj | 16 +- src/Client/Wiki.cs | 427 ++++++++++++++++++ src/Client/Wiki.razor | 352 --------------- src/Client/Wiki.razor.css | 276 ----------- src/Client/assets/css/wiki.css | 276 ++++++++++- src/Server/Tavenem.Wiki.Blazor.Server.csproj | 4 +- 30 files changed, 1283 insertions(+), 1167 deletions(-) rename src/Client/{ => Configuration}/InteractiveRenderSettings.cs (100%) rename src/Client/{ => Configuration}/ServiceExtensions.cs (100%) rename src/Client/{ => Configuration}/WikiBlazorJsonSerializerContext.cs (100%) rename src/Client/{ => Configuration}/WikiBlazorOptions.cs (100%) rename src/Client/{ => Internal}/Extensions.cs (100%) create mode 100644 src/Client/Pages/ArticleView.cs delete mode 100644 src/Client/Pages/ArticleView.razor delete mode 100644 src/Client/Pages/ArticleView.razor.cs create mode 100644 src/Client/Pages/WikiPage.razor rename src/Client/{Wiki.razor.cs => Pages/WikiPage.razor.cs} (53%) delete mode 100644 src/Client/Shared/DynamicWikiComponent.cs rename src/Client/Shared/{WikiLayout.razor.cs => WikiLayout.cs} (66%) delete mode 100644 src/Client/Shared/WikiLayout.razor create mode 100644 src/Client/Wiki.cs delete mode 100644 src/Client/Wiki.razor delete mode 100644 src/Client/Wiki.razor.css diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e95fc6e..8bf7046 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,11 +9,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET 8 + - name: Setup .NET 9 uses: actions/setup-dotnet@v4 with: # Semantic version range syntax or exact version of a dotnet version - dotnet-version: '8.x' + dotnet-version: '9.x' - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3cc2e87..c568694 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,6 @@ name: publish env: - VERSION: '0.9.13-preview' + VERSION: '0.10.0-preview' PRERELEASE: true on: push: @@ -12,11 +12,11 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET 8 + - name: Setup .NET 9 uses: actions/setup-dotnet@v4 with: # Semantic version range syntax or exact version of a dotnet version - dotnet-version: '8.x' + dotnet-version: '9.x' - name: Install dependencies diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0bd5c6e..3146c7d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.10.0-preview +### Updated +- Update to .NET 9 +- Update dependencies +### Fixed +- Talk page + ## 0.9.12-13-preview ### Updated - Update dependencies diff --git a/sample/Web App Example/Tavenem.Wiki.Blazor.Example.Client/Tavenem.Wiki.Blazor.Example.Client.csproj b/sample/Web App Example/Tavenem.Wiki.Blazor.Example.Client/Tavenem.Wiki.Blazor.Example.Client.csproj index 181f80d..bd2423b 100644 --- a/sample/Web App Example/Tavenem.Wiki.Blazor.Example.Client/Tavenem.Wiki.Blazor.Example.Client.csproj +++ b/sample/Web App Example/Tavenem.Wiki.Blazor.Example.Client/Tavenem.Wiki.Blazor.Example.Client.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable true @@ -9,7 +9,7 @@ - + diff --git a/sample/Web App Example/Tavenem.Wiki.Blazor.Example/Program.cs b/sample/Web App Example/Tavenem.Wiki.Blazor.Example/Program.cs index 3490a12..3ad9d4d 100644 --- a/sample/Web App Example/Tavenem.Wiki.Blazor.Example/Program.cs +++ b/sample/Web App Example/Tavenem.Wiki.Blazor.Example/Program.cs @@ -65,6 +65,8 @@ app.UseAntiforgery(); +app.MapStaticAssets(); + app.MapWiki(); app.MapRazorComponents() .AddInteractiveWebAssemblyRenderMode() diff --git a/sample/Web App Example/Tavenem.Wiki.Blazor.Example/Tavenem.Wiki.Blazor.Example.csproj b/sample/Web App Example/Tavenem.Wiki.Blazor.Example/Tavenem.Wiki.Blazor.Example.csproj index d317ca7..5d35324 100644 --- a/sample/Web App Example/Tavenem.Wiki.Blazor.Example/Tavenem.Wiki.Blazor.Example.csproj +++ b/sample/Web App Example/Tavenem.Wiki.Blazor.Example/Tavenem.Wiki.Blazor.Example.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable false @@ -11,7 +11,7 @@ - + diff --git a/sample/WebAssembly Example/Tavenem.Wiki.Blazor.Sample.csproj b/sample/WebAssembly Example/Tavenem.Wiki.Blazor.Sample.csproj index 76bd9e6..509716c 100644 --- a/sample/WebAssembly Example/Tavenem.Wiki.Blazor.Sample.csproj +++ b/sample/WebAssembly Example/Tavenem.Wiki.Blazor.Sample.csproj @@ -1,16 +1,16 @@ - net8.0 + net9.0 enable enable - - - - + + + + diff --git a/src/Client/InteractiveRenderSettings.cs b/src/Client/Configuration/InteractiveRenderSettings.cs similarity index 100% rename from src/Client/InteractiveRenderSettings.cs rename to src/Client/Configuration/InteractiveRenderSettings.cs diff --git a/src/Client/ServiceExtensions.cs b/src/Client/Configuration/ServiceExtensions.cs similarity index 100% rename from src/Client/ServiceExtensions.cs rename to src/Client/Configuration/ServiceExtensions.cs diff --git a/src/Client/WikiBlazorJsonSerializerContext.cs b/src/Client/Configuration/WikiBlazorJsonSerializerContext.cs similarity index 100% rename from src/Client/WikiBlazorJsonSerializerContext.cs rename to src/Client/Configuration/WikiBlazorJsonSerializerContext.cs diff --git a/src/Client/WikiBlazorOptions.cs b/src/Client/Configuration/WikiBlazorOptions.cs similarity index 100% rename from src/Client/WikiBlazorOptions.cs rename to src/Client/Configuration/WikiBlazorOptions.cs diff --git a/src/Client/Extensions.cs b/src/Client/Internal/Extensions.cs similarity index 100% rename from src/Client/Extensions.cs rename to src/Client/Internal/Extensions.cs diff --git a/src/Client/Internal/WikiState.cs b/src/Client/Internal/WikiState.cs index e8f1e87..033ec4c 100644 --- a/src/Client/Internal/WikiState.cs +++ b/src/Client/Internal/WikiState.cs @@ -30,6 +30,16 @@ public string DisplayTitle /// public bool IsCompact { get; internal set; } + /// + /// Whether the current wiki page is being edited. + /// + public bool IsEditing { get; set; } + + /// + /// Whether the current wiki page is being previewed. + /// + public bool IsPreview { get; set; } + /// /// Whether the current page is a special system page. /// @@ -67,6 +77,16 @@ public bool NotAuthorized /// public string PageTitle { get; private set; } + /// + /// Whether the history of the current wiki page is being shown, rather than the content. + /// + public bool ShowHistory { get; set; } + + /// + /// Whether the links to the current wiki page are being shown, rather than the content. + /// + public bool ShowWhatLinksHere { get; set; } + /// /// The current user (if non-anonymous). /// diff --git a/src/Client/LinkerConfig.xml b/src/Client/LinkerConfig.xml index e875420..fc6b6b4 100644 --- a/src/Client/LinkerConfig.xml +++ b/src/Client/LinkerConfig.xml @@ -2,5 +2,4 @@ - diff --git a/src/Client/Pages/ArticleView.cs b/src/Client/Pages/ArticleView.cs new file mode 100644 index 0000000..cfca3f2 --- /dev/null +++ b/src/Client/Pages/ArticleView.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using System.Diagnostics.CodeAnalysis; + +namespace Tavenem.Wiki.Blazor.Client.Pages; + +/// +/// The article view. +/// +public class ArticleView : ComponentBase +{ + /// + /// The article to display. + /// + [Parameter] public Page? Page { get; set; } + + /// + /// Whether the current user has permission to edit this article. + /// + [Parameter] public bool CanEdit { get; set; } + + /// + /// The content to display. + /// + [Parameter] public MarkupString Content { get; set; } + + /// + /// Whether to display a diff. + /// + [Parameter] public bool IsDiff { get; set; } + + /// + /// The current user (may be null if the current user is browsing anonymously). + /// + [Parameter] public IWikiUser? User { get; set; } + + [Inject, NotNull] private WikiBlazorOptions? WikiBlazorClientOptions { get; set; } + + [Inject, NotNull] private WikiOptions? WikiOptions { get; set; } + + /// + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2111:RequiresUnreferencedCode", + Justification = "OpenComponent already has the right set of attributes")] + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", "wiki-site-subtitle"); + builder.AddContent(2, "From "); + builder.AddContent(3, WikiOptions.SiteName); + builder.CloseElement(); + + if (!IsDiff && Page is not null) + { + var frontMatterType = WikiBlazorClientOptions.GetArticleFrontMatter(Page); + if (frontMatterType is not null) + { + var frontMatterRenderMode = WikiBlazorClientOptions.GetArticleFrontMatterRenderMode(Page); + builder.OpenComponent(4, frontMatterType); + builder.AddAttribute(5, nameof(WikiComponent.Page), Page); + builder.AddAttribute(6, nameof(WikiComponent.CanEdit), CanEdit); + builder.AddAttribute(7, nameof(WikiComponent.User), User); + if (frontMatterRenderMode is not null) + { + builder.AddComponentRenderMode(frontMatterRenderMode); + } + builder.CloseComponent(); + } + } + + builder.OpenElement(8, "tf-syntax-highlight"); + builder.AddAttribute(9, "class", "wiki-parser-output"); + builder.AddContent(10, Content); + builder.CloseElement(); + + if (!IsDiff && Page is not null) + { + var endMatterType = WikiBlazorClientOptions.GetArticleFrontMatter(Page); + if (endMatterType is not null) + { + var endMatterRenderMode = WikiBlazorClientOptions.GetArticleFrontMatterRenderMode(Page); + builder.OpenComponent(11, endMatterType); + builder.AddAttribute(12, nameof(WikiComponent.Page), Page); + builder.AddAttribute(13, nameof(WikiComponent.CanEdit), CanEdit); + builder.AddAttribute(14, nameof(WikiComponent.User), User); + if (endMatterRenderMode is not null) + { + builder.AddComponentRenderMode(endMatterRenderMode); + } + builder.CloseComponent(); + } + } + } +} \ No newline at end of file diff --git a/src/Client/Pages/ArticleView.razor b/src/Client/Pages/ArticleView.razor deleted file mode 100644 index 8ef87c2..0000000 --- a/src/Client/Pages/ArticleView.razor +++ /dev/null @@ -1,18 +0,0 @@ -
From @WikiOptions.SiteName
-@if (!IsDiff && FrontMatterType is not null) -{ - -} -@Content -@if (!IsDiff && EndMatterType is not null) -{ - -} diff --git a/src/Client/Pages/ArticleView.razor.cs b/src/Client/Pages/ArticleView.razor.cs deleted file mode 100644 index 5f06ebc..0000000 --- a/src/Client/Pages/ArticleView.razor.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Microsoft.AspNetCore.Components; -using System.Diagnostics.CodeAnalysis; - -namespace Tavenem.Wiki.Blazor.Client.Pages; - -/// -/// The article view. -/// -public partial class ArticleView -{ - /// - /// The article to display. - /// - [Parameter] public Page? Page { get; set; } - - /// - /// Whether the current user has permission to edit this article. - /// - [Parameter] public bool CanEdit { get; set; } - - /// - /// The content to display. - /// - [Parameter] public MarkupString Content { get; set; } - - /// - /// Whether to display a diff. - /// - [Parameter] public bool IsDiff { get; set; } - - /// - /// The current user (may be null if the current user is browsing anonymously). - /// - [Parameter] public IWikiUser? User { get; set; } - - private IComponentRenderMode? EndMatterRenderMode { get; set; } - - private Type? EndMatterType { get; set; } - - private IComponentRenderMode? FrontMatterRenderMode { get; set; } - - private Type? FrontMatterType { get; set; } - - [Inject, NotNull] private WikiBlazorOptions? WikiBlazorClientOptions { get; set; } - - [Inject, NotNull] private WikiOptions? WikiOptions { get; set; } - - /// - protected override void OnParametersSet() - { - EndMatterRenderMode = null; - EndMatterType = null; - FrontMatterRenderMode = null; - FrontMatterType = null; - - if (Page is null) - { - return; - } - - EndMatterType = WikiBlazorClientOptions.GetArticleEndMatter(Page); - if (EndMatterType is not null) - { - EndMatterRenderMode = WikiBlazorClientOptions.GetArticleEndMatterRenderMode(Page); - } - - FrontMatterType = WikiBlazorClientOptions.GetArticleFrontMatter(Page); - if (FrontMatterType is not null) - { - FrontMatterRenderMode = WikiBlazorClientOptions.GetArticleFrontMatterRenderMode(Page); - } - } -} \ No newline at end of file diff --git a/src/Client/Pages/Talk.razor.cs b/src/Client/Pages/Talk.razor.cs index 5e8bb6b..4700c35 100644 --- a/src/Client/Pages/Talk.razor.cs +++ b/src/Client/Pages/Talk.razor.cs @@ -2,10 +2,8 @@ using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics.CodeAnalysis; -using System.Net.Http.Json; -using System.Text; -using Tavenem.Blazor.Framework; using Tavenem.Wiki.Blazor.Client.Internal.Models; +using Tavenem.Wiki.Blazor.Client.Services; namespace Tavenem.Wiki.Blazor.Client.Pages; @@ -27,18 +25,14 @@ public partial class Talk [MemberNotNullWhen(true, nameof(TopicId))] private bool CanTalk { get; set; } - [Inject, NotNull] private HttpClient? HttpClient { get; set; } - - [Inject, NotNull] private NavigationManager? Navigation { get; set; } - [Inject, NotNull] private IServiceProvider? ServiceProvider { get; set; } - [Inject, NotNull] private SnackbarService? SnackbarService { get; set; } - private List TalkMessages { get; set; } = []; [Inject, NotNull] private WikiBlazorOptions? WikiBlazorClientOptions { get; set; } + [Inject, NotNull] private ClientWikiDataService? WikiDataService { get; set; } + [Inject, NotNull] private WikiState? WikiState { get; set; } /// @@ -59,40 +53,7 @@ protected override async Task OnParametersSetAsync() : await AuthenticationStateProvider.GetAuthenticationStateAsync(); CanPost = state?.User.Identity?.IsAuthenticated == true; - List? messages = null; - try - { - var url = new StringBuilder(WikiBlazorClientOptions.WikiServerApiRoute) - .Append("/talk?title=") - .Append(WikiState.WikiTitle); - if (!string.IsNullOrEmpty(WikiState.WikiNamespace)) - { - url.Append("&namespace=") - .Append(WikiState.WikiNamespace); - } - var response = await HttpClient.GetAsync(url.ToString()); - if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - WikiState.NotAuthorized = true; - } - else if (response.StatusCode is System.Net.HttpStatusCode.BadRequest - or System.Net.HttpStatusCode.NoContent) - { - Navigation.NavigateTo( - WikiState.Link(WikiState.WikiTitle, WikiState.WikiNamespace), - replace: true); - } - else - { - messages = await response.Content.ReadFromJsonAsync(WikiBlazorJsonSerializerContext.Default.ListMessageResponse); - } - } - catch (Exception ex) - { - Console.WriteLine(ex); - SnackbarService.Add("An error occurred", ThemeColor.Danger); - } - + var messages = await WikiDataService.GetTalkAsync(WikiState.GetCurrentPageTitle()); if (messages is null) { return; diff --git a/src/Client/Pages/WikiPage.razor b/src/Client/Pages/WikiPage.razor new file mode 100644 index 0000000..c8cb197 --- /dev/null +++ b/src/Client/Pages/WikiPage.razor @@ -0,0 +1,342 @@ +
+
+ +
+ +
+
+
+

+ @if (IsSearch) + { + Search results + } + else if (WikiState.IsSystem) + { + Special page + : + @WikiState.DisplayTitle + } + else + { + if (WikiState.IsTalk) + { + Talk + : + } + else if (WikiState.IsEditing) + { + Editing + } + if (IsCategory + || IsGroupPage + || IsUserPage + || !CanEdit + || Page?.Exists == true) + { + @if (!WikiState.DefaultNamespace) + { + @WikiState.WikiNamespace + : + } + @WikiState.DisplayTitle + @if (WikiState.ShowHistory) + { + : + Revision history + } + } + else + { + + @if (!WikiState.DefaultNamespace) + { + @WikiState.WikiNamespace + : + } + @WikiState.DisplayTitle + @if (WikiState.ShowHistory) + { + : + Revision history + } + + } + } +

+ @if (WikiState.IsCompact && CanEdit && !WikiState.IsEditing) + { + Edit + } +
+
+ @if (WikiState.LoadError) + { +

An error occurred while loading this item.

+

You can try refreshing the page to see if that helps.

+

+ If this error is still here after refreshing, the page may be configured + incorrectly. Please contact the author of the page. +

+ @if (!string.IsNullOrEmpty(WikiOptions.HelpPageTitle)) + { +

+ If you are the author of the page and you're having trouble, please see the + help page + for information that may help. +

+ } + else + { +

+ If you are the author of the page and you're having trouble, + please contact a site administrator for help. +

+ } + } + else if (WikiState.NotAuthorized) + { + + } + else if (IsSearch) + { + + } + else if (IsUpload) + { + + } + else if (IsAllSpecials) + { + + } + else if (IsSpecialList) + { + + } + else if (WikiState.ShowHistory) + { + + } + else if (WikiState.IsEditing && CanEdit) + { + + } + else if (IsCategory && !IsRevisionRequested) + { + + } + else if (IsGroupPage && !IsRevisionRequested) + { + + } + else if (IsUserPage && !IsRevisionRequested) + { + + } + else if (Page?.Exists != true) + { +

+ This + @(IsFile ? "file" : "page") + does not exist. + @if (!IsFile && CanCreate) + { + Create it? + } +

+ } + else if (WikiState.IsTalk) + { + + } + else if (IsFile) + { + + } + else + { + + } + @if (!WikiState.IsCompact + && !WikiState.IsTalk + && !WikiState.ShowHistory + && Page?.Categories?.Count > 0) + { +
+ @WikiOptions.CategoriesTitle + +
+ } +
+
+ @if (!WikiState.IsCompact) + { +
+
+
    + @if (!WikiState.IsSystem + && !WikiState.IsTalk + && Page?.Exists == true + && Page.Timestamp > DateTimeOffset.MinValue) + { +
  • This page was last edited @Page.Timestamp.ToWikiDisplayString() (UTC).
  • + } + @if (!string.IsNullOrWhiteSpace(WikiOptions.CopyrightPageTitle)) + { +
  • + The content of this page is available under the terms of the site's + copyright policy. +
  • + } + @if (!string.IsNullOrWhiteSpace(WikiOptions.PolicyPageTitle)) + { +
  • + By using this site you agree to the site's + policies. +
  • + } +
+
+
+ } +
\ No newline at end of file diff --git a/src/Client/Wiki.razor.cs b/src/Client/Pages/WikiPage.razor.cs similarity index 53% rename from src/Client/Wiki.razor.cs rename to src/Client/Pages/WikiPage.razor.cs index d567a9b..66bf212 100644 --- a/src/Client/Wiki.razor.cs +++ b/src/Client/Pages/WikiPage.razor.cs @@ -4,178 +4,94 @@ using Microsoft.Extensions.DependencyInjection; using System.Diagnostics.CodeAnalysis; using System.Text; -using System.Web; using Tavenem.Blazor.Framework; using Tavenem.Wiki.Blazor.Client.Services; -namespace Tavenem.Wiki.Blazor.Client; +namespace Tavenem.Wiki.Blazor.Client.Pages; /// -/// -/// A component which renders a Tavenem wiki. -/// -/// -/// Should normally be placed in the NotFound template of your Router, and also as the -/// only content of a routable page whose route is the same as followed by "/{*route}". -/// +/// A wiki page. /// -public partial class Wiki : IDisposable +public partial class WikiPage : IDisposable { - internal const string DescendingParameter = "pg-d"; - internal const string EndParameter = "h-e"; - internal const string FilterParameter = "pg-f"; - internal const string PageNumberParameter = "pg-p"; - internal const string PageSizeParameter = "pg-ps"; - internal const string SortParameter = "pg-s"; - internal const string SearchDomainParameter = "s-d"; - internal const string SearchNamespaceParameter = "s-n"; - internal const string SearchOwnerParameter = "s-o"; - internal const string StartParameter = "h-s"; - internal const string EditorParameter = "h-ed"; - - private const string RevisionParameter = "rev"; - private bool _disposedValue; - /// - /// - /// Whether to show the compact view of the wiki. - /// - /// - /// This is normally supplied by a query parameter or route value, but you can also set this to - /// explicitly. - /// - /// - [Parameter, SupplyParameterFromQuery] public bool Compact { get; set; } + private string ArticleType => IsCategory ? "Category" : "Article"; /// /// Whether any current search is sorted in descending order. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = DescendingParameter)] - public bool Descending { get; set; } + [Parameter] public bool Descending { get; set; } /// /// Any requested editor filter. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = EditorParameter)] - public string? Editor { get; set; } + [Parameter] public string? Editor { get; set; } /// /// The last requested result in a paged set. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = EndParameter)] - public string? End { get; set; } + [Parameter] public string? End { get; set; } /// /// Any requested text filter. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = FilterParameter)] - public string? Filter { get; set; } + [Parameter] public string? Filter { get; set; } /// /// Whether the requested page should be loaded without following any redirects. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery] - public bool NoRedirect { get; set; } + [Parameter] public bool NoRedirect { get; set; } /// /// The requested page number in a paged set. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = PageNumberParameter)] - public int? PageNumber { get; set; } + [Parameter] public int? PageNumber { get; set; } /// /// The requested page size for a paged set. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = PageSizeParameter)] - public int? PageSize { get; set; } + [Parameter] public int? PageSize { get; set; } /// /// The timestamp of a requested revision. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = RevisionParameter)] - public string[]? Revisions { get; set; } + [Parameter] public string[]? Revisions { get; set; } + + /// + /// The requested route. + /// + [Parameter] public string? Route { get; set; } /// /// The domain filter of a search. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = SearchDomainParameter)] - public string? SearchDomain { get; set; } + [Parameter] public string? SearchDomain { get; set; } /// /// The namespace filter of a search. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = SearchNamespaceParameter)] - public string? SearchNamespace { get; set; } + [Parameter] public string? SearchNamespace { get; set; } /// /// The page owner filter of a search. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = SearchOwnerParameter)] - public string? SearchOwner { get; set; } + [Parameter] public string? SearchOwner { get; set; } /// /// The sort property of a search. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = SortParameter)] - public string? Sort { get; set; } + [Parameter] public string? Sort { get; set; } /// /// The first requested result in a paged set. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery(Name = StartParameter)] - public string? Start { get; set; } + [Parameter] public string? Start { get; set; } /// /// Whether the current user has not yet been authenticated. /// - /// - /// Expected to be provided by query string, not set explicitly. - /// - [SupplyParameterFromQuery] - public bool Unauthenticated { get; set; } - - private string ArticleType => IsCategory ? "Category" : "Article"; + [Parameter] public bool Unauthenticated { get; set; } private AuthenticationStateProvider? AuthenticationStateProvider { get; set; } @@ -214,8 +130,6 @@ private bool CanRename || RequestedFirstTime.HasValue || RequestedSecondTime.HasValue; - private bool IsEditing { get; set; } - private bool IsFile { get; set; } private bool IsGroupPage { get; set; } @@ -232,24 +146,14 @@ private bool CanRename [Inject, NotNull] private NavigationManager? NavigationManager { get; set; } - private MarkupString Preview { get; set; } - - private bool PreviewDisplayed { get; set; } - private bool RequestedDiff { get; set; } private DateTimeOffset? RequestedFirstTime { get; set; } private DateTimeOffset? RequestedSecondTime { get; set; } - private string? Route { get; set; } - [Inject, NotNull] private IServiceProvider? ServiceProvider { get; set; } - private bool ShowHistory { get; set; } - - private bool ShowWhatLinksHere { get; set; } - private SpecialListType SpecialListType { get; set; } private string? TargetDomain { get; set; } @@ -262,7 +166,7 @@ private bool CanRename [Inject, NotNull] private ClientWikiDataService? WikiDataService { get; set; } - private Page? WikiPage { get; set; } + private Page? Page { get; set; } [Inject, NotNull] private WikiOptions? WikiOptions { get; set; } @@ -321,13 +225,7 @@ private async Task GetWikiItemAsync() { var title = WikiState.GetCurrentPageTitle(); - if (PreviewDisplayed) - { - Preview = new(await WikiDataService.GetPreviewAsync(title) ?? string.Empty); - return; - } - - var item = IsEditing + var item = WikiState.IsEditing ? await WikiDataService.GetEditInfoAsync(title) : await WikiDataService.GetItemAsync( title, @@ -340,60 +238,59 @@ private async Task GetWikiItemAsync() CanCreate = false; CanEdit = false; Content = default; - WikiPage = null; + Page = null; IsDiff = false; WikiState.UpdateTitle(null); + return; } - else + + Page = item; + CanCreate = item.Permission.HasFlag(WikiPermission.Create); + CanEdit = Page?.Exists != true + ? item.Permission.HasFlag(WikiPermission.Create) + : item.Permission.HasFlag(WikiPermission.Write); + CanRename = CanEdit && item.CanRename; + if (!CanEdit && WikiState.IsEditing) { - WikiPage = item; - CanCreate = item.Permission.HasFlag(WikiPermission.Create); - CanEdit = WikiPage?.Exists != true - ? item.Permission.HasFlag(WikiPermission.Create) - : item.Permission.HasFlag(WikiPermission.Write); - CanRename = CanEdit && item.CanRename; - if (!CanEdit && IsEditing) - { - WikiState.NotAuthorized = true; + WikiState.NotAuthorized = true; - var state = AuthenticationStateProvider is null - ? null - : await AuthenticationStateProvider.GetAuthenticationStateAsync(); - if (state?.User.Identity?.IsAuthenticated != true) + var state = AuthenticationStateProvider is null + ? null + : await AuthenticationStateProvider.GetAuthenticationStateAsync(); + if (state?.User.Identity?.IsAuthenticated != true) + { + if (string.IsNullOrEmpty(WikiBlazorClientOptions.LoginPath)) + { + NavigationManager.NavigateTo(NavigationManager.GetUriWithQueryParameter(nameof(Unauthenticated), true)); + } + else { - if (string.IsNullOrEmpty(WikiBlazorClientOptions.LoginPath)) + var path = new StringBuilder(WikiBlazorClientOptions.LoginPath) + .Append(WikiBlazorClientOptions.LoginPath.Contains('?') ? '&' : '?') + .Append("returnUrl=") + .Append(Uri.EscapeDataString(NavigationManager.Uri)); + Uri? uri = null; + try + { + uri = new Uri(path.ToString()); + } + catch { } + if (uri?.IsAbsoluteUri != false) { - NavigationManager.NavigateTo(NavigationManager.GetUriWithQueryParameter(nameof(Unauthenticated), true)); + NavigationManager.NavigateTo(NavigationManager + .GetUriWithQueryParameter(nameof(Unauthenticated), true)); } else { - var path = new StringBuilder(WikiBlazorClientOptions.LoginPath) - .Append(WikiBlazorClientOptions.LoginPath.Contains('?') ? '&' : '?') - .Append("returnUrl=") - .Append(Uri.EscapeDataString(NavigationManager.Uri)); - Uri? uri = null; - try - { - uri = new Uri(path.ToString()); - } - catch { } - if (uri?.IsAbsoluteUri != false) - { - NavigationManager.NavigateTo(NavigationManager - .GetUriWithQueryParameter(nameof(Unauthenticated), true)); - } - else - { - NavigationManager.NavigateTo(uri.ToString()); - } + NavigationManager.NavigateTo(uri.ToString()); } } } - Content = new MarkupString(item.DisplayHtml ?? string.Empty); - IsDiff = item.IsDiff; - WikiState.UpdateTitle(item.DisplayTitle); - StateHasChanged(); } + Content = new MarkupString(item.DisplayHtml ?? string.Empty); + IsDiff = item.IsDiff; + WikiState.UpdateTitle(item.DisplayTitle); + StateHasChanged(); } private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) @@ -404,14 +301,12 @@ private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) private async Task RefreshAsync() { Reset(); - SetIsCompact(); - SetRoute(); await SetRouteProperties(); if (!IsSpecialList && !IsSearch && !IsAllSpecials && !IsUpload - && !ShowWhatLinksHere) + && !WikiState.ShowWhatLinksHere) { await GetWikiItemAsync(); } @@ -425,7 +320,6 @@ private void Reset() Content = default; IsAllSpecials = false; IsCategory = false; - IsEditing = false; IsFile = false; IsGroupPage = false; IsSearch = false; @@ -436,124 +330,8 @@ private void Reset() RequestedDiff = false; RequestedFirstTime = null; RequestedSecondTime = null; - ShowHistory = false; SpecialListType = SpecialListType.None; - WikiPage = null; - WikiState.IsSystem = false; - WikiState.LoadError = false; - WikiState.User = null; - WikiState.UpdateTitle(null); - } - - private void SetIsCompact() - { - WikiState.SetIsCompact(Compact); - if (!WikiState.IsCompact) - { - var uri = new Uri(NavigationManager.Uri); - if (WikiBlazorClientOptions.CompactRoutePort.HasValue - && uri.Port == WikiBlazorClientOptions.CompactRoutePort.Value) - { - WikiState.SetIsCompact(true); - } - if (!WikiState.IsCompact - && !string.IsNullOrEmpty(WikiBlazorClientOptions.CompactRouteHostPart)) - { - var parts = uri.Host.Split('.'); - var position = WikiBlazorClientOptions.CompactRouteHostPosition ?? 0; - if (parts.Length > position - && string.Equals( - parts[position], - WikiBlazorClientOptions.CompactRouteHostPart, - StringComparison.OrdinalIgnoreCase)) - { - WikiState.SetIsCompact(true); - } - } - } - if (WikiState.IsCompact) - { - StateHasChanged(); - } - } - - private void SetRoute() - { - var relativeUri = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); - if (!string.IsNullOrEmpty(WikiOptions.WikiLinkPrefix) - && relativeUri.StartsWith(WikiOptions.WikiLinkPrefix, StringComparison.OrdinalIgnoreCase)) - { - relativeUri = relativeUri[WikiOptions.WikiLinkPrefix.Length..]; - } - if (relativeUri.StartsWith('/') - || relativeUri.StartsWith(':')) - { - relativeUri = relativeUri[1..]; - } - var index = relativeUri.IndexOf('?'); - if (index == 0) - { - Route = null; - } - else if (index > 0) - { - Route = relativeUri[..index]; - } - else - { - Route = relativeUri; - } - - string? actionString = null; - if (Route is not null) - { - index = Route.IndexOf('/'); - if (index != -1) - { - actionString = Route[(index + 1)..]; - Route = Route[..index]; - } - } - - WikiState.IsTalk = false; - IsEditing = false; - ShowHistory = false; - Preview = new(string.Empty); - PreviewDisplayed = false; - ShowWhatLinksHere = false; - switch (actionString) - { - case null: - break; - case { Length: 0 }: - break; - case { } when actionString.Equals("talk", StringComparison.OrdinalIgnoreCase): - WikiState.IsTalk = true; - break; - case { } when actionString.Equals("edit", StringComparison.OrdinalIgnoreCase): - IsEditing = true; - break; - case { } when actionString.Equals("history", StringComparison.OrdinalIgnoreCase): - ShowHistory = true; - break; - case { } when actionString.Equals("preview", StringComparison.OrdinalIgnoreCase): - PreviewDisplayed = true; - break; - case { } when actionString.Equals("whatlinkshere", StringComparison.OrdinalIgnoreCase): - ShowWhatLinksHere = true; - break; - } - - if (Route is not null) - { - index = Route.IndexOf('#'); - if (index != -1) - { - Route = Route[..index]; - } - - Route = HttpUtility.UrlDecode(Route); - } + Page = null; } private async Task SetRouteProperties() @@ -583,7 +361,7 @@ private async Task SetRouteProperties() return; } - if (ShowWhatLinksHere) + if (WikiState.ShowWhatLinksHere) { IsSpecialList = true; SpecialListType = SpecialListType.What_Links_Here; diff --git a/src/Client/Services/ClientWikiDataService.cs b/src/Client/Services/ClientWikiDataService.cs index 67199c6..57c20f3 100644 --- a/src/Client/Services/ClientWikiDataService.cs +++ b/src/Client/Services/ClientWikiDataService.cs @@ -76,15 +76,6 @@ public Task EditAsync(EditRequest request, string? failMessage = null) => /// may request an archive of content without a domain, or the entire wiki. /// /// - /// - /// - /// The user does not have permission for the given domain. - /// - /// - /// Or, an archive of non-domain content or the entire wiki was requested by the user is not an - /// admin. - /// - /// public Task GetArchiveAsync(string? domain = null) { var url = new StringBuilder(wikiBlazorClientOptions.WikiServerApiRoute) @@ -329,9 +320,6 @@ public Task EditAsync(EditRequest request, string? failMessage = null) => /// /// A instance. /// - /// - /// The user does not have permission for the given content. - /// public async Task?> GetTalkAsync(PageTitle title, bool noRedirect = false) { _httpClient ??= serviceProvider.GetService(); @@ -412,12 +400,6 @@ public Task GetUploadLimitAsync() => FetchIntAsync( /// /// A instance. /// - /// - /// was empty. - /// - /// - /// The user does not have permission to view the requested page. - /// public Task GetUserPageAsync(string title) => FetchDataAsync( new StringBuilder(wikiBlazorClientOptions.WikiServerApiRoute) .Append("/userpage?title=") @@ -516,9 +498,6 @@ public Task GetUploadLimitAsync() => FetchIntAsync( /// Restores an to the wiki. /// /// An instance. - /// - /// The user does not have appropriate permission to restore all the pages in the archive. - /// public Task RestoreArchiveAsync(Archive archive) => PostAsync( $"{wikiBlazorClientOptions.WikiServerApiRoute}/restorearchive", archive, diff --git a/src/Client/Shared/DynamicWikiComponent.cs b/src/Client/Shared/DynamicWikiComponent.cs deleted file mode 100644 index 43867ff..0000000 --- a/src/Client/Shared/DynamicWikiComponent.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; -using System.Diagnostics.CodeAnalysis; - -namespace Tavenem.Wiki.Blazor.Client.Shared; - -/// -/// Dynamically renders a component in the wiki page view with a render mode. -/// -public class DynamicWikiComponent : ComponentBase -{ - /// - /// Whether the current user has permission to edit this page. - /// - [Parameter] public bool CanEdit { get; set; } - - /// - /// The page to display. - /// - [Parameter] public Page? Page { get; set; } - - /// - /// The current user (may be null if the current user is browsing anonymously). - /// - [Parameter] public IWikiUser? User { get; set; } - - /// - /// The type of component to render. - /// - [Parameter] - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] - public Type? ComponentType { get; set; } - - /// - /// The render mode to use, or if static rendering should be used. - /// - [Parameter] public IComponentRenderMode? RenderMode { get; set; } - - /// - protected override void BuildRenderTree(RenderTreeBuilder builder) - { - if (ComponentType is null) - { - return; - } - - builder.OpenComponent(0, ComponentType); - builder.AddAttribute(1, "Page", Page); - builder.AddAttribute(2, "CanEdit", CanEdit); - builder.AddAttribute(3, "User", User); - builder.AddComponentRenderMode(RenderMode); - builder.CloseComponent(); - } -} \ No newline at end of file diff --git a/src/Client/Shared/WikiLayout.razor.cs b/src/Client/Shared/WikiLayout.cs similarity index 66% rename from src/Client/Shared/WikiLayout.razor.cs rename to src/Client/Shared/WikiLayout.cs index 2a87287..180fa03 100644 --- a/src/Client/Shared/WikiLayout.razor.cs +++ b/src/Client/Shared/WikiLayout.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; using System.Diagnostics.CodeAnalysis; namespace Tavenem.Wiki.Blazor.Client.Shared; @@ -6,7 +7,7 @@ namespace Tavenem.Wiki.Blazor.Client.Shared; /// /// The default wiki layout. /// -public partial class WikiLayout : IDisposable +public partial class WikiLayout : LayoutComponentBase, IDisposable { private bool _disposedValue; @@ -23,6 +24,24 @@ public partial class WikiLayout : IDisposable /// protected override void OnInitialized() => WikiState.CompactChanged += CompactChanged; + /// + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2111:RequiresUnreferencedCode", + Justification = "OpenComponent already has the right set of attributes")] + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenComponent(0); + builder.AddComponentParameter( + 1, + nameof(LayoutView.Layout), + WikiState.IsCompact + ? ResolvedCompactLayout + : ResolvedMainLayout); + builder.AddComponentParameter(2, nameof(LayoutView.ChildContent), Body); + builder.CloseComponent(); + } + private void CompactChanged(object? sender, bool e) => StateHasChanged(); /// diff --git a/src/Client/Shared/WikiLayout.razor b/src/Client/Shared/WikiLayout.razor deleted file mode 100644 index a999916..0000000 --- a/src/Client/Shared/WikiLayout.razor +++ /dev/null @@ -1,14 +0,0 @@ -@inherits LayoutComponentBase - -@if (WikiState.IsCompact) -{ - - @Body - -} -else -{ - - @Body - -} diff --git a/src/Client/Tavenem.Wiki.Blazor.Client.csproj b/src/Client/Tavenem.Wiki.Blazor.Client.csproj index af5d460..2d7270c 100644 --- a/src/Client/Tavenem.Wiki.Blazor.Client.csproj +++ b/src/Client/Tavenem.Wiki.Blazor.Client.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable true @@ -43,13 +43,13 @@
- - - - - - - + + + + + + + diff --git a/src/Client/Wiki.cs b/src/Client/Wiki.cs new file mode 100644 index 0000000..8ebf1e7 --- /dev/null +++ b/src/Client/Wiki.cs @@ -0,0 +1,427 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics.CodeAnalysis; +using System.Web; +using Tavenem.Wiki.Blazor.Client.Pages; +using Tavenem.Wiki.Blazor.Client.Services; +using Tavenem.Wiki.Blazor.Client.Shared; + +namespace Tavenem.Wiki.Blazor.Client; + +/// +/// +/// A component which renders a Tavenem wiki. +/// +/// +/// Should normally be placed in the NotFound template of your Router, and also as the +/// only content of a routable page whose route is the same as followed by "/{*route}". +/// +/// +public class Wiki : ComponentBase, IDisposable +{ + internal const string DescendingParameter = "pg-d"; + internal const string EndParameter = "h-e"; + internal const string FilterParameter = "pg-f"; + internal const string PageNumberParameter = "pg-p"; + internal const string PageSizeParameter = "pg-ps"; + internal const string SortParameter = "pg-s"; + internal const string SearchDomainParameter = "s-d"; + internal const string SearchNamespaceParameter = "s-n"; + internal const string SearchOwnerParameter = "s-o"; + internal const string StartParameter = "h-s"; + internal const string EditorParameter = "h-ed"; + + private const string RevisionParameter = "rev"; + + private bool _disposedValue; + + /// + /// + /// Whether to show the compact view of the wiki. + /// + /// + /// This is normally supplied by a query parameter or route value, but you can also set this to + /// explicitly. + /// + /// + [Parameter, SupplyParameterFromQuery] public bool Compact { get; set; } + + /// + /// Whether any current search is sorted in descending order. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = DescendingParameter)] + public bool Descending { get; set; } + + /// + /// Any requested editor filter. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = EditorParameter)] + public string? Editor { get; set; } + + /// + /// The last requested result in a paged set. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = EndParameter)] + public string? End { get; set; } + + /// + /// Any requested text filter. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = FilterParameter)] + public string? Filter { get; set; } + + /// + /// Whether the requested page should be loaded without following any redirects. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery] + public bool NoRedirect { get; set; } + + /// + /// The requested page number in a paged set. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = PageNumberParameter)] + public int? PageNumber { get; set; } + + /// + /// The requested page size for a paged set. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = PageSizeParameter)] + public int? PageSize { get; set; } + + /// + /// The timestamp of a requested revision. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = RevisionParameter)] + public string[]? Revisions { get; set; } + + /// + /// The domain filter of a search. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = SearchDomainParameter)] + public string? SearchDomain { get; set; } + + /// + /// The namespace filter of a search. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = SearchNamespaceParameter)] + public string? SearchNamespace { get; set; } + + /// + /// The page owner filter of a search. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = SearchOwnerParameter)] + public string? SearchOwner { get; set; } + + /// + /// The sort property of a search. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = SortParameter)] + public string? Sort { get; set; } + + /// + /// The first requested result in a paged set. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery(Name = StartParameter)] + public string? Start { get; set; } + + /// + /// Whether the current user has not yet been authenticated. + /// + /// + /// Expected to be provided by query string, not set explicitly. + /// + [SupplyParameterFromQuery] + public bool Unauthenticated { get; set; } + + private AuthenticationStateProvider? AuthenticationStateProvider { get; set; } + + [Inject, NotNull] private NavigationManager? NavigationManager { get; set; } + + private MarkupString Preview { get; set; } + + private string? Route { get; set; } + + [Inject, NotNull] private IServiceProvider? ServiceProvider { get; set; } + + [Inject, NotNull] private WikiBlazorOptions? WikiBlazorClientOptions { get; set; } + + [Inject, NotNull] private ClientWikiDataService? WikiDataService { get; set; } + + [Inject, NotNull] private WikiOptions? WikiOptions { get; set; } + + [Inject, NotNull] private WikiState? WikiState { get; set; } + + /// + protected override Task OnParametersSetAsync() + => RefreshAsync(); + + /// + protected override void OnInitialized() + { + AuthenticationStateProvider = ServiceProvider.GetService(); + if (AuthenticationStateProvider is not null) + { + AuthenticationStateProvider.AuthenticationStateChanged += OnStateChanged; + } + } + + /// + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + NavigationManager.LocationChanged += OnLocationChanged; + StateHasChanged(); + } + } + + /// + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2111:RequiresUnreferencedCode", + Justification = "OpenComponent already has the right set of attributes")] + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenComponent(0); + builder.AddContent(1, WikiState.PageTitle); + builder.CloseComponent(); + + if (WikiState.IsPreview) + { + builder.AddContent(2, Preview); + return; + } + + builder.OpenComponent(3); + builder.AddComponentParameter( + 4, + nameof(LayoutView.Layout), + typeof(WikiLayout)); + builder.AddComponentParameter(5, nameof(LayoutView.ChildContent), (RenderFragment)(builder => + { + builder.OpenComponent(6); + builder.AddComponentParameter(7, nameof(WikiPage.Descending), Descending); + builder.AddComponentParameter(8, nameof(WikiPage.Editor), Editor); + builder.AddComponentParameter(9, nameof(WikiPage.End), End); + builder.AddComponentParameter(10, nameof(WikiPage.Filter), Filter); + builder.AddComponentParameter(11, nameof(WikiPage.NoRedirect), NoRedirect); + builder.AddComponentParameter(12, nameof(WikiPage.PageNumber), PageNumber); + builder.AddComponentParameter(13, nameof(WikiPage.PageSize), PageSize); + builder.AddComponentParameter(14, nameof(WikiPage.Revisions), Revisions); + builder.AddComponentParameter(15, nameof(WikiPage.Route), Route); + builder.AddComponentParameter(16, nameof(WikiPage.SearchDomain), SearchDomain); + builder.AddComponentParameter(17, nameof(WikiPage.SearchNamespace), SearchNamespace); + builder.AddComponentParameter(18, nameof(WikiPage.SearchOwner), SearchOwner); + builder.AddComponentParameter(19, nameof(WikiPage.Sort), Sort); + builder.AddComponentParameter(20, nameof(WikiPage.Start), Start); + builder.AddComponentParameter(21, nameof(WikiPage.Unauthenticated), Unauthenticated); + builder.CloseComponent(); + })); + builder.CloseComponent(); + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting + /// unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing && AuthenticationStateProvider is not null) + { + AuthenticationStateProvider.AuthenticationStateChanged -= OnStateChanged; + } + + _disposedValue = true; + } + } + + private async void OnLocationChanged(object? sender, LocationChangedEventArgs e) + => await RefreshAsync(); + + private async void OnStateChanged(object? sender) => await RefreshAsync(); + + private async Task RefreshAsync() + { + Reset(); + SetIsCompact(); + SetRoute(); + if (WikiState.IsPreview) + { + var title = WikiState.GetCurrentPageTitle(); + Preview = new(await WikiDataService.GetPreviewAsync(title) ?? string.Empty); + } + StateHasChanged(); + } + + private void Reset() + { + Preview = new(string.Empty); + WikiState.IsEditing = false; + WikiState.IsPreview = false; + WikiState.IsSystem = false; + WikiState.IsTalk = false; + WikiState.LoadError = false; + WikiState.ShowHistory = false; + WikiState.ShowWhatLinksHere = false; + WikiState.User = null; + WikiState.UpdateTitle(null); + } + + private void SetIsCompact() + { + WikiState.SetIsCompact(Compact); + if (!WikiState.IsCompact) + { + var uri = new Uri(NavigationManager.Uri); + if (WikiBlazorClientOptions.CompactRoutePort.HasValue + && uri.Port == WikiBlazorClientOptions.CompactRoutePort.Value) + { + WikiState.SetIsCompact(true); + } + if (!WikiState.IsCompact + && !string.IsNullOrEmpty(WikiBlazorClientOptions.CompactRouteHostPart)) + { + var parts = uri.Host.Split('.'); + var position = WikiBlazorClientOptions.CompactRouteHostPosition ?? 0; + if (parts.Length > position + && string.Equals( + parts[position], + WikiBlazorClientOptions.CompactRouteHostPart, + StringComparison.OrdinalIgnoreCase)) + { + WikiState.SetIsCompact(true); + } + } + } + if (WikiState.IsCompact) + { + StateHasChanged(); + } + } + + private void SetRoute() + { + var relativeUri = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); + if (!string.IsNullOrEmpty(WikiOptions.WikiLinkPrefix) + && relativeUri.StartsWith(WikiOptions.WikiLinkPrefix, StringComparison.OrdinalIgnoreCase)) + { + relativeUri = relativeUri[WikiOptions.WikiLinkPrefix.Length..]; + } + if (relativeUri.StartsWith('/') + || relativeUri.StartsWith(':')) + { + relativeUri = relativeUri[1..]; + } + var index = relativeUri.IndexOf('?'); + if (index == 0) + { + Route = null; + } + else if (index > 0) + { + Route = relativeUri[..index]; + } + else + { + Route = relativeUri; + } + + string? actionString = null; + if (Route is not null) + { + index = Route.IndexOf('/'); + if (index != -1) + { + actionString = Route[(index + 1)..]; + Route = Route[..index]; + } + } + + switch (actionString) + { + case null: + break; + case { Length: 0 }: + break; + case { } when actionString.Equals("talk", StringComparison.OrdinalIgnoreCase): + WikiState.IsTalk = true; + break; + case { } when actionString.Equals("edit", StringComparison.OrdinalIgnoreCase): + WikiState.IsEditing = true; + break; + case { } when actionString.Equals("history", StringComparison.OrdinalIgnoreCase): + WikiState.ShowHistory = true; + break; + case { } when actionString.Equals("preview", StringComparison.OrdinalIgnoreCase): + WikiState.IsPreview = true; + break; + case { } when actionString.Equals("whatlinkshere", StringComparison.OrdinalIgnoreCase): + WikiState.ShowWhatLinksHere = true; + break; + } + + if (Route is not null) + { + index = Route.IndexOf('#'); + if (index != -1) + { + Route = Route[..index]; + } + + Route = HttpUtility.UrlDecode(Route); + } + } +} \ No newline at end of file diff --git a/src/Client/Wiki.razor b/src/Client/Wiki.razor deleted file mode 100644 index d742279..0000000 --- a/src/Client/Wiki.razor +++ /dev/null @@ -1,352 +0,0 @@ -@WikiState.PageTitle - -@if (PreviewDisplayed) -{ - @Preview - return; -} - - -
-
- -
- -
-
-
-

- @if (IsSearch) - { - Search results - } - else if (WikiState.IsSystem) - { - Special page - : - @WikiState.DisplayTitle - } - else - { - if (WikiState.IsTalk) - { - Talk - : - } - else if (IsEditing) - { - Editing - } - if (IsCategory - || IsGroupPage - || IsUserPage - || !CanEdit - || WikiPage?.Exists == true) - { - @if (!WikiState.DefaultNamespace) - { - @WikiState.WikiNamespace - : - } - @WikiState.DisplayTitle - @if (ShowHistory) - { - : - Revision history - } - } - else - { - - @if (!WikiState.DefaultNamespace) - { - @WikiState.WikiNamespace - : - } - @WikiState.DisplayTitle - @if (ShowHistory) - { - : - Revision history - } - - } - } -

- @if (WikiState.IsCompact && CanEdit && !IsEditing) - { - Edit - } -
-
- @if (WikiState.LoadError) - { -

An error occurred while loading this item.

-

You can try refreshing the page to see if that helps.

-

- If this error is still here after refreshing, the page may be configured - incorrectly. Please contact the author of the page. -

- @if (!string.IsNullOrEmpty(WikiOptions.HelpPageTitle)) - { -

- If you are the author of the page and you're having trouble, please see the - help page - for information that may help. -

- } - else - { -

- If you are the author of the page and you're having trouble, - please contact site administrator for help. -

- } - } - else if (WikiState.NotAuthorized) - { - - } - else if (IsSearch) - { - - } - else if (IsUpload) - { - - } - else if (IsAllSpecials) - { - - } - else if (IsSpecialList) - { - - } - else if (ShowHistory) - { - - } - else if (IsEditing && CanEdit) - { - - } - else if (IsCategory && !IsRevisionRequested) - { - - } - else if (IsGroupPage && !IsRevisionRequested) - { - - } - else if (IsUserPage && !IsRevisionRequested) - { - - } - else if (WikiPage?.Exists != true) - { -

- This - @(IsFile ? "file" : "page") - does not exist. - @if (!IsFile && CanCreate) - { - Create it? - } -

- } - else if (WikiState.IsTalk) - { - - } - else if (IsFile) - { - - } - else - { - - } - @if (!WikiState.IsCompact - && !WikiState.IsTalk - && !ShowHistory - && WikiPage?.Categories?.Count > 0) - { -
- @WikiOptions.CategoriesTitle - -
- } -
-
- @if (!WikiState.IsCompact) - { -
-
-
    - @if (!WikiState.IsSystem - && !WikiState.IsTalk - && WikiPage?.Exists == true - && WikiPage.Timestamp > DateTimeOffset.MinValue) - { -
  • This page was last edited @WikiPage.Timestamp.ToWikiDisplayString() (UTC).
  • - } - @if (!string.IsNullOrWhiteSpace(WikiOptions.CopyrightPageTitle)) - { -
  • - The content of this page is available under the terms of the site's - copyright policy. -
  • - } - @if (!string.IsNullOrWhiteSpace(WikiOptions.PolicyPageTitle)) - { -
  • - By using this site you agree to the site's - policies. -
  • - } -
-
-
- } -
-
\ No newline at end of file diff --git a/src/Client/Wiki.razor.css b/src/Client/Wiki.razor.css deleted file mode 100644 index be3248e..0000000 --- a/src/Client/Wiki.razor.css +++ /dev/null @@ -1,276 +0,0 @@ -.wiki { - background-color: var(--tavenem-color-bg-alt); - background-image: linear-gradient(to bottom, var(--tavenem-color-bg) 0, transparent 2.5em); - display: flex; - flex-direction: column; - flex-grow: 1; - flex-shrink: 0; - position: relative; - width: 100%; -} - -::deep h1, -::deep h2, -::deep h3, -::deep h4, -::deep h5, -::deep h6 { - border-bottom: 1px solid var(--tavenem-color-border); - clear: both; - padding-bottom: .125em; -} - -::deep .emoji { - font-family: 'Segoe UI Emoji', 'Segoe UI Symbol', 'Segoe UI', 'Apple Color Emoji', 'Twemoji Mozilla', 'Noto Color Emoji', 'EmojiOne Color', 'Android Emoji'; -} - -.wiki-header nav { - display: flex; -} - - .wiki-header nav ul { - display: flex; - margin: 0; - padding: 0; - } - - .wiki-header nav ul li { - border-bottom-width: 1px; - border-image: linear-gradient(transparent 0px, var(--tavenem-color-border) 100%) 1 stretch; - border-left-width: 1px; - border-right-width: 0; - border-style: solid; - border-top-width: 0; - font-size: var(--tavenem-font-size-sm); - margin: 0; - padding: 0; - white-space: nowrap; - } - - .wiki-header nav ul li.is-active { - background-color: var(--tavenem-color-bg); - border-bottom-width: 0; - } - - .wiki-header nav ul li.is-active a { - color: inherit; - } - - .wiki-header nav ul li:last-child { - border-right-width: 1px; - } - - .wiki-header nav ul li * { - display: inline-block; - padding-bottom: 7px; - padding-left: .5rem; - padding-right: .5rem; - padding-top: 1.25rem; - } - -::deep .wiki-header nav .field { - margin-bottom: 0; -} - -::deep .wiki-header nav .field .input { - margin-top: 0; - padding-top: 7px; -} - - ::deep .wiki-header nav .field .input .input-core { - padding-inline-start: .5rem; - } - - ::deep .wiki-header nav .field .input tf-icon { - margin-top: 0; - } - -.wiki-content { - background-color: var(--tavenem-color-bg); - border-bottom: 1px solid var(--tavenem-color-border); - border-left: 1px solid var(--tavenem-color-border); - display: flex; - flex-direction: column; - flex-grow: 1; - padding: 1em; -} - -@media screen and (min-width: 992px) { - .wiki-content { - padding: 1.25em 1.5em 1.5em 1.5em; - } -} - -.wiki-layout-compact .wiki-content { - border-bottom: none; - border-left: none; -} -@media print { - .wiki-content { - border-left: none; - } -} - -#wiki-main-heading { - display: inline-flex; - flex-grow: 1; - font-size: 1.8em; - gap: .25em; - line-height: 1.3; - margin-bottom: 0.25em; -} - #wiki-main-heading a { - display: inline-flex; - gap: .25em; - } - -.wiki-main-heading-domain-separator:after { - content: ':'; -} - -.wiki-main-heading-namespace-separator:after { - content: ':'; -} - -.wiki-body { - align-items: start; - display: flex; - flex-direction: column; - flex-grow: 1; - font-size: var(--tavenem-font-size-sm); - line-height: 1.6; -} - -.preview .wiki-body { - padding-top: 1em; -} - -::deep .wiki-site-subtitle { - font-size: .92em; - margin-bottom: 1.4em; -} - -::deep .wiki-parser-output { - align-self: stretch; - flex-grow: 1; -} - - ::deep .wiki-parser-output img, - ::deep .wiki-parser-output figure { - float: right; - margin-bottom: 1em; - margin-left: 1em; - margin-top: 1em; - max-width: 40%; - } - - ::deep .wiki-parser-output img:nth-child(2n), - ::deep .wiki-parser-output figure:nth-child(2n) { - float: left; - margin-left: 0; - margin-right: 1em; - } - - ::deep .wiki-parser-output img.hero, - ::deep .wiki-parser-output figure.hero { - float: none; - margin-left: 0; - margin-right: 0; - max-width: 100%; - } - -::deep .diff-deleted { - color: var(--tavenem-color-danger); - text-decoration: line-through var(--tavenem-color-danger); -} - -::deep .diff-inserted { - color: var(--tavenem-color-success); -} - -::deep .toc { - background-color: var(--tavenem-color-bg-alt); - border: 1px solid var(--tavenem-color-border); - display: table; - float: left; - font-size: 95%; - margin-bottom: 1.5em; - margin-right: 1em; - padding: .5rem; -} - ::deep .toc .toc-title { - text-align: center; - font-family: sans-serif; - font-size: 100%; - } - - ::deep .toc ul { - list-style-type: none; - list-style-image: none; - margin-left: 0; - padding: 0; - } - - ::deep .toc ul li { - margin-bottom: 0.1em; - } - - ::deep .toc ul ul { - margin-left: 2em; - } - - ::deep .toc .toc-number { - color: var(--tavenem-color-text); - display: table-cell; - padding-right: 0.5em; - } - - ::deep .toc .toc-heading { - display: table-cell; - } - -.wiki-category-list-section { - border: 1px solid var(--tavenem-color-border); - margin-top: 1em; - padding: 5px; -} - - .wiki-category-list-section li { - border-left: 1px solid var(--tavenem-color-border); - line-height: 1.25em; - margin: 2px 0; - } - - .wiki-category-list-section li:first-child { - border-left: none; - } - -.wiki-category-list-category-link:after { - content: ':'; -} - -.wiki-footer { - font-size: var(--tavenem-font-size-sm); - line-height: 1.4; - padding: .75rem; -} - -@media screen and (min-width: 992px) { - .wiki-footer { - padding: 1.25rem; - } -} - -.wiki-footer > div:first-child li { - padding: .5em 0; -} - -.wiki-footer > div:last-child { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; -} - - .wiki-footer > div:last-child > span { - padding: .5em 0; - } diff --git a/src/Client/assets/css/wiki.css b/src/Client/assets/css/wiki.css index 4cb4a72..cdf9ef6 100644 --- a/src/Client/assets/css/wiki.css +++ b/src/Client/assets/css/wiki.css @@ -1,4 +1,5 @@ -@layer tavenem-wiki.wiki { +@layer tavenem-framework, tavenem-wiki; +@layer tavenem-wiki.wiki { :root { --tavenem-wiki-link-color-missing: var(--tavenem-color-danger); --tavenem-wiki-link-color-missing-hover: var(--tavenem-color-danger-darken); @@ -143,4 +144,277 @@ .toc { display: none!important; } + + .wiki { + background-color: var(--tavenem-color-bg-alt); + background-image: linear-gradient(to bottom, var(--tavenem-color-bg) 0, transparent 2.5em); + display: flex; + flex-direction: column; + flex-grow: 1; + flex-shrink: 0; + position: relative; + width: 100%; + + h1,h2,h3,h4,h5,h6 { + border-bottom: 1px solid var(--tavenem-color-border); + clear: both; + padding-bottom: .125em; + } + + .emoji { + font-family: 'Segoe UI Emoji', 'Segoe UI Symbol', 'Segoe UI', 'Apple Color Emoji', 'Twemoji Mozilla', 'Noto Color Emoji', 'EmojiOne Color', 'Android Emoji'; + } + + .wiki-header nav { + display: flex; + + ul { + display: flex; + margin: 0; + padding: 0; + + li { + border-bottom-width: 1px; + border-image: linear-gradient(transparent 0px, var(--tavenem-color-border) 100%) 1 stretch; + border-left-width: 1px; + border-right-width: 0; + border-style: solid; + border-top-width: 0; + font-size: var(--tavenem-font-size-sm); + margin: 0; + padding: 0; + white-space: nowrap; + + &.is-active { + background-color: var(--tavenem-color-bg); + border-bottom-width: 0; + + a { + color: inherit; + } + } + + &:last-child { + border-right-width: 1px; + } + + * { + display: inline-block; + padding-bottom: 7px; + padding-left: .5rem; + padding-right: .5rem; + padding-top: 1.25rem; + } + } + } + + .field { + margin-bottom: 0; + + .input { + margin-top: 0; + padding-top: 7px; + + .input-core { + padding-inline-start: .5rem; + } + + tf-icon { + margin-top: 0; + } + } + } + } + + .wiki-content { + background-color: var(--tavenem-color-bg); + border-bottom: 1px solid var(--tavenem-color-border); + border-left: 1px solid var(--tavenem-color-border); + display: flex; + flex-direction: column; + flex-grow: 1; + padding: 1em; + } + + #wiki-main-heading { + display: inline-flex; + flex-grow: 1; + font-size: 1.8em; + gap: .25em; + line-height: 1.3; + margin-bottom: 0.25em; + + a { + display: inline-flex; + gap: .25em; + } + + .wiki-main-heading-domain-separator:after { + content: ':'; + } + + .wiki-main-heading-namespace-separator:after { + content: ':'; + } + } + + .wiki-body { + align-items: start; + display: flex; + flex-direction: column; + flex-grow: 1; + font-size: var(--tavenem-font-size-sm); + line-height: 1.6; + } + + &.preview .wiki-body { + padding-top: 1em; + } + + .wiki-site-subtitle { + font-size: .92em; + margin-bottom: 1.4em; + } + + .wiki-parser-output { + align-self: stretch; + flex-grow: 1; + + img, + figure { + float: right; + margin-bottom: 1em; + margin-left: 1em; + margin-top: 1em; + max-width: 40%; + + &:nth-child(2n) { + float: left; + margin-left: 0; + margin-right: 1em; + } + + &.hero { + float: none; + margin-left: 0; + margin-right: 0; + max-width: 100%; + } + } + } + + .diff-deleted { + color: var(--tavenem-color-danger); + text-decoration: line-through var(--tavenem-color-danger); + } + + .diff-inserted { + color: var(--tavenem-color-success); + } + + .toc { + background-color: var(--tavenem-color-bg-alt); + border: 1px solid var(--tavenem-color-border); + display: table; + float: left; + font-size: 95%; + margin-bottom: 1.5em; + margin-right: 1em; + padding: .5rem; + + .toc-title { + text-align: center; + font-family: sans-serif; + font-size: 100%; + } + + ul { + list-style-type: none; + list-style-image: none; + margin-left: 0; + padding: 0; + + li { + margin-bottom: 0.1em; + } + + ul { + margin-left: 2em; + } + } + + .toc-number { + color: var(--tavenem-color-text); + display: table-cell; + padding-right: 0.5em; + } + + .toc-heading { + display: table-cell; + } + } + + .wiki-category-list-section { + border: 1px solid var(--tavenem-color-border); + margin-top: 1em; + padding: 5px; + + li { + border-left: 1px solid var(--tavenem-color-border); + line-height: 1.25em; + margin: 2px 0; + + &:first-child { + border-left: none; + } + } + } + + .wiki-category-list-category-link:after { + content: ':'; + } + + .wiki-footer { + font-size: var(--tavenem-font-size-sm); + line-height: 1.4; + padding: .75rem; + + > div:first-child li { + padding: .5em 0; + } + + > div:last-child { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + + > span { + padding: .5em 0; + } + } + } + } + + @media screen and (min-width: 992px) { + .wiki .wiki-content { + padding: 1.25em 1.5em 1.5em 1.5em; + } + } + + .wiki-layout-compact .wiki .wiki-content { + border-bottom: none; + border-left: none; + } + + @media print { + .wiki .wiki-content { + border-left: none; + } + } + + @media screen and (min-width: 992px) { + .wiki .wiki-footer { + padding: 1.25rem; + } + } } \ No newline at end of file diff --git a/src/Server/Tavenem.Wiki.Blazor.Server.csproj b/src/Server/Tavenem.Wiki.Blazor.Server.csproj index 7cf2e87..aec74c8 100644 --- a/src/Server/Tavenem.Wiki.Blazor.Server.csproj +++ b/src/Server/Tavenem.Wiki.Blazor.Server.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable true @@ -35,7 +35,7 @@
- +