From a4638bd5bbbdc7c12cd9d7a0b91a974760f94705 Mon Sep 17 00:00:00 2001 From: Jing Ling Date: Tue, 1 Sep 2026 13:19:45 +0800 Subject: [PATCH] feat: support .NET 9 and .NET 10 --- .github/workflows/build.yml | 59 +++++++++++++++++ .github/workflows/test.yml | 41 ++++++++++++ README.md | 14 ++++ ToastUIEditor.sln | 7 ++ .../Sample.BlazorApp.Client.csproj | 6 +- .../Sample.BlazorApp/Sample.BlazorApp.csproj | 4 +- .../Sample.BlazorServer.csproj | 4 +- .../Sample.BlazorWebAssembly.csproj | 8 +-- src/ToastUIEditor/Editor.events.cs | 10 +-- src/ToastUIEditor/Editor.methods.cs | 1 + .../Internals/EnumValueConverter.cs | 52 ++++++++++++++- src/ToastUIEditor/ToastUIEditor.csproj | 16 ++--- src/ToastUIEditor/Viewer.events.cs | 2 +- src/ToastUIEditor/Viewer.methods.cs | 1 + tests/ToastUIEditor.Tests/ComponentTests.cs | 44 +++++++++++++ tests/ToastUIEditor.Tests/EditorTests.cs | 66 +++++++++++++++++++ .../ToastUIEditor.Tests.csproj | 24 +++++++ tests/ToastUIEditor.Tests/Usings.cs | 1 + 18 files changed, 331 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/test.yml create mode 100644 tests/ToastUIEditor.Tests/ComponentTests.cs create mode 100644 tests/ToastUIEditor.Tests/EditorTests.cs create mode 100644 tests/ToastUIEditor.Tests/ToastUIEditor.Tests.csproj create mode 100644 tests/ToastUIEditor.Tests/Usings.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..cc79dbc --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: Publish NuGet Package + +on: + workflow_dispatch: + inputs: + version: + description: Optional package version; defaults to the release tag or project version + required: false + type: string + release: + types: [published] + +concurrency: + group: publish-nuget + cancel-in-progress: false + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: true + NUGET_DIRECTORY: ${{ github.workspace }}/artifacts/packages + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Restore dependencies + run: dotnet restore src/ToastUIEditor/ToastUIEditor.csproj + - name: Pack + shell: bash + env: + RELEASE_VERSION: ${{ github.event.release.tag_name || inputs.version }} + run: | + version="${RELEASE_VERSION#v}" + args=(src/ToastUIEditor/ToastUIEditor.csproj --configuration Release --no-restore --output "$NUGET_DIRECTORY" -p:ContinuousIntegrationBuild=true) + if [ -n "$version" ]; then args+=("-p:Version=$version"); fi + dotnet pack "${args[@]}" + - name: Publish to NuGet.org + shell: bash + run: | + packages=$(find "$NUGET_DIRECTORY" -maxdepth 1 -name '*.nupkg' ! -name '*.snupkg' -print) + if [ -z "$packages" ]; then + echo "No NuGet package was produced." >&2 + exit 1 + fi + while IFS= read -r package; do + dotnet nuget push "$package" --api-key "${{ secrets.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate + done <<< "$packages" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ff148f0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,41 @@ +name: Build and Test + +on: + workflow_dispatch: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + +concurrency: + group: test-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: true + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Restore dependencies + run: dotnet restore ToastUIEditor.sln + - name: Build + run: dotnet build ToastUIEditor.sln --configuration Release --no-restore -p:ContinuousIntegrationBuild=true + - name: Run tests + run: dotnet test tests/ToastUIEditor.Tests/ToastUIEditor.Tests.csproj --configuration Release --no-build --no-restore --verbosity normal + - name: Verify NuGet package can be created + run: dotnet pack src/ToastUIEditor/ToastUIEditor.csproj --configuration Release --no-build --no-restore --output ./artifacts/packages -p:ContinuousIntegrationBuild=true diff --git a/README.md b/README.md index 15e0b65..0e69d90 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,20 @@ `tui.editor.blazor` is a Blazor component that provides a Markdown editor based on the [tui.editor](https://github.com/nhn/tui.editor) library. +Supports .NET 9 and .NET 10 for Blazor Server and Blazor WebAssembly applications. + +## Development + +```shell +dotnet build ToastUIEditor.sln --configuration Release +dotnet test tests/ToastUIEditor.Tests/ToastUIEditor.Tests.csproj --configuration Release +``` + +Packages are built automatically by GitHub Actions for pushes and pull requests. A NuGet +publication can be triggered by publishing a GitHub Release (the tag is used as the package +version) or manually from the `Publish NuGet Package` workflow. The repository must have a +`NUGET_API_KEY` secret configured for publishing. + ## Installation diff --git a/ToastUIEditor.sln b/ToastUIEditor.sln index af80fb3..7374bb5 100644 --- a/ToastUIEditor.sln +++ b/ToastUIEditor.sln @@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "git", "git", "{F6BA15F6-D72 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ToastUIEditor.Generators", "src\ToastUIEditor.Generators\ToastUIEditor.Generators.csproj", "{F3C426CF-BC81-4EB3-A765-D535B545F889}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ToastUIEditor.Tests", "tests\ToastUIEditor.Tests\ToastUIEditor.Tests.csproj", "{A4E75D31-4D8D-4F3E-9C93-7A2C4C95D8D7}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.BlazorApp", "samples\Sample.BlazorApp\Sample.BlazorApp\Sample.BlazorApp.csproj", "{17C9C8F2-94AD-46CD-8B8D-BD6739800509}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.BlazorApp.Client", "samples\Sample.BlazorApp\Sample.BlazorApp.Client\Sample.BlazorApp.Client.csproj", "{C35EAE3A-68AF-4EC8-9B91-7C2CE00EB577}" @@ -40,6 +42,10 @@ Global {F3C426CF-BC81-4EB3-A765-D535B545F889}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3C426CF-BC81-4EB3-A765-D535B545F889}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3C426CF-BC81-4EB3-A765-D535B545F889}.Release|Any CPU.Build.0 = Release|Any CPU + {A4E75D31-4D8D-4F3E-9C93-7A2C4C95D8D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4E75D31-4D8D-4F3E-9C93-7A2C4C95D8D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4E75D31-4D8D-4F3E-9C93-7A2C4C95D8D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4E75D31-4D8D-4F3E-9C93-7A2C4C95D8D7}.Release|Any CPU.Build.0 = Release|Any CPU {17C9C8F2-94AD-46CD-8B8D-BD6739800509}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17C9C8F2-94AD-46CD-8B8D-BD6739800509}.Debug|Any CPU.Build.0 = Debug|Any CPU {17C9C8F2-94AD-46CD-8B8D-BD6739800509}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -63,6 +69,7 @@ Global GlobalSection(NestedProjects) = preSolution {8D6E935A-3EDC-4774-95ED-1D4C2D7C71F0} = {3EFCFD32-EC2A-40B3-9A4D-32DD3FD98A29} {F3C426CF-BC81-4EB3-A765-D535B545F889} = {3EFCFD32-EC2A-40B3-9A4D-32DD3FD98A29} + {A4E75D31-4D8D-4F3E-9C93-7A2C4C95D8D7} = {3EFCFD32-EC2A-40B3-9A4D-32DD3FD98A29} {17C9C8F2-94AD-46CD-8B8D-BD6739800509} = {73CA8193-E7DD-47DD-91E9-A3183F63AF53} {C35EAE3A-68AF-4EC8-9B91-7C2CE00EB577} = {73CA8193-E7DD-47DD-91E9-A3183F63AF53} {59E5EE05-8208-424D-9A01-CB6E5590D171} = {73CA8193-E7DD-47DD-91E9-A3183F63AF53} diff --git a/samples/Sample.BlazorApp/Sample.BlazorApp.Client/Sample.BlazorApp.Client.csproj b/samples/Sample.BlazorApp/Sample.BlazorApp.Client/Sample.BlazorApp.Client.csproj index 86a9df0..e59cda8 100644 --- a/samples/Sample.BlazorApp/Sample.BlazorApp.Client/Sample.BlazorApp.Client.csproj +++ b/samples/Sample.BlazorApp/Sample.BlazorApp.Client/Sample.BlazorApp.Client.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable true @@ -9,8 +9,8 @@ - - + + diff --git a/samples/Sample.BlazorApp/Sample.BlazorApp/Sample.BlazorApp.csproj b/samples/Sample.BlazorApp/Sample.BlazorApp/Sample.BlazorApp.csproj index 3b1d4ae..acb7e74 100644 --- a/samples/Sample.BlazorApp/Sample.BlazorApp/Sample.BlazorApp.csproj +++ b/samples/Sample.BlazorApp/Sample.BlazorApp/Sample.BlazorApp.csproj @@ -1,14 +1,14 @@ - net8.0 + net10.0 enable enable - + diff --git a/samples/Sample.BlazorServer/Sample.BlazorServer.csproj b/samples/Sample.BlazorServer/Sample.BlazorServer.csproj index 7b6b0d2..cd1d78f 100644 --- a/samples/Sample.BlazorServer/Sample.BlazorServer.csproj +++ b/samples/Sample.BlazorServer/Sample.BlazorServer.csproj @@ -1,13 +1,13 @@  - net7.0 + net10.0 enable enable - + diff --git a/samples/Sample.BlazorWebAssembly/Sample.BlazorWebAssembly.csproj b/samples/Sample.BlazorWebAssembly/Sample.BlazorWebAssembly.csproj index a300032..7c583d1 100644 --- a/samples/Sample.BlazorWebAssembly/Sample.BlazorWebAssembly.csproj +++ b/samples/Sample.BlazorWebAssembly/Sample.BlazorWebAssembly.csproj @@ -1,15 +1,15 @@ - net8.0 + net10.0 enable enable - - - + + + diff --git a/src/ToastUIEditor/Editor.events.cs b/src/ToastUIEditor/Editor.events.cs index cf81b04..cac04f4 100644 --- a/src/ToastUIEditor/Editor.events.cs +++ b/src/ToastUIEditor/Editor.events.cs @@ -91,23 +91,23 @@ public async Task InvokeLoadAsync() await Task.Delay(100); time -= 100; } - _ = Load.InvokeAsync(); + await Load.InvokeAsync(); } /// /// Do not call this method directly. This method is called by JavaScript. /// [JSInvokable("change")] - public Task InvokeChangeAsync(string editorType, string value) + public async Task InvokeChangeAsync(string editorType, string value) { if (value != CurrentValueAsString) { CurrentValueAsString = value; StateHasChanged(); - _ = ValueChanged.InvokeAsync(value); - _ = Change.InvokeAsync(value); + await ValueChanged.InvokeAsync(value); + await Change.InvokeAsync(value); } - return Task.CompletedTask; + await Task.CompletedTask; } /// diff --git a/src/ToastUIEditor/Editor.methods.cs b/src/ToastUIEditor/Editor.methods.cs index c8af38c..c0af975 100644 --- a/src/ToastUIEditor/Editor.methods.cs +++ b/src/ToastUIEditor/Editor.methods.cs @@ -454,5 +454,6 @@ private async Task DisposeJavaScriptObjects() { await _module.DisposeAsync(); } + Options.Reference?.Dispose(); } } diff --git a/src/ToastUIEditor/Internals/EnumValueConverter.cs b/src/ToastUIEditor/Internals/EnumValueConverter.cs index cfd384b..197306e 100644 --- a/src/ToastUIEditor/Internals/EnumValueConverter.cs +++ b/src/ToastUIEditor/Internals/EnumValueConverter.cs @@ -8,7 +8,23 @@ internal sealed class EnumValueJsonConverter : JsonConverter where T : Enu { public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + if (reader.TokenType != JsonTokenType.String) + { + throw new JsonException($"Expected a string value for {typeof(T).Name}."); + } + + var value = reader.GetString(); + foreach (var member in typeof(T).GetMembers(BindingFlags.Public | BindingFlags.Static)) + { + var attribute = member.GetCustomAttribute(); + if (attribute?.Value.Equals(value, StringComparison.OrdinalIgnoreCase) == true || + member.Name.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + return (T)Enum.Parse(typeof(T), member.Name); + } + } + + throw new JsonException($"Unknown {typeof(T).Name} value '{value}'."); } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) @@ -25,7 +41,39 @@ internal sealed class EnumDictionaryKeyJsonConverter : JsonConvert { public override Dictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - throw new NotImplementedException(); + using var document = JsonDocument.ParseValue(ref reader); + if (document.RootElement.ValueKind != JsonValueKind.Object) + { + throw new JsonException($"Expected an object for {typeof(TKey).Name} dictionary."); + } + + var result = new Dictionary(); + foreach (var property in document.RootElement.EnumerateObject()) + { + TKey key = default!; + var found = false; + foreach (TKey item in Enum.GetValues(typeof(TKey))) + { + var member = typeof(TKey).GetMember(item.ToString())[0]; + var attribute = member.GetCustomAttribute(); + if (attribute?.Value.Equals(property.Name, StringComparison.OrdinalIgnoreCase) == true || + item.ToString().Equals(property.Name, StringComparison.OrdinalIgnoreCase)) + { + key = item; + found = true; + break; + } + } + + if (!found) + { + throw new JsonException($"Unknown {typeof(TKey).Name} value '{property.Name}'."); + } + + result[key] = property.Value.Deserialize(options)!; + } + + return result; } public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) diff --git a/src/ToastUIEditor/ToastUIEditor.csproj b/src/ToastUIEditor/ToastUIEditor.csproj index 066ae6a..77e96dc 100644 --- a/src/ToastUIEditor/ToastUIEditor.csproj +++ b/src/ToastUIEditor/ToastUIEditor.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0;net8.0; + net9.0;net10.0 enable enable ToastUI @@ -14,7 +14,7 @@ ToastUIEditor - 1.0.0 + 1.1.0 ling921; Blazor wrapper for ToastUI Editor, see README.md file for more details. Copyright © ling921 All rights reserved. @@ -31,16 +31,12 @@ - - + + - - - - - - + + diff --git a/src/ToastUIEditor/Viewer.events.cs b/src/ToastUIEditor/Viewer.events.cs index 47eccad..b914a46 100644 --- a/src/ToastUIEditor/Viewer.events.cs +++ b/src/ToastUIEditor/Viewer.events.cs @@ -43,7 +43,7 @@ public async Task InvokeLoadAsync() await Task.Delay(100); time -= 100; } - _ = Load.InvokeAsync(); + await Load.InvokeAsync(); } /// diff --git a/src/ToastUIEditor/Viewer.methods.cs b/src/ToastUIEditor/Viewer.methods.cs index e9fc827..df167b2 100644 --- a/src/ToastUIEditor/Viewer.methods.cs +++ b/src/ToastUIEditor/Viewer.methods.cs @@ -67,5 +67,6 @@ private async Task DisposeJavaScriptObjects() { await _module.DisposeAsync(); } + Options.Reference?.Dispose(); } } diff --git a/tests/ToastUIEditor.Tests/ComponentTests.cs b/tests/ToastUIEditor.Tests/ComponentTests.cs new file mode 100644 index 0000000..ef154b7 --- /dev/null +++ b/tests/ToastUIEditor.Tests/ComponentTests.cs @@ -0,0 +1,44 @@ +using System.Text.Json; +using ToastUI; + +namespace ToastUIEditor.Tests; + +public class ComponentTests +{ + [Fact] + public void ViewerOptions_HaveExpectedDefaults() + { + var options = new ViewerOptions(); + + Assert.True(options.Viewer); + Assert.True(options.UsageStatistics); + Assert.Equal(Theme.Light, options.Theme); + } + + [Fact] + public void WidgetRule_InvokesDelegate() + { + using var rule = new WidgetRule("@\\w+", value => $"{value}"); + + Assert.Equal("@user", rule.ToDOM("@user")); + } + + [Fact] + public void EditorLanguage_RejectsUnknownDefaultLanguage() + { + Assert.Throws(() => Editor.SetDefaultLanguage("not-a-language")); + } + + [Fact] + public void ViewerOptions_SerializeLinkAttributeKeys() + { + var options = new ViewerOptions + { + LinkAttributes = new() { [LinkAttributeNames.Rel] = "nofollow" } + }; + + using var json = JsonDocument.Parse(JsonSerializer.Serialize(options, JsonSerializerOptions.Web)); + + Assert.Equal("nofollow", json.RootElement.GetProperty("linkAttributes").GetProperty("rel").GetString()); + } +} diff --git a/tests/ToastUIEditor.Tests/EditorTests.cs b/tests/ToastUIEditor.Tests/EditorTests.cs new file mode 100644 index 0000000..4f07f18 --- /dev/null +++ b/tests/ToastUIEditor.Tests/EditorTests.cs @@ -0,0 +1,66 @@ +using System.Text.Json; +using ToastUI; + +namespace ToastUIEditor.Tests; + +public class EditorTests +{ + [Fact] + public void EditorOptions_HaveExpectedDefaults() + { + var options = new EditorOptions(); + + Assert.Equal("300px", options.Height); + Assert.Equal("200px", options.MinHeight); + Assert.Equal(EditorType.Markdown, options.InitialEditType); + Assert.Equal(6, options.ToolbarItems.Length); + Assert.True(options.Autofocus); + Assert.True(options.UsageStatistics); + } + + [Fact] + public void EditorOptions_SerializeEnumValuesAndDictionaryKeys() + { + var options = new EditorOptions + { + InitialEditType = EditorType.WYSIWYG, + Theme = Theme.Dark, + LinkAttributes = new() { [LinkAttributeNames.Target] = "_blank" } + }; + + using var json = JsonDocument.Parse(JsonSerializer.Serialize(options, JsonSerializerOptions.Web)); + var root = json.RootElement; + + Assert.Equal("wysiwyg", root.GetProperty("initialEditType").GetString()); + Assert.Equal("dark", root.GetProperty("theme").GetString()); + Assert.Equal("_blank", root.GetProperty("linkAttributes").GetProperty("target").GetString()); + } + + [Fact] + public void EditorOptions_DeserializeEnumValuesAndDictionaryKeys() + { + var options = JsonSerializer.Deserialize( + "{\"initialEditType\":\"wysiwyg\",\"theme\":\"dark\",\"linkAttributes\":{\"target\":\"_blank\"}}", JsonSerializerOptions.Web)!; + + Assert.Equal(EditorType.WYSIWYG, options.InitialEditType); + Assert.Equal(Theme.Dark, options.Theme); + Assert.Equal("_blank", options.LinkAttributes![LinkAttributeNames.Target]); + } + + [Fact] + public void Editor_SetHeightRejectsEmptyValue() + { + var editor = new Editor(); + + Assert.Throws(() => editor.SetHeight(string.Empty)); + } + + [Fact] + public void Editor_SetSelectionRejectsInvalidRange() + { + var editor = new Editor(); + + Assert.Throws(() => editor.SetSelection(2, 1)); + } + +} diff --git a/tests/ToastUIEditor.Tests/ToastUIEditor.Tests.csproj b/tests/ToastUIEditor.Tests/ToastUIEditor.Tests.csproj new file mode 100644 index 0000000..08a4867 --- /dev/null +++ b/tests/ToastUIEditor.Tests/ToastUIEditor.Tests.csproj @@ -0,0 +1,24 @@ + + + + net10.0 + false + true + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/ToastUIEditor.Tests/Usings.cs b/tests/ToastUIEditor.Tests/Usings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/tests/ToastUIEditor.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit;