Skip to content

Commit 42021d6

Browse files
authored
Merge pull request #6 from RoboKiwi/feature/monaco-code-blocks
Monaco extension PoC
2 parents 38f776b + 4df57db commit 42021d6

20 files changed

+2622
-15
lines changed

Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<ImplicitUsings>enable</ImplicitUsings>
55
<Nullable>enable</Nullable>
66
<LangVersion>latest</LangVersion>
7+
<Version>0.1.8-alpha</Version>
78
</PropertyGroup>
89
<ItemGroup>
910
<InternalsVisibleTo Include="SiteGen.Tests"></InternalsVisibleTo>

SiteGen.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SiteGen.Extensions.Markdown
1111
EndProject
1212
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SiteGen.Tests", "tests\SiteGen.Tests\SiteGen.Tests.csproj", "{EA6501B2-937C-451E-9CEA-872AD9B1F15B}"
1313
EndProject
14+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SiteGen.Extensions.Markdown.Monaco", "src\SiteGen.Extensions.Markdown.Monaco\SiteGen.Extensions.Markdown.Monaco.csproj", "{17FEE8E7-1E95-4649-9C1A-6C148E60C11A}"
15+
EndProject
1416
Global
1517
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1618
Debug|Any CPU = Debug|Any CPU
@@ -33,6 +35,10 @@ Global
3335
{EA6501B2-937C-451E-9CEA-872AD9B1F15B}.Debug|Any CPU.Build.0 = Debug|Any CPU
3436
{EA6501B2-937C-451E-9CEA-872AD9B1F15B}.Release|Any CPU.ActiveCfg = Release|Any CPU
3537
{EA6501B2-937C-451E-9CEA-872AD9B1F15B}.Release|Any CPU.Build.0 = Release|Any CPU
38+
{17FEE8E7-1E95-4649-9C1A-6C148E60C11A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39+
{17FEE8E7-1E95-4649-9C1A-6C148E60C11A}.Debug|Any CPU.Build.0 = Debug|Any CPU
40+
{17FEE8E7-1E95-4649-9C1A-6C148E60C11A}.Release|Any CPU.ActiveCfg = Release|Any CPU
41+
{17FEE8E7-1E95-4649-9C1A-6C148E60C11A}.Release|Any CPU.Build.0 = Release|Any CPU
3642
EndGlobalSection
3743
GlobalSection(SolutionProperties) = preSolution
3844
HideSolutionNode = FALSE

src/SiteGen.Cli/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"profiles": {
33
"SiteGen.Cli": {
44
"commandName": "Project",
5-
"commandLineArgs": "serve"
5+
"commandLineArgs": ""
66
}
77
}
88
}

src/SiteGen.Cli/SiteGen.Cli.csproj

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
<ToolCommandName>sitegen</ToolCommandName>
77
<Title>SiteGen</Title>
88
<FileVersion></FileVersion>
9-
<Version>0.1.3-alpha</Version>
109
</PropertyGroup>
1110

1211
<ItemGroup>
13-
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor" Version="2.2.0" />
14-
<PackageReference Include="Microsoft.AspNetCore.Razor.Runtime" Version="2.2.0" />
12+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor" Version="2.3.0" />
13+
<PackageReference Include="Microsoft.AspNetCore.Razor.Runtime" Version="2.3.0" />
1514
</ItemGroup>
1615

1716
<ItemGroup>

src/SiteGen.Core/Extensions/SpanExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ namespace SiteGen.Core.Extensions;
55

66
public static class SpanExtensions
77
{
8+
public static ReadOnlySpan<char> StripStart(this ReadOnlySpan<char> value, ReadOnlySpan<char> x)
9+
{
10+
if (value == null || x == null || x.Length > value.Length) return value;
11+
return value.IndexOf(x) == 0 ? value[x.Length..] : value;
12+
}
13+
814
public static bool IsEmpty([NotNullWhen(false)] this string? value)
915
{
1016
return string.IsNullOrWhiteSpace(value);

src/SiteGen.Core/SiteGen.Core.csproj

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@
22

33
<PropertyGroup>
44
<OutputType>Library</OutputType>
5-
<Version>0.1.3-alpha</Version>
65
<IsPackable>true</IsPackable>
76
</PropertyGroup>
87

98
<ItemGroup>
10-
<PackageReference Include="Markdig" Version="0.33.0" />
11-
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
12-
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
13-
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
14-
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
15-
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
16-
<PackageReference Include="Microsoft.Playwright" Version="1.40.0" />
9+
<None Include="..\..\Directory.Build.props" Link="Directory.Build.props" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Markdig" Version="0.41.2" />
14+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
15+
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
16+
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" />
17+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.5" />
18+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
19+
<PackageReference Include="Microsoft.Playwright" Version="1.52.0" />
1720
<PackageReference Include="Tommy.Extensions.Configuration" Version="6.2.0" />
18-
<PackageReference Include="YamlDotNet" Version="13.3.1" />
21+
<PackageReference Include="YamlDotNet" Version="16.3.0" />
1922
</ItemGroup>
2023

2124
</Project>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using Markdig.Parsers;
2+
using Markdig.Syntax;
3+
4+
namespace SiteGen.Extensions.Markdown.Monaco;
5+
6+
public class MonacoCodeBlock(BlockParser parser) : FencedCodeBlock(parser)
7+
{
8+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using Markdig.Helpers;
2+
using Markdig.Parsers;
3+
using Markdig.Renderers.Html;
4+
using Markdig.Syntax;
5+
6+
namespace SiteGen.Extensions.Markdown.Monaco;
7+
8+
public class MonacoCodeBlockParser : FencedBlockParserBase<MonacoCodeBlock>
9+
{
10+
public MonacoCodeBlockParser()
11+
{
12+
OpeningCharacters = new[] { '`' };
13+
MinimumMatchCount = 3;
14+
MaximumMatchCount = 3;
15+
InfoPrefix = "language-";
16+
InfoParser = MonacoCodeInfoParser;
17+
DefaultClass = "";
18+
}
19+
20+
public string? DefaultClass { get; private set; }
21+
22+
protected override MonacoCodeBlock CreateFencedBlock(BlockProcessor processor)
23+
{
24+
var block = new MonacoCodeBlock(this);
25+
if (DefaultClass != null)
26+
{
27+
block.GetAttributes().AddClass(DefaultClass);
28+
}
29+
return block;
30+
}
31+
32+
private bool MonacoCodeInfoParser(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter)
33+
{
34+
var matches = DefaultInfoParser(state, ref line, fenced, openingCharacter);
35+
if (!matches) return false;
36+
return !line.MatchLowercase("mermaid");
37+
}
38+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Markdig.Renderers;
2+
using Markdig.Renderers.Html;
3+
4+
namespace SiteGen.Extensions.Markdown.Monaco;
5+
6+
public class MonacoCodeBlockRenderer(MonacoHost host) : HtmlObjectRenderer<MonacoCodeBlock>
7+
{
8+
readonly MonacoHost host = host;
9+
10+
protected override void Write(HtmlRenderer renderer, MonacoCodeBlock obj)
11+
{
12+
renderer.EnsureLine();
13+
14+
if (renderer.EnableHtmlForBlock)
15+
{
16+
// Get the code language, defaulting to text.
17+
var lang = obj.Info?.ToString() ?? "text";
18+
if (string.IsNullOrWhiteSpace(lang)) lang = "text";
19+
20+
renderer.Write("<pre").WriteAttributes(obj).Write(">");
21+
22+
try
23+
{
24+
var contents = obj.Lines.ToString();
25+
var output = host.Highlight(contents, lang).GetAwaiter().GetResult();
26+
27+
renderer.Write("<code>");
28+
renderer.Write(output);
29+
renderer.Write("</code>");
30+
}
31+
catch (Exception ex)
32+
{
33+
renderer.Write("<code>");
34+
renderer.Write(ex.ToString());
35+
renderer.Write("</code>");
36+
}
37+
finally
38+
{
39+
renderer.WriteLine("</pre>");
40+
}
41+
}
42+
else
43+
{
44+
renderer.WriteLeafRawLines(obj, true, renderer.EnableHtmlEscape);
45+
}
46+
}
47+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using Microsoft.AspNetCore.Hosting.Server.Features;
2+
using Microsoft.AspNetCore.Hosting.Server;
3+
using Microsoft.Playwright;
4+
using System.Reflection;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.AspNetCore.Hosting;
8+
using SiteGen.Core.Extensions;
9+
10+
namespace SiteGen.Extensions.Markdown.Monaco;
11+
12+
public class MonacoHost : IAsyncDisposable
13+
{
14+
const string url = "http://127.0.0.1:0";
15+
readonly DirectoryInfo directory = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), ".monaco"));
16+
IPage page;
17+
WebApplication app;
18+
static bool isInitialized;
19+
20+
static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
21+
22+
public MonacoHost(IPage page)
23+
{
24+
this.page = page;
25+
26+
var options = new WebApplicationOptions
27+
{
28+
WebRootPath = directory.FullName,
29+
ContentRootPath = directory.FullName,
30+
ApplicationName = "monaco"
31+
};
32+
33+
if (!directory.Exists) directory.Create();
34+
35+
var builder = WebApplication.CreateSlimBuilder(options);
36+
37+
builder.WebHost.UseUrls(url);
38+
39+
app = builder.Build();
40+
41+
app
42+
.UseDefaultFiles()
43+
.UseStaticFiles(new StaticFileOptions { ServeUnknownFileTypes = true });
44+
}
45+
46+
public async ValueTask DisposeAsync()
47+
{
48+
await page.CloseAsync();
49+
await app.StopAsync();
50+
await app.DisposeAsync();
51+
}
52+
53+
public async Task<string> Highlight(string source, string language)
54+
{
55+
if (!isInitialized)
56+
{
57+
await semaphore.WaitAsync();
58+
59+
try
60+
{
61+
if (!isInitialized)
62+
{
63+
// Purge the directory
64+
directory.Delete(true);
65+
directory.Create();
66+
67+
var type = GetType()!;
68+
var resourceNameBase = type.Namespace! + ".dist";
69+
70+
// Write resources to the root directory
71+
var assembly = Assembly.GetExecutingAssembly();
72+
foreach (var name in assembly.GetManifestResourceNames())
73+
{
74+
var ext = Path.GetExtension(name);
75+
var filename = Path.GetFileNameWithoutExtension(name.AsSpan().StripStart(resourceNameBase.AsSpan()).ToString()).TrimStart('.');
76+
77+
using var stream = assembly.GetManifestResourceStream(name);
78+
using var destination = File.Open(Path.Combine(directory.FullName, $"{filename}{ext}"), FileMode.Create, FileAccess.Write);
79+
stream.CopyTo(destination);
80+
stream.Flush();
81+
}
82+
83+
await Task.Run(() => app.StartAsync());
84+
85+
// Get the address that was bound to (random port)
86+
var server = app.Services.GetRequiredService<IServer>();
87+
var addressFeature = server.Features.Get<IServerAddressesFeature>()!;
88+
var address = addressFeature.Addresses.Single();
89+
90+
await page.GotoAsync(address);
91+
await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
92+
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
93+
await page.WaitForLoadStateAsync(LoadState.Load);
94+
95+
isInitialized = true;
96+
}
97+
}
98+
finally
99+
{
100+
semaphore.Release();
101+
}
102+
}
103+
104+
// Set the source code
105+
return await page.EvaluateAsync<string>($@"(source) => {{
106+
return monaco.editor.colorize(source, ""{language.ToLowerInvariant()}\\"", {{}});
107+
}}", source);
108+
}
109+
}

0 commit comments

Comments
 (0)