Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add & use BitMarkdownService class to BlazorUI (#10221) #10223

Merged
merged 5 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System.Globalization;
using Jint;

namespace Bit.BlazorUI;
namespace Bit.BlazorUI;

/// <summary>
/// BitMarkdownViewer is a SEO friendly Blazor wrapper around the famous markedjs library.
Expand All @@ -10,13 +7,12 @@ namespace Bit.BlazorUI;
public partial class BitMarkdownViewer : BitComponentBase
{
private string? _html;
private static string? _markedScriptText;
private readonly CancellationTokenSource _cts = new();
private static readonly SemaphoreSlim _markedScriptReadTextSemaphore = new(1, 1);



[Inject] private IJSRuntime _js { get; set; } = default!;
[Inject] private BitMarkdownService _markdownService { get; set; } = default!;



Expand All @@ -32,90 +28,15 @@ public partial class BitMarkdownViewer : BitComponentBase

protected override async Task OnInitializedAsync()
{
if (_js.IsRuntimeInvalid()) // prerendering
{
try
{
await RunJint();
}
catch (FileNotFoundException ex) when (ex.FileName?.StartsWith("Jint") is true)
{
Console.Error.WriteLine("Please install `Jint` nuget package on the server project.");
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
}
}
else
{
var scriptPath = "_content/Bit.BlazorUI.Extras/marked/marked-15.0.7.js";
if ((await _js.BitMarkdownViewerCheckScriptLoaded(scriptPath)) is false)
{
await _js.BitExtrasInitScripts([scriptPath]);
}

await ParseAndRender();
}
_html = await _markdownService.Parse(Markdown, _cts.Token);

StateHasChanged();

await base.OnInitializedAsync();
}



private async Task RunJint()
{
if (Markdown.HasNoValue()) return;

await Task.Run(async () =>
{
await ReadMarkedScriptText();
if (_markedScriptText.HasNoValue()) return;

using var engine = new Engine(options =>
{
options.Strict();
options.CancellationToken(_cts.Token);
options.Culture(CultureInfo.CurrentUICulture);
}).Execute(_markedScriptText!);

var fn = engine.Evaluate("marked.parse").AsFunctionInstance();

_html = fn.Call(Markdown).AsString();

await InvokeAsync(StateHasChanged);
}, _cts.Token);
}

private async Task<string> ReadMarkedScriptText()
{
if (_markedScriptText is not null) return _markedScriptText;

try
{
await _markedScriptReadTextSemaphore.WaitAsync(_cts.Token);
if (_markedScriptText is not null) return _markedScriptText;

var scriptPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "_content", "Bit.BlazorUI.Extras", "marked", "marked-15.0.7.js");

if (File.Exists(scriptPath) is false)
{
scriptPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "marked", "marked-15.0.7.js");
}

if (File.Exists(scriptPath) is false)
{
return _markedScriptText = string.Empty;
}

return _markedScriptText = await File.ReadAllTextAsync(scriptPath);
}
finally
{
_markedScriptReadTextSemaphore.Release();
}
}

private async Task OnMarkdownSet()
{
if (IsRendered is false) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public static IServiceCollection AddBitBlazorUIExtrasServices(this IServiceColle
}

services.TryAddScoped<BitExtraServices>();
services.TryAddScoped<BitMarkdownService>();

return services;
}
Expand Down
106 changes: 106 additions & 0 deletions src/BlazorUI/Bit.BlazorUI.Extras/Services/BitMarkdownService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.Globalization;
using Jint;

namespace Bit.BlazorUI;

/// <summary>
/// A utility service to parse Markdown texts into html strings. Works smoothly in both server and client.
/// </summary>
public class BitMarkdownService(IJSRuntime js)
{
private const string MARKED_FILE = "marked/marked-15.0.7.js";



private static string? _markedScriptText;
private static readonly SemaphoreSlim _markedScriptReadTextSemaphore = new(1, 1);



public async Task<string> Parse(string? markdown, CancellationToken cancellationToken)
{
if (markdown.HasNoValue()) return string.Empty;

var html = string.Empty;

if (js.IsRuntimeInvalid()) // server (prerendering)
{
try
{
html = await Task.Run(async () => await RuntJint(markdown, cancellationToken), cancellationToken);
}
catch (FileNotFoundException ex) when (ex.FileName?.StartsWith("Jint") is true)
{
Console.Error.WriteLine("Please install `Jint` nuget package on the server project.");
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.Message);
}
}
else // client
{
var scriptPath = "_content/Bit.BlazorUI.Extras/marked/marked-15.0.7.js";
if ((await js.BitMarkdownViewerCheckScriptLoaded(scriptPath)) is false)
{
await js.BitExtrasInitScripts([scriptPath]);
}

html = await js.BitMarkdownViewerParse(markdown!);
}

return html;
}



private static async Task<string> RuntJint(string? markdown, CancellationToken cancellationToken)
{
if (markdown.HasNoValue()) return string.Empty;

await ReadMarkedScriptText(cancellationToken);
if (_markedScriptText.HasNoValue()) return string.Empty;

using var engine = new Engine(options =>
{
options.Strict();
options.CancellationToken(cancellationToken);
options.Culture(CultureInfo.CurrentUICulture);
}).Execute(_markedScriptText!);

var fn = engine.Evaluate("marked.parse").AsFunctionInstance();

return fn.Call(markdown).AsString();
}

private static async Task<string> ReadMarkedScriptText(CancellationToken cancellationToken)
{
if (_markedScriptText is not null) return _markedScriptText;

try
{
await _markedScriptReadTextSemaphore.WaitAsync(cancellationToken);
if (_markedScriptText is not null) return _markedScriptText;

//TODO: this script path discovery needs improvement!
var scriptPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "_content", "Bit.BlazorUI.Extras", MARKED_FILE);

if (File.Exists(scriptPath) is false)
{
scriptPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", MARKED_FILE);
}

if (File.Exists(scriptPath) is false)
{
Console.Error.WriteLine("Could not find the marked js script file!");
return _markedScriptText = string.Empty;
}

return _markedScriptText = await File.ReadAllTextAsync(scriptPath, cancellationToken);
}
finally
{
_markedScriptReadTextSemaphore.Release();
}
}
}
Loading