| File: Model\Markdown\HighlightedCodeBlockRenderer.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Text; using Aspire.Dashboard.Extensions; using Aspire.Dashboard.Resources; using Markdig.Renderers; using Markdig.Renderers.Html; using Markdig.Syntax; using Microsoft.Extensions.Localization; using Microsoft.FluentUI.AspNetCore.Components; using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons; namespace Aspire.Dashboard.Model.Markdown; public class HighlightedCodeBlockRenderer : HtmlObjectRenderer<CodeBlock> { private static readonly Icon s_copyIcon = new Icons.Regular.Size16.Copy(); private static readonly Icon s_checkmarkIcon = new Icons.Regular.Size16.Checkmark(); private readonly IStringLocalizer<ControlsStrings> _loc; public HighlightedCodeBlockRenderer(IStringLocalizer<ControlsStrings> loc) { _loc = loc; } protected override void Write(HtmlRenderer renderer, CodeBlock obj) { // Markdown output can sometimes contain blank lines at the start and end of a code block. // Check for blank lines in these places and remove them before rendering to HTML. if (obj.Lines.Count > 0) { // Remove first line if blank line if (obj.Lines.Lines[0].Slice.Length == 0) { obj.Lines.RemoveAt(0); } // Remove last line if blank line if (obj.Lines.Count > 0 && obj.Lines.Lines[obj.Lines.Count - 1].Slice.Length == 0) { obj.Lines.RemoveAt(obj.Lines.Count - 1); } } // Don't render anything until code block content is available. // This means the complete syntax is available when rendering. if (obj.Lines.Count == 0 && obj.IsOpen) { return; } var codeAttributes = new HtmlAttributes(); // If there isn't a language specified then default to generic "code" string title; if (obj is FencedCodeBlock fencedCode && fencedCode.Info is { Length: > 0 } info) { // Language is added to a CSS class name for highlightjs. // Fix known languages that contain invalid CSS class name characters. title = info.ToLower() switch { "c#" => "csharp", "c++" => "cpp", _ => info.ToLower() }; codeAttributes.AddClass($"language-{title}"); // This isn't used by highlightjs but it might be useful to see the underlying value in the HTML. codeAttributes.AddProperty("data-language", info); } else { title = "code"; } // Add copy attributes to the copy button. var rawCode = GetRawCodeText(obj); var attributes = FluentUIExtensions.GetClipboardCopyAdditionalAttributes(rawCode, _loc[nameof(ControlsStrings.GridValueCopyToClipboard)], _loc[nameof(ControlsStrings.GridValueCopied)]); var copyButtonAttributes = new HtmlAttributes(); copyButtonAttributes.AddClass("code-copy-button"); copyButtonAttributes.AddProperty("id", $"code-copy-button-{obj.Span.Start}"); foreach (var item in attributes) { copyButtonAttributes.AddProperty(item.Key, item.Value.ToString()!); } // Render the code block along with surrounding divs for styling, positioning, and the copy button. renderer.EnsureLine(); renderer.Write("<pre>"); renderer.Writer.Write(@"<div class=""code-block"">"); renderer.Writer.Write(@"<div class=""code-title"">"); renderer.Writer.Write(title); renderer.Writer.Write("</div>"); renderer.Writer.Write(@"<div class=""code-buttons-anchor"">"); renderer.Writer.Write(@"<div class=""code-buttons-hover"">"); renderer.Writer.Write("<button"); renderer.WriteAttributes(copyButtonAttributes); renderer.Writer.Write('>'); renderer.Writer.Write(@"<div class=""copy-icon"">"); renderer.Writer.Write(ToMarkup(s_copyIcon)); renderer.Writer.Write("</div>"); renderer.Writer.Write(@"<div class=""checkmark-icon"" style=""display:none;"">"); renderer.Writer.Write(ToMarkup(s_checkmarkIcon)); renderer.Writer.Write("</div>"); renderer.Writer.Write("</button>"); renderer.Writer.Write("</div>"); renderer.Writer.Write("</div>"); renderer.Write(@"<div class=""code-container"">"); renderer.Write("<code"); renderer.WriteAttributes(codeAttributes); renderer.Writer.Write('>'); renderer.WriteLeafRawLines(obj, true, true); renderer.Writer.Write("</code>"); renderer.Writer.Write("</div>"); renderer.Writer.Write("</div>"); renderer.Writer.Write("</pre>"); renderer.EnsureLine(); } public static string GetRawCodeText(CodeBlock codeBlock) { var sb = new StringBuilder(); var slices = codeBlock.Lines.Lines; if (slices is not null) { for (var i = 0; i < slices.Length; i++) { ref var slice = ref slices[i].Slice; if (slice.Text is null) { break; } sb.Append(slice.AsSpan()); sb.AppendLine(); } } return sb.ToString(); } public static string ToMarkup(Icon icon) { var sizePx = (int)icon.Size; var size = $"{sizePx}px"; return $@"<svg viewBox=""0 0 {sizePx} {sizePx}"" width=""{size}"" fill=""var(--accent-fill-rest)"" style=""width: {size};"" aria-hidden=""true"">{icon.Content}</svg>"; } }