Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions Terminal.Gui/Views/Markdown/MarkdownView.Ansi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using Terminal.Gui.App;
using Terminal.Gui.Drivers;

namespace Terminal.Gui.Views;

public partial class Markdown
{
private const int MAX_RENDER_WIDTH = 4096;
private static readonly Lock _renderToAnsiLock = new ();

/// <summary>
/// Renders the current <see cref="Text"/> (or the supplied <paramref name="markdown"/>)
/// to an ANSI escape-sequence string suitable for writing directly to a terminal.
/// </summary>
/// <param name="markdown">
/// Optional markdown text to render. If <see langword="null"/>, uses the current <see cref="Text"/>.
/// </param>
/// <param name="width">
/// The target column width for word-wrapping. Defaults to 80. Clamped to
/// the range [<see cref="MIN_WRAP_WIDTH"/>, 4096].
/// </param>
/// <returns>A string containing ANSI escape sequences that reproduce the styled markdown output.</returns>
/// <remarks>
/// <para>
/// This method does not require <see cref="Application"/> to be initialized. It creates a
/// temporary headless ANSI driver internally, performs layout and drawing into an off-screen
/// buffer, and returns the ANSI representation.
/// </para>
/// <para>
/// Configuration properties set on this instance — <see cref="SyntaxHighlighter"/>,
/// <see cref="MarkdownPipeline"/>, <see cref="UseThemeBackground"/>, and
/// <see cref="ShowHeadingPrefix"/> — are copied to the temporary view used for rendering.
/// Copy buttons (<see cref="ShowCopyButtons"/>) are always disabled for ANSI output because
/// they are interactive-only controls.
/// </para>
/// </remarks>
public string RenderToAnsi (string? markdown = null, int width = 80)
{
if (width < MIN_WRAP_WIDTH)
{
width = MIN_WRAP_WIDTH;
}
Comment thread
tig marked this conversation as resolved.
else if (width > MAX_RENDER_WIDTH)
{
width = MAX_RENDER_WIDTH;
}

string text = markdown ?? Text;

if (string.IsNullOrEmpty (text))
{
return string.Empty;
}

// A static lock guards the process-wide DisableRealDriverIO environment variable
// so concurrent calls do not race on set/restore.
lock (_renderToAnsiLock)
{
string? previousValue = Environment.GetEnvironmentVariable ("DisableRealDriverIO");
Environment.SetEnvironmentVariable ("DisableRealDriverIO", "1");

try
{
using IApplication app = Application.Create ().Init (DriverRegistry.Names.ANSI);

// Use a small initial height for the first layout pass (just enough to compute content height)
app.Driver!.SetScreenSize (width, 1);

using Markdown renderView = new ()
{
Text = text,
Width = Dim.Fill (),
Height = Dim.Fill (),
SyntaxHighlighter = SyntaxHighlighter,
MarkdownPipeline = MarkdownPipeline,
UseThemeBackground = UseThemeBackground,
ShowHeadingPrefix = ShowHeadingPrefix,
ShowCopyButtons = false // Copy buttons are interactive-only
};

renderView.App = app;
renderView.SetRelativeLayout (app.Screen.Size);
renderView.Layout ();

int contentHeight = renderView.GetContentHeight ();

if (contentHeight < 1)
{
return string.Empty;
}

// Resize to the full content height so the entire document is drawn
app.Driver.SetScreenSize (width, contentHeight);
renderView.Frame = app.Screen with { X = 0, Y = 0 };
renderView.Layout ();
app.Driver.ClearContents ();
renderView.Draw ();

return app.Driver.ToAnsi ();
}
finally
{
Environment.SetEnvironmentVariable ("DisableRealDriverIO", previousValue);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using JetBrains.Annotations;
using Terminal.Gui.Drawing;

namespace ViewsTests.Markdown;

// Copilot

[TestSubject (typeof (Terminal.Gui.Views.Markdown))]
public class MarkdownRenderToAnsiTests
{
[Fact]
public void RenderToAnsi_BasicMarkdown_ReturnsNonEmptyAnsi ()
{
Terminal.Gui.Views.Markdown view = new () { Text = "# Hello\n\nWorld" };

string result = view.RenderToAnsi ();

Assert.NotEmpty (result);
// ANSI escape sequences start with ESC [
Assert.Contains ("\x1b[", result);
// The text content should be present
Assert.Contains ("Hello", result);
Assert.Contains ("World", result);
}

[Fact]
public void RenderToAnsi_WithMarkdownParameter_OverridesText ()
{
Terminal.Gui.Views.Markdown view = new () { Text = "Original" };

string result = view.RenderToAnsi ("**Override**");

Assert.Contains ("Override", result);
Assert.DoesNotContain ("Original", result);
}

[Fact]
public void RenderToAnsi_EmptyText_ReturnsEmpty ()
{
Terminal.Gui.Views.Markdown view = new ();

string result = view.RenderToAnsi ("");

Assert.Equal (string.Empty, result);
}

[Fact]
public void RenderToAnsi_NullMarkdownUsesInstanceText ()
{
Terminal.Gui.Views.Markdown view = new () { Text = "# Title" };

string result = view.RenderToAnsi (null);

Assert.Contains ("Title", result);
}

[Fact]
public void RenderToAnsi_WidthAffectsWrapping ()
{
string longLine = "This is a very long line that should be wrapped when the width is narrow enough to force wrapping behavior.";
Terminal.Gui.Views.Markdown view = new () { Text = longLine };

string narrow = view.RenderToAnsi (width: 20);
string wide = view.RenderToAnsi (width: 200);

// Narrow output should have more newlines (more lines due to wrapping)
int narrowNewlines = narrow.Split ('\n').Length;
int wideNewlines = wide.Split ('\n').Length;
Assert.True (narrowNewlines > wideNewlines, $"Narrow ({narrowNewlines} lines) should have more lines than wide ({wideNewlines} lines)");
}

[Fact]
public void RenderToAnsi_WithSyntaxHighlighter_ProducesOutput ()
{
Terminal.Gui.Views.Markdown view = new ()
{
Text = "```csharp\nint x = 42;\n```",
SyntaxHighlighter = new TextMateSyntaxHighlighter ()
};

string result = view.RenderToAnsi ();

Assert.NotEmpty (result);
Assert.Contains ("42", result);
}

[Fact]
public void RenderToAnsi_SmallWidth_ClampsToMinimum ()
{
Terminal.Gui.Views.Markdown view = new () { Text = "Hello" };

// Width below MIN_WRAP_WIDTH (4) should not throw
string result = view.RenderToAnsi (width: 1);

Assert.NotEmpty (result);
}

[Fact]
public void RenderToAnsi_DoesNotMutateInstance ()
{
Terminal.Gui.Views.Markdown view = new () { Text = "# Original" };

_ = view.RenderToAnsi ("# Different");

// Original text should be unchanged
Assert.Equal ("# Original", view.Text);
}

[Fact]
public void RenderToAnsi_UseThemeBackground_False_ProducesOutput ()
{
Terminal.Gui.Views.Markdown view = new ()
{
Text = "# Hello",
UseThemeBackground = false
};

string result = view.RenderToAnsi ();

Assert.NotEmpty (result);
Assert.Contains ("Hello", result);
}
}
Loading