| File: Utils\Markdown\MarkdownToSpectreConverter.Renderable.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// 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 Markdig.Extensions.Tables; using Markdig.Syntax; using Spectre.Console; using Spectre.Console.Rendering; using MarkdigTable = Markdig.Extensions.Tables.Table; using MarkdigTableCell = Markdig.Extensions.Tables.TableCell; using MarkdigTableRow = Markdig.Extensions.Tables.TableRow; using SpectreTable = Spectre.Console.Table; namespace Aspire.Cli.Utils.Markdown; internal partial class MarkdownToSpectreConverter { /// <summary> /// Converts markdown text to a Spectre.Console renderable tree for CLI display. /// </summary> /// <param name="markdown">The markdown text to convert.</param> /// <param name="plainTextLinks">When <c>true</c>, links are rendered as <c>text (url)</c> instead of terminal hyperlinks.</param> /// <returns>The converted Spectre.Console renderable.</returns> public static IRenderable ConvertToRenderable(string markdown, bool plainTextLinks = false) { if (string.IsNullOrWhiteSpace(markdown)) { return Text.Empty; } var converter = new MarkdownToSpectreConverter(plainTextLinks); var document = ParseMarkdown(markdown, out var normalizedMarkdown); var renderables = converter.RenderBlocksToRenderables(document, normalizedMarkdown); return renderables.Count switch { 0 => Text.Empty, 1 => renderables[0], _ => new Rows(renderables) }; } private List<IRenderable> RenderBlocksToRenderables(ContainerBlock container, string markdown) { var renderables = new List<IRenderable>(); foreach (var block in container) { var renderable = RenderBlockToRenderable(block, markdown); if (renderable is not null) { if (renderables.Count > 0) { renderables.Add(Text.Empty); } renderables.Add(renderable); } } return renderables; } private IRenderable? RenderBlockToRenderable(Block block, string markdown) { return RenderBlockContentToRenderable(block, markdown); } private IRenderable? RenderBlockContentToRenderable(Block block, string markdown) => block switch { ThematicBreakBlock => new Markup(GetOriginalMarkdownSpan(block.Span, markdown).ToString().EscapeMarkup()), QuoteBlock quote => RenderQuoteToRenderable(quote, markdown), ListBlock list => RenderListToRenderable(list, markdown), MarkdigTable table => RenderTableToRenderable(table, markdown), _ => CreateMarkupRenderable(block, markdown) }; private IRenderable? CreateMarkupRenderable(Block block, string markdown) { var builder = new StringBuilder(); AppendBlockToMarkup(builder, block, markdown); return builder.Length == 0 ? null : new Markup(builder.ToString()); } private IRenderable? RenderContainerContentToRenderable(ContainerBlock container, string markdown) { var renderables = new List<IRenderable>(); Block? previousBlock = null; foreach (var block in container) { var renderable = RenderBlockContentToRenderable(block, markdown); if (renderable is null) { continue; } if (renderables.Count > 0 && ShouldInsertBlankLineBetween(previousBlock, block)) { renderables.Add(Text.Empty); } renderables.Add(renderable); previousBlock = block; } return renderables.Count switch { 0 => null, 1 => renderables[0], _ => new Rows(renderables) }; } private static bool ShouldInsertBlankLineBetween(Block? previous, Block current) { if (previous is null) { return false; } // Keep nested list and quote content tightly coupled to the preceding block. return current is not ListBlock and not QuoteBlock; } private IRenderable RenderQuoteToRenderable(QuoteBlock quote, string markdown) { var grid = new Grid(); grid.AddColumn(); grid.AddColumn(); grid.Columns[0].NoWrap = true; grid.Columns[0].Padding = new Padding(0); grid.Columns[1].Padding = new Padding(0); grid.AddRow( new Markup("[grey]>[/] "), RenderContainerContentToRenderable(quote, markdown) ?? Text.Empty); return grid; } private IRenderable RenderListToRenderable(ListBlock list, string markdown) { var items = list.OfType<ListItemBlock>().ToList(); if (items.Count == 0) { return Text.Empty; } var orderedStart = int.TryParse(list.OrderedStart, out var parsedOrderedStart) ? parsedOrderedStart : 1; var grid = new Grid(); grid.AddColumn(); grid.AddColumn(); grid.Columns[0].NoWrap = true; grid.Columns[0].Padding = new Padding(0); grid.Columns[1].Padding = new Padding(0); var index = orderedStart; foreach (var item in items) { var content = RenderContainerContentToRenderable(item, markdown); if (content is null) { continue; } var marker = list.IsOrdered ? $"{index++}. " : "• "; grid.AddRow(new Markup(marker.EscapeMarkup()), content); } return grid; } private IRenderable RenderTableToRenderable(MarkdigTable markdownTable, string markdown) { var rows = markdownTable.OfType<MarkdigTableRow>().ToList(); if (rows.Count == 0) { return Text.Empty; } var columnCount = rows.Max(static row => row.Count); var headerRow = rows.FirstOrDefault(static row => row.IsHeader); var spectreTable = new SpectreTable(); for (var i = 0; i < columnCount; i++) { var headerCell = headerRow is not null && i < headerRow.Count ? (MarkdigTableCell)headerRow[i] : null; var headerMarkup = headerCell is not null ? RenderTableCellToMarkup(headerCell, markdown) : string.Empty; var column = new TableColumn(string.IsNullOrEmpty(headerMarkup) ? Text.Empty : new Markup(headerMarkup)); if (markdownTable.ColumnDefinitions is { Count: > 0 } && i < markdownTable.ColumnDefinitions.Count) { var alignment = markdownTable.ColumnDefinitions[i].Alignment; if (alignment is not null) { column.Alignment = alignment switch { TableColumnAlign.Left => Justify.Left, TableColumnAlign.Center => Justify.Center, TableColumnAlign.Right => Justify.Right, _ => column.Alignment }; } } spectreTable.AddColumn(column); } foreach (var row in rows) { if (row.IsHeader) { continue; } var cells = new IRenderable[columnCount]; for (var i = 0; i < columnCount; i++) { if (i < row.Count) { var markup = RenderTableCellToMarkup((MarkdigTableCell)row[i], markdown); cells[i] = string.IsNullOrEmpty(markup) ? Text.Empty : new Markup(markup); } else { cells[i] = Text.Empty; } } spectreTable.AddRow(cells); } return spectreTable; } }