Skip to content

Commit 10e1826

Browse files
authored
feat(blazorui): add MarkdownViewer extra component #10111 (#10211)
1 parent 11e1d1e commit 10e1826

30 files changed

+1375
-109
lines changed

src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
<PackageReference Include="Newtonsoft.Json" Version="13.0.3">
2121
<PrivateAssets>all</PrivateAssets>
2222
</PackageReference>
23+
<PackageReference Include="Jint" Version="4.2.0">
24+
<PrivateAssets>all</PrivateAssets>
25+
</PackageReference>
2326
<PackageReference Condition="'$(TargetFramework)' == 'net8.0'" Include="Microsoft.AspNetCore.Components.Web" Version="8.0.0" />
2427
<PackageReference Condition="'$(TargetFramework)' == 'net9.0'" Include="Microsoft.AspNetCore.Components.Web" Version="9.0.0" />
2528
</ItemGroup>

src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.razor.cs

+3-5
Original file line numberDiff line numberDiff line change
@@ -130,20 +130,18 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
130130
{
131131
if (firstRender)
132132
{
133-
var scripts = new List<string> { "_content/Bit.BlazorUI.Extras/chart.js/chartjs-2.9.4.js" };
133+
await _js.BitExtrasInitScripts(["_content/Bit.BlazorUI.Extras/chart.js/chartjs-2.9.4.js"]);
134134

135135
if (IsDateAdapterRequired && DateAdapterScripts is null)
136136
{
137-
scripts.Add("_content/Bit.BlazorUI.Extras/chart.js/chartjs-2.9.4-adapter.js");
137+
await _js.BitExtrasInitScripts(["_content/Bit.BlazorUI.Extras/chart.js/chartjs-2.9.4-adapter.js"]);
138138
}
139139

140140
if (DateAdapterScripts is not null)
141141
{
142-
scripts.AddRange(DateAdapterScripts);
142+
await _js.BitExtrasInitScripts(DateAdapterScripts);
143143
}
144144

145-
await _js.BitChartJsInitChartJs(scripts);
146-
147145
if (Config is not null)
148146
{
149147
await _js.BitChartJsSetupChart(Config);

src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/BitChart.ts

-33
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ namespace BitBlazorUI {
2222
}
2323

2424
export class BitChart {
25-
private static _initPromise?: Promise<unknown>;
2625
private static _bitCharts = new Map<string, Chart>();
2726

2827
public static getChartJs(canvasId: string) {
@@ -31,38 +30,6 @@ namespace BitBlazorUI {
3130
return BitChart._bitCharts.get(canvasId)!;
3231
}
3332

34-
public static async initChartJs(scripts: string[]) {
35-
if (BitChart._initPromise) {
36-
await BitChart._initPromise;
37-
}
38-
39-
const allScripts = Array.from(document.scripts).map(s => s.src);
40-
const notAppenedScripts = scripts.filter(s => !allScripts.find(as => as.endsWith(s)));
41-
42-
if (notAppenedScripts.length == 0) return Promise.resolve();
43-
44-
const promise = new Promise(async (resolve: any, reject: any) => {
45-
try {
46-
for (let url of notAppenedScripts) await addScript(url);
47-
resolve();
48-
} catch (e: any) {
49-
reject(e);
50-
}
51-
});
52-
BitChart._initPromise = promise;
53-
return promise;
54-
55-
async function addScript(url: string) {
56-
return new Promise((res, rej) => {
57-
const script = document.createElement('script');
58-
script.src = url;
59-
script.onload = res;
60-
script.onerror = rej;
61-
document.body.appendChild(script);
62-
})
63-
}
64-
}
65-
6633
public static removeChart(canvasId: string) {
6734
if (!BitChart._bitCharts.has(canvasId)) return;
6835

src/BlazorUI/Bit.BlazorUI.Extras/Components/Chart/JsInterop/BitChartJsInterop.cs

-5
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ internal static class BitChartJsInterop
2020
Converters = { new IsoDateTimeConverter() }
2121
};
2222

23-
public static ValueTask BitChartJsInitChartJs(this IJSRuntime jsRuntime, IEnumerable<string> scripts)
24-
{
25-
return jsRuntime.InvokeVoid("BitBlazorUI.BitChart.initChartJs", scripts);
26-
}
27-
2823
public static ValueTask BitChartJsRemoveChart(this IJSRuntime jsRuntime, string? canvasId)
2924
{
3025
return jsRuntime.InvokeVoid("BitBlazorUI.BitChart.removeChart", canvasId);

src/BlazorUI/Bit.BlazorUI.Extras/Components/DataGrid/BitDataGrid.razor.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace Bit.BlazorUI;
1+
// a fork from the Blazor QuickGrid at https://github.com/dotnet/aspnetcore/tree/main/src/Components/QuickGrid
2+
3+
namespace Bit.BlazorUI;
24

35
/// <summary>
46
/// BitDataGrid is a robust way to display an information-rich collection of items, and allow people to sort, and filter the content.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@namespace Bit.BlazorUI
2+
@inherits BitComponentBase
3+
4+
<div @ref="RootElement"
5+
@attributes="HtmlAttributes"
6+
id="@_Id"
7+
style="@StyleBuilder.Value"
8+
class="@ClassBuilder.Value"
9+
dir="@Dir?.ToString().ToLower()">
10+
@((MarkupString)(_html ?? ""))
11+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System.Globalization;
2+
using Jint;
3+
4+
namespace Bit.BlazorUI;
5+
6+
/// <summary>
7+
/// BitMarkdownViewer is a SEO friendly Blazor wrapper around the famous markedjs library.
8+
/// <see href="https://github.com/markedjs/marked"/>
9+
/// </summary>
10+
public partial class BitMarkdownViewer : BitComponentBase
11+
{
12+
private string? _html;
13+
private static string? _markedScriptText;
14+
private readonly CancellationTokenSource _cts = new();
15+
private static readonly SemaphoreSlim _markedScriptReadTextSemaphore = new(1, 1);
16+
17+
18+
19+
[Inject] private IJSRuntime _js { get; set; } = default!;
20+
21+
22+
23+
/// <summary>
24+
/// The Markdown string value to render as an html element.
25+
/// </summary>
26+
[Parameter, CallOnSet(nameof(OnMarkdownSet))]
27+
public string? Markdown { get; set; }
28+
29+
30+
31+
protected override string RootElementClass => "bit-mdv";
32+
33+
protected override async Task OnInitializedAsync()
34+
{
35+
if (_js.IsRuntimeInvalid()) // prerendering
36+
{
37+
try
38+
{
39+
await RunJint();
40+
}
41+
catch (FileNotFoundException ex) when (ex.FileName?.StartsWith("Jint") is true)
42+
{
43+
Console.Error.WriteLine("Please install `Jint` nuget package on the server project.");
44+
}
45+
catch (Exception ex)
46+
{
47+
Console.Error.WriteLine(ex.Message);
48+
}
49+
}
50+
else
51+
{
52+
var scriptPath = "_content/Bit.BlazorUI.Extras/marked/marked-15.0.7.js";
53+
if ((await _js.BitMarkdownViewerCheckScriptLoaded(scriptPath)) is false)
54+
{
55+
await _js.BitExtrasInitScripts([scriptPath]);
56+
}
57+
58+
await ParseAndRender();
59+
}
60+
61+
await base.OnInitializedAsync();
62+
}
63+
64+
65+
66+
private async Task RunJint()
67+
{
68+
if (Markdown.HasNoValue()) return;
69+
70+
await Task.Run(async () =>
71+
{
72+
await ReadMarkedScriptText();
73+
if (_markedScriptText.HasNoValue()) return;
74+
75+
using var engine = new Engine(options =>
76+
{
77+
options.Strict();
78+
options.CancellationToken(_cts.Token);
79+
options.Culture(CultureInfo.CurrentUICulture);
80+
}).Execute(_markedScriptText!);
81+
82+
var fn = engine.Evaluate("marked.parse").AsFunctionInstance();
83+
84+
_html = fn.Call(Markdown).AsString();
85+
86+
await InvokeAsync(StateHasChanged);
87+
}, _cts.Token);
88+
}
89+
90+
private async Task<string> ReadMarkedScriptText()
91+
{
92+
if (_markedScriptText is not null) return _markedScriptText;
93+
94+
try
95+
{
96+
await _markedScriptReadTextSemaphore.WaitAsync(_cts.Token);
97+
if (_markedScriptText is not null) return _markedScriptText;
98+
99+
var scriptPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "_content", "Bit.BlazorUI.Extras", "marked", "marked-15.0.7.js");
100+
101+
if (File.Exists(scriptPath) is false)
102+
{
103+
scriptPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "marked", "marked-15.0.7.js");
104+
}
105+
106+
if (File.Exists(scriptPath) is false)
107+
{
108+
return _markedScriptText = string.Empty;
109+
}
110+
111+
return _markedScriptText = await File.ReadAllTextAsync(scriptPath);
112+
}
113+
finally
114+
{
115+
_markedScriptReadTextSemaphore.Release();
116+
}
117+
}
118+
119+
private async Task OnMarkdownSet()
120+
{
121+
if (IsRendered is false) return;
122+
123+
await ParseAndRender();
124+
}
125+
126+
private async Task ParseAndRender()
127+
{
128+
if (Markdown.HasNoValue()) return;
129+
130+
_html = await _js.BitMarkdownViewerParse(Markdown!);
131+
132+
StateHasChanged();
133+
}
134+
135+
136+
137+
protected override async ValueTask DisposeAsync(bool disposing)
138+
{
139+
if (IsDisposed || disposing is false) return;
140+
141+
_cts.Cancel();
142+
_cts.Dispose();
143+
144+
await base.DisposeAsync(disposing);
145+
}
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@import '../../../Bit.BlazorUI/Styles/functions.scss';
2+
3+
.bit-mdv {
4+
all: revert;
5+
6+
* {
7+
all: revert;
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace BitBlazorUI {
2+
export class MarkdownViewer {
3+
public static checkScriptLoaded(script: string) {
4+
return window.marked !== undefined;
5+
}
6+
7+
public static parse(md: string) {
8+
const html = marked.parse(md, { async: false });
9+
return html;
10+
}
11+
12+
public static async parseAsync(md: string) {
13+
const html = await marked.parse(md, { async: true });
14+
return html;
15+
}
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Bit.BlazorUI;
2+
3+
internal static class BitMarkdownViewerJsRuntimeExtensions
4+
{
5+
public static ValueTask<bool> BitMarkdownViewerCheckScriptLoaded(this IJSRuntime jsRuntime, string script)
6+
{
7+
return jsRuntime.FastInvoke<bool>("BitBlazorUI.MarkdownViewer.checkScriptLoaded", script);
8+
}
9+
10+
public static ValueTask<string> BitMarkdownViewerParse(this IJSRuntime jsRuntime, string markdown)
11+
{
12+
return OperatingSystem.IsBrowser()
13+
? jsRuntime.FastInvoke<string>("BitBlazorUI.MarkdownViewer.parse", markdown)
14+
: jsRuntime.Invoke<string>("BitBlazorUI.MarkdownViewer.parseAsync", markdown);
15+
}
16+
}

0 commit comments

Comments
 (0)