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;