docs: add monaco code view for demos (#3499)

* docs: add monaco code view

* fix code view render

* clean up

* fix css

* clean up

* try load wasm

* try copy dll files to output

* fix directory

* fix directory

* enable aot in preview build

* fix copy dll

* fix action

* fix

* show code before loaded
This commit is contained in:
James Yeung 2024-09-10 01:15:18 +08:00 committed by GitHub
parent ab8135c1cf
commit beb9491bb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 552 additions and 52 deletions

View File

@ -49,7 +49,8 @@ jobs:
npm install
dotnet build
sed -i s/{version}/$PACKAGE_VERSION/g ./site/AntDesign.Docs/Shared/HeaderMenu.razor
dotnet publish ./site/AntDesign.Docs.Wasm -c Release -f net8 -o _site
dotnet publish ./site/AntDesign.Docs.Wasm -c Release -f net8 -o _site -p:EnableAOT=true
cp -rf ./site/AntDesign.Docs.Wasm/bin/Debug/net8/*.dll _site/wwwroot/_framework
env:
PACKAGE_VERSION: ${{ github.event.number }}

View File

@ -17,6 +17,7 @@
<link href="manifest.json" rel="manifest" />
<link rel="stylesheet/less" type="text/css" href="_content/AntDesign.Docs/color.less" />
<link rel="stylesheet" type="text/css" href="./css/toast.css" />
<link rel="stylesheet" data-name="vs/editor/editor.main" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.min.css">
</head>
<body>
@ -239,6 +240,27 @@
<script src="/_content/AntDesign.Charts/g2plot.js?v={version}"></script>
<script src="/_content/AntDesign.Charts/ant-design-charts-blazor.js?v={version}"></script>
<script src="/_content/AntDesign.Docs/js/prism.js?v={version}"></script>
<script>var require = { paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs' } }</script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/loader.min.js"></script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.nls.js"></script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.js"></script>
<script>
var instances = {};
function createEditor(element, id, ref, options) {
console.log(id);
var instance = monaco.editor.create(element, options);
instance.onDidChangeModelContent(function () {
ref.invokeMethodAsync('OnChangeAsync', instance.getValue());
});
instances[id] = instance;
return instance;
}
function copy(id) {
var text = instances[id].getValue();
navigator.clipboard.writeText(text);
}
</script>
<script>
window.XPrism = {};
window.XPrism.highlight = function (code, language) {

View File

@ -50,6 +50,28 @@
Prism.highlightAll();
}
</script>
<script>var require = { paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs' } }</script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/loader.min.js"></script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.nls.js"></script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.js"></script>
<script>
var instances = {};
function createEditor(element, id, ref, options) {
console.log(id);
var instance = monaco.editor.create(element, options);
instance.onDidChangeModelContent(function() {
ref.invokeMethodAsync('OnChangeAsync', instance.getValue());
});
instances[id] = instance;
return instance;
}
function copy(id) {
var text = instances[id].getValue();
navigator.clipboard.writeText(text);
}
</script>
<script src="@("https://fastly.jsdelivr.net/npm/@docsearch/js@3")"></script>
<script src="_content/AntDesign.Docs/js/docsearch.js"></script>
@if (!isNET6)

View File

@ -13,6 +13,7 @@
<link rel="stylesheet" href="https://fastly.jsdelivr.net/npm/@docsearch/css@3" />
<link href="_content/AntDesign.Docs/css/default.css" rel="stylesheet">
<link rel="stylesheet/less" type="text/css" href="_content/AntDesign.Docs/color.less">
<link rel="stylesheet" data-name="vs/editor/editor.main" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.min.css">
</head>
<body>
@ -202,6 +203,27 @@
<script src="_content/AntDesign.Charts/ant-design-charts-blazor.js"></script>
<script src="_content/AntDesign.Docs/js/prism.js"></script>
<script>var require = { paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs' } }</script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/loader.min.js"></script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.nls.js"></script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.1/min/vs/editor/editor.main.js"></script>
<script>
var instances = {};
function createEditor(element, id, ref, options) {
console.log(id);
var instance = monaco.editor.create(element, options);
instance.onDidChangeModelContent(function () {
ref.invokeMethodAsync('OnChangeAsync', instance.getValue());
});
instances[id] = instance;
return instance;
}
function copy(id) {
var text = instances[id].getValue();
navigator.clipboard.writeText(text);
}
</script>
<script>
window.XPrism = {};
window.XPrism.highlight = function (code, language) {

View File

@ -7,6 +7,9 @@
<ItemGroup>
<PackageReference Include="AntDesign.Charts" Version="0.5.5" />
<PackageReference Include="System.Net.Http.Json" Version="7.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Razor" Version="6.0.24" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.4" />
</ItemGroup>

View File

@ -37,6 +37,7 @@ namespace Microsoft.Extensions.DependencyInjection
//}, ServiceLifetime.Singleton);
//services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddScoped<CompilerService>();
return services;
}
}

View File

@ -0,0 +1,212 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Razor;
namespace AntDesign.Docs.Services
{
public class CompilerService
{
class CollectibleAssemblyLoadContext : AssemblyLoadContext
{
public CollectibleAssemblyLoadContext() : base(isCollectible: true)
{
}
}
class InMemoryProjectItem : RazorProjectItem
{
private readonly string _source;
private readonly string _fileName;
public override string BasePath => _fileName;
public override string FilePath => _fileName;
public override string PhysicalPath => _fileName;
public override bool Exists => true;
public override Stream Read()
{
return new MemoryStream(Encoding.UTF8.GetBytes(_source));
}
private readonly string _fileKind = FileKinds.Component;
public override string FileKind => _fileKind;
public InMemoryProjectItem(string source)
{
this._source = source;
_fileName = "DynamicComponent.razor";
_fileKind = "component";
}
}
private CSharpCompilation _compilation;
private RazorProjectEngine _razorProjectEngine;
private CollectibleAssemblyLoadContext _loadContext;
private AssemblyLoadContext GetLoadContext()
{
if (_loadContext != null)
{
_loadContext.Unload();
GC.Collect();
}
_loadContext = new CollectibleAssemblyLoadContext();
return _loadContext; ;
}
private const string Imports = @"@using System.Net.Http
@using System
@using System.Collections.Generic
@using System.Linq
@using System.Threading.Tasks
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using AntDesign
@using AntDesign.Docs.Pages
@using AntDesign.Docs.Shared
@using System.Text.Json
";
private readonly HttpClient _httpClient;
public CompilerService(HttpClient httpClient)
{
this._httpClient = httpClient;
}
private async Task InitializeAsync()
{
var streams = await GetStreamsAsync();
var referenceAssemblies = streams.Select(stream => MetadataReference.CreateFromStream(stream)).ToList();
_compilation = CSharpCompilation.Create(
"AntDesign.Docs.DynamicAssembly",
Array.Empty<SyntaxTree>(),
referenceAssemblies,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
_razorProjectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, RazorProjectFileSystem.Create("/"), builder =>
{
builder.SetRootNamespace("AntDesign.Docs.Demo");
CompilerFeatures.Register(builder);
var metadataReferenceFeature = new DefaultMetadataReferenceFeature { References = referenceAssemblies.ToArray() };
builder.Features.Add(metadataReferenceFeature);
builder.Features.Add(new CompilationTagHelperFeature());
});
}
private async Task<IEnumerable<Stream>> GetStreamsAsync()
{
var referenceAssemblyRoots = new[]
{
typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly, // System.Runtime
typeof(ComponentBase).Assembly,
typeof(AntDesign._Imports).Assembly,
typeof(ExpandoObject).Assembly,
typeof(_Imports).Assembly,
};
var referencedAssemblies = referenceAssemblyRoots
.SelectMany(assembly => assembly.GetReferencedAssemblies().Append(assembly.GetName()))
.Select(Assembly.Load)
.Distinct()
.ToList();
if (referencedAssemblies.Any(assembly => string.IsNullOrEmpty(assembly.Location)))
{
var list = new List<Stream>();
await Task.WhenAll(
referencedAssemblies.Select(async assembly =>
{
var result = await _httpClient.GetAsync($"/_framework/{assembly.GetName().Name}.dll");
result.EnsureSuccessStatusCode();
list.Add(await result.Content.ReadAsStreamAsync());
})
);
return list;
}
else
{
return referencedAssemblies.Select(assembly => File.OpenRead(assembly.Location));
}
}
public async Task<Type> CompileAsync(string source)
{
if (_compilation == null)
{
await InitializeAsync();
}
var projectItem = new InMemoryProjectItem(source);
var codeDocument = _razorProjectEngine.Process(RazorSourceDocument.ReadFrom(projectItem), FileKinds.Component,
new[] { RazorSourceDocument.Create(Imports, "_Imports.razor") }, null);
var csharpDocument = codeDocument.GetCSharpDocument();
var syntaxTree = CSharpSyntaxTree.ParseText(csharpDocument.GeneratedCode, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10));
_compilation = _compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(syntaxTree);
var errors = _compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error);
if (errors.Any())
{
throw new ApplicationException(string.Join(Environment.NewLine, errors.Select(e => e.GetMessage())));
}
using var stream = new MemoryStream();
var emitResult = _compilation.Emit(stream);
if (!emitResult.Success)
{
throw new ApplicationException(string.Join(Environment.NewLine, emitResult.Diagnostics.Select(d => d.GetMessage())));
}
stream.Seek(0, SeekOrigin.Begin);
var assembly = GetLoadContext().LoadFromStream(stream);
var type = assembly.GetType("AntDesign.Docs.Demo.DynamicComponent");
return type;
}
}
}

View File

@ -13,9 +13,22 @@
{
<MockBrowser Height="@Demo.Iframe.Value" WithUrl="@(Demo.Link??$"/mock?demoId={DemoId}&type={Demo.Type}")" />
}
else if (_demoType != null)
else
{
<DynamicComponent Type="_demoType" />
@if (_demoType != null)
{
<ErrorBoundary @ref="_errorBoundary">
<ChildContent>
<DynamicComponent Type="_demoType" />
</ChildContent>
<ErrorContent Context="ex">
<Alert Type="@AlertType.Error"
Message="@ex.Message"
Description="@ex.StackTrace">
</Alert>
</ErrorContent>
</ErrorBoundary>
}
}
}
else
@ -43,7 +56,7 @@
<Icon RefBack="context" Type="@(_copied?"check" : "snippets")" Class="code-box-code-copy code-box-code-action" OnClick="Copy" />
</Unbound>
</Tooltip>
<Tooltip Title="@(CodeExpand? Localizer["app.demo.code.hide"] : Localizer["app.demo.code.show"])">
<Tooltip Title="@(CodeExpand?Localizer["app.demo.code.hide"] : Localizer["app.demo.code.show"])">
<Unbound>
<span @ref="context.Current" class="code-expand-icon code-box-code-action" @onclick="_ => CodeExpand = !CodeExpand">
<img alt="expand code" src="https://gw.alipayobjects.com/zos/rmsportal/wSAkBuJFbdxsosKKpqyq.svg" class="@(CodeExpand ? "code-expand-icon-hide" : "code-expand-icon-show")">
@ -55,9 +68,14 @@
</div>
</section>
<section class="highlight-wrapper @(CodeExpand?"highlight-wrapper-expand":"")">
<div class="highlight">
@* <div class="highlight">
<HighlightedCode Code="@Demo.Code" CanLoad="CodeExpand" Language="html"></HighlightedCode>
</div>
</div> *@
@if (CodeExpand || _codeViewRendered)
{
_codeViewRendered = true;
<CodeView @bind-Value="@Demo.Code" Compiled="OnCompiled" />
}
</section>
<style>
@Demo.Style
@ -68,6 +86,7 @@
@inject DemoService demoService;
@inject ILocalizationService lang;
@inject IStringLocalizer Localizer;
@code {
[Parameter]
@ -85,31 +104,26 @@
private bool _copied;
private bool _rendered;
private bool _codeViewRendered;
private ErrorBoundary _errorBoundary;
protected string DemoId => $"components-{ComponentName.ToLower()}-demo-{Demo.Name.ToLower()}";
protected string DemoId => $"components-{ComponentName.ToLower()}-demo-{Demo.Name}";
private string EditUrl => $"https://github.com/ant-design-blazor/ant-design-blazor/edit/master/site/AntDesign.Docs/{Demo.Type.Replace(".", "/")}.razor";
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
lang.LanguageChanged += OnLangeChanged;
#if DEBUG
DebugIcon =
@<a href="@($"{(Demo.Link??"mock")}?type={Demo.Type}")" class="code-box-code-action" target="_blank">
@<a href="@(Demo.Link??$"mock?type={Demo.Type}")" class="code-box-code-action" target="_blank">
<Icon Type="bug" />
</a>
;
#endif
}
void IDisposable.Dispose()
{
lang.LanguageChanged -= OnLangeChanged;
}
async Task Copy()
{
await InteropService.Copy(Demo.Code);
@ -121,6 +135,11 @@
await InteropService.JsInvokeAsync("window.eval", $"window.location.hash='{DemoId}'");
}
void IDisposable.Dispose()
{
lang.LanguageChanged -= OnLangeChanged;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
@ -138,6 +157,13 @@
}
}
void OnCompiled(Type type)
{
_demoType = type;
_errorBoundary.Recover();
StateHasChanged();
}
private void OnLangeChanged(object sender, CultureInfo e)
{
_demoType = null;

View File

@ -0,0 +1,104 @@
@using System.Text.RegularExpressions;
@using AntDesign.Docs.Services
@inject IJSRuntime JSRuntime
@inject CompilerService Compiler
@if (_error != null)
{
<Alert Type="error">
@((MarkupString)_error)
</Alert>
}
@if (!_loaded)
{
<div class="highlight">
<HighlightedCode Code="@Value" CanLoad Language="html"></HighlightedCode>
</div>
}
<Monaco Value=@_source Language="@_language" ValueChanged=@OnValueChanged @ref="@_monaco" OnLoaded="OnLoaded" />
@code {
private string Copy => $"copy('{_monaco?.Id}')";
private Monaco _monaco;
private ElementReference pre;
private string _error;
private string _language = "razor";
private string _source;
private string _initValue;
private bool _loaded;
bool _loading;
[Parameter]
public EventCallback<Type> Compiled { get; set; }
[Parameter]
public string Value { get; set; }
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
protected override void OnParametersSet()
{
_language = "razor";
_initValue ??= Value;
_source = Value;
_error = null;
}
async Task OnValueChanged(string value)
{
_source = value;
await ValueChanged.InvokeAsync(value);
await Run();
}
void OnLoaded()
{
_loaded = true;
}
async Task Reset()
{
_source = _initValue;
await OnValueChanged(_source);
}
async Task CopyCode()
{
await JSRuntime.InvokeVoidAsync(Copy);
}
async Task Run()
{
try
{
_error = null;
_loading = true;
// await _monaco.SetReadOnly(true);
await Task.Yield();
var type = await Compiler.CompileAsync(_source);
await Compiled.InvokeAsync(type);
}
catch (Exception ex)
{
_error = ex.Message;
}
finally
{
// await _monaco.SetReadOnly(false);
_loading = false;
}
}
}

View File

@ -0,0 +1,124 @@
@implements IDisposable
<div @ref="_editor" style="height: 500px; width: 100%; @(_loaded?"":"display:hidden;")"></div>
@code {
[Inject]
private IJSRuntime JSRuntime { get; set; }
[Parameter]
public string Value { get; set; }
[Parameter]
public bool ReadOnly { get; set; }
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[Parameter]
public EventCallback<bool> OnLoaded { get; set; }
[Parameter]
public string Language { get; set; }
IJSObjectReference _monaco;
ElementReference _editor;
DotNetObjectReference<Monaco> _reference;
public string Id => _id;
private string _id;
private string _value;
private bool _loaded;
protected override void OnInitialized()
{
base.OnInitialized();
_id = Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace("/", "-").Replace("+", "-").Substring(0, 10);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_reference = DotNetObjectReference.Create(this);
_monaco = await JSRuntime.InvokeAsync<IJSObjectReference>("createEditor", _editor, _id, _reference, new
{
language = Language,
theme = "vs",
codeLens = false,
readOnly = ReadOnly,
minimap = new
{
enabled = false
},
automaticLaout = true
});
_value = Value;
await _monaco.InvokeVoidAsync("setValue", _value);
}
}
[JSInvokable]
public async Task OnChangeAsync(string value)
{
if (!_loaded)
{
_loaded = true;
if (OnLoaded.HasDelegate)
{
OnLoaded.InvokeAsync();
}
}
if (Value != value)
{
_value = value;
await ValueChanged.InvokeAsync(value);
}
}
public override async Task SetParametersAsync(ParameterView parameters)
{
var valueChanged = parameters.IsParameterChanged(nameof(Value), Value);
var readOnlyChanged = parameters.IsParameterChanged(nameof(ReadOnly), ReadOnly);
await base.SetParametersAsync(parameters);
if (_monaco != null && valueChanged && _value != Value)
{
_value = Value;
await _monaco.InvokeVoidAsync("setValue", _value);
}
if (_monaco != null && readOnlyChanged)
{
await _monaco.InvokeVoidAsync("setOptions", new { readOnly = ReadOnly });
}
}
public void Dispose()
{
if (JSRuntime != null && _monaco != null)
{
_monaco.InvokeVoidAsync("dispose");
}
_monaco?.DisposeAsync();
_reference?.Dispose();
}
public async Task SetReadOnly(bool isReadOnly)
{
if (_monaco != null)
{
await _monaco.InvokeVoidAsync("updateOptions", new { domReadOnly = isReadOnly, readOnly = isReadOnly });
}
}
}

View File

@ -1,37 +0,0 @@
@using AntDesign.Docs.Services
@using System.Reflection;
@inherits ComponentBase
@if (_demoType != null)
{
<DynamicComponent Type="_demoType" />
}
@inject DemoService demoService;
@code {
[Parameter]
public DemoItem Demo { get; set; }
private Type _demoType;
private bool _render;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
_render = true;
StateHasChanged();
return;
}
if (_render && _demoType == null)
{
_demoType = demoService.GetShowCase(Demo.Type);
StateHasChanged();
}
base.OnAfterRender(firstRender);
}
}