-
Notifications
You must be signed in to change notification settings - Fork 774
Fixes #5385. Add Markdown.RenderToAnsi() API for headless ANSI rendering #5388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| 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); | ||
| } | ||
| } | ||
| } | ||
| } | ||
123 changes: 123 additions & 0 deletions
123
Tests/UnitTestsParallelizable/Views/Markdown/MarkdownRenderToAnsiTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.