11#nullable enable
22
33using System ;
4+ using System . Buffers . Text ;
45using System . Collections . Immutable ;
6+ using System . Diagnostics ;
7+ using System . Diagnostics . CodeAnalysis ;
8+ using System . IO ;
9+ using System . IO . Compression ;
510using System . Text . RegularExpressions ;
611using System . Threading . Tasks ;
712using Discord ;
813using Discord . Interactions ;
914using LZStringCSharp ;
1015using Modix . Bot . Responders . AutoRemoveMessages ;
11- using Modix . Services . AutoRemoveMessage ;
1216using Modix . Services . CommandHelp ;
1317using Modix . Services . Utilities ;
18+ using ProtoBuf ;
1419
1520namespace Modix . Bot . Modules
1621{
17- [ ModuleHelp ( "SharpLab " , "Commands for working with SharpLab ." ) ]
18- public class SharpLabModule : InteractionModuleBase
22+ [ ModuleHelp ( "LabShortener " , "Commands for working with sharplab.io/lab.razor.fyi ." ) ]
23+ public partial class LabShortenerModule : InteractionModuleBase
1924 {
2025 private readonly AutoRemoveMessageService _autoRemoveMessageService ;
2126
22- public SharpLabModule ( AutoRemoveMessageService autoRemoveMessageService )
27+ public LabShortenerModule ( AutoRemoveMessageService autoRemoveMessageService )
2328 {
2429 _autoRemoveMessageService = autoRemoveMessageService ;
2530 }
2631
27- [ SlashCommand ( "sharplab " , "Shortens the provided link." ) ]
32+ [ SlashCommand ( "shorten " , "Shortens the provided link." ) ]
2833 public async Task LinkAsync (
2934 [ Summary ( description : "The link to shorten." ) ]
30- Uri uri )
35+ Uri uri )
3136 {
3237 var host = uri . Host ;
3338
@@ -43,7 +48,15 @@ public async Task LinkAsync(
4348
4449 var urlMarkdown = Format . Url ( $ "{ host } (click here)", uri . ToString ( ) ) ;
4550
46- var description = host . Equals ( "sharplab.io" ) && TryPrepareSharplabPreview ( uri . OriginalString , urlMarkdown . Length + 1 , out var preview )
51+ string ? preview = null ;
52+ var previewSuccess = host switch
53+ {
54+ "sharplab.io" => TryPrepareSharplabPreview ( uri . Fragment , urlMarkdown . Length + 1 , out preview ) ,
55+ "lab.razor.fyi" => TryPrepareRazorLabPreview ( uri . Fragment , urlMarkdown . Length + 1 , out preview ) ,
56+ _ => throw new UnreachableException ( "already checked for other hosts" ) ,
57+ } ;
58+
59+ var description = previewSuccess
4760 ? $ "{ urlMarkdown } \n { preview } "
4861 : urlMarkdown ;
4962
@@ -61,9 +74,9 @@ public async Task LinkAsync(
6174 await _autoRemoveMessageService . RegisterRemovableMessageAsync ( Context . User , embed , async e => await FollowupAsync ( embed : e . Build ( ) ) ) ;
6275 }
6376
64- private static bool TryPrepareSharplabPreview ( string url , int markdownLength , out string ? preview )
77+ private static bool TryPrepareSharplabPreview ( string fragment , int markdownLength , [ NotNullWhen ( true ) ] out string ? preview )
6578 {
66- if ( ! url . Contains ( "#v2:" ) )
79+ if ( ! fragment . StartsWith ( "#v2:" ) )
6780 {
6881 preview = null ;
6982 return false ;
@@ -72,7 +85,7 @@ private static bool TryPrepareSharplabPreview(string url, int markdownLength, ou
7285 try
7386 {
7487 // Decode the compressed code from the URL payload
75- var base64Text = url . Substring ( url . IndexOf ( "#v2:" ) + "#v2:" . Length ) ;
88+ var base64Text = fragment [ 4 .. ] ;
7689 var plainText = LZString . DecompressFromBase64 ( base64Text ) ;
7790
7891 // Extract the option and get the target language
@@ -87,7 +100,7 @@ private static bool TryPrepareSharplabPreview(string url, int markdownLength, ou
87100 sourceCode = ReplaceTokens ( sourceCode , _sharplabCSTokens ) ;
88101
89102 // Strip using directives
90- sourceCode = Regex . Replace ( sourceCode , @"using \w+(?:\.\w+)*;" , string . Empty ) ;
103+ sourceCode = RemoveUsings ( sourceCode ) ;
91104 }
92105 else if ( language is "il" )
93106 sourceCode = ReplaceTokens ( sourceCode , _sharplabILTokens ) ;
@@ -107,6 +120,47 @@ private static bool TryPrepareSharplabPreview(string url, int markdownLength, ou
107120 }
108121 }
109122
123+ private static bool TryPrepareRazorLabPreview ( string fragment , int markdownLength , [ NotNullWhen ( true ) ] out string ? preview )
124+ {
125+ if ( string . IsNullOrWhiteSpace ( fragment ) )
126+ {
127+ preview = null ;
128+ return false ;
129+ }
130+
131+ try
132+ {
133+ var bytes = Base64Url . DecodeFromChars ( fragment . AsSpan ( 1 ) ) ;
134+ using var deflateStream = new DeflateStream ( new MemoryStream ( bytes ) , CompressionMode . Decompress ) ;
135+
136+ var savedState = Serializer . Deserialize < RazorLabSavedState > ( deflateStream ) ;
137+ var selectedFile = savedState . Inputs [ savedState . SelectedInputIndex ] ;
138+
139+ if ( selectedFile . FileExtension != ".cs" )
140+ {
141+ preview = null ;
142+ return false ;
143+ }
144+
145+ var source = RemoveUsings ( selectedFile . Text ) ;
146+
147+ var maxPreviewLength = EmbedBuilder . MaxDescriptionLength - ( markdownLength + "```cs\n \n ```" . Length ) ;
148+
149+ preview = FormatUtilities . FormatCodeForEmbed ( "cs" , source , maxPreviewLength ) ;
150+
151+ return ! string . IsNullOrWhiteSpace ( preview ) ;
152+ }
153+ catch
154+ {
155+ preview = null ;
156+ return false ;
157+ }
158+ }
159+
160+ [ GeneratedRegex ( @"using \w+(?:\.\w+)*;" ) ]
161+ private static partial Regex UsingsRegex ( ) ;
162+ private static string RemoveUsings ( string sourceCode ) => UsingsRegex ( ) . Replace ( sourceCode , "" ) ;
163+
110164 private static string ReplaceTokens ( string sourceCode , ImmutableArray < string > tokens )
111165 {
112166 return Regex . Replace ( sourceCode , @"@(\d+|@)" , match =>
@@ -119,14 +173,13 @@ private static string ReplaceTokens(string sourceCode, ImmutableArray<string> to
119173 }
120174
121175 private static readonly ImmutableArray < string > _allowedHosts
122- = ImmutableArray . Create
123- (
124- "sharplab.io "
125- ) ;
176+ = [
177+ "sharplab.io" ,
178+ "lab.razor.fyi "
179+ ] ;
126180
127181 private static readonly ImmutableArray < string > _sharplabCSTokens
128- = ImmutableArray . Create ( new [ ]
129- {
182+ = [
130183 "using" ,
131184 "System" ,
132185 "class" ,
@@ -153,11 +206,10 @@ private static readonly ImmutableArray<string> _sharplabCSTokens
153206 "public static class Program" ,
154207 "Inspect.Allocations(() =>" ,
155208 "Inspect.MemoryGraph("
156- } ) ;
209+ ] ;
157210
158211 private static readonly ImmutableArray < string > _sharplabILTokens
159- = ImmutableArray . Create ( new [ ]
160- {
212+ = [
161213 "Main ()" ,
162214 "Program" ,
163215 "ConsoleApp" ,
@@ -169,6 +221,29 @@ private static readonly ImmutableArray<string> _sharplabILTokens
169221 "extends System.Object" ,
170222 ".method public hidebysig" ,
171223 "call void [System.Console]System.Console::WriteLine("
172- } ) ;
224+ ] ;
225+ }
226+
227+ // the below definitions were taken from https://github.com/jjonescz/DotNetLab at commit dedcefec241a1d32fe8a6683ccaa39ff40dc1730
228+ // as such they are licensed under the MIT license in that repo, https://github.com/jjonescz/DotNetLab/blob/dedcefec241a1d32fe8a6683ccaa39ff40dc1730/LICENSE
229+ [ ProtoContract ]
230+ sealed file record RazorLabInputCode
231+ {
232+ [ ProtoMember ( 1 ) ]
233+ public required string FileName { get ; init ; }
234+ [ ProtoMember ( 2 ) ]
235+ public required string Text { get ; init ; }
236+
237+ public string FileExtension => Path . GetExtension ( FileName ) ;
238+ }
239+
240+ [ ProtoContract ]
241+ sealed file record RazorLabSavedState
242+ {
243+ [ ProtoMember ( 1 ) ]
244+ public ImmutableArray < RazorLabInputCode > Inputs { get ; init ; }
245+
246+ [ ProtoMember ( 8 ) ]
247+ public int SelectedInputIndex { get ; init ; }
173248 }
174249}
0 commit comments