Skip to content
Open
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
130 changes: 130 additions & 0 deletions src/Agents/Extensions/Dedent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// <auto-generated />
#region License
// MIT License
//
// Copyright (c) Daniel Cazzulino
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#endregion

#nullable enable
using System.Text;
using System.Text.RegularExpressions;

namespace System
{
/// <summary>
/// String extension methods for text processing.
/// </summary>
static partial class StringExtensions
{
/// <summary>
/// Remove leading whitespace from each line of a multi-line string that is common
/// to all non-empty lines. This is equivalent to Python's textwrap.dedent().
/// </summary>
/// <param name="text">The text to dedent.</param>
/// <returns>The dedented text.</returns>
/// <example>
/// <code>
/// var text = """
/// Line 1
/// Line 2
/// Line 3
/// """;
/// var dedented = text.Dedent();
/// // Result:
/// // Line 1
/// // Line 2
/// // Line 3
/// </code>
/// </example>
public static string Dedent(this string text)
{
if (string.IsNullOrEmpty(text))
return text;

// Detect the line ending style used in the input
var lineEnding = Environment.NewLine;
if (text.Contains("\r\n"))
lineEnding = "\r\n";
else if (text.Contains('\r'))
lineEnding = "\r";
else if (text.Contains('\n'))
lineEnding = "\n";

// Split using regex to properly handle different line endings
var lines = NewLineExpr().Split(text);

// Remove leading and trailing empty lines
int start = 0;
int end = lines.Length - 1;

while (start < lines.Length && string.IsNullOrWhiteSpace(lines[start]))
start++;

while (end >= 0 && string.IsNullOrWhiteSpace(lines[end]))
end--;

if (start > end)
return string.Empty;

// Find the minimum indentation (ignoring empty lines)
int minIndent = int.MaxValue;
for (int i = start; i <= end; i++)
{
var line = lines[i];
if (!string.IsNullOrWhiteSpace(line))
{
int indent = 0;
while (indent < line.Length && char.IsWhiteSpace(line[indent]))
indent++;

minIndent = Math.Min(minIndent, indent);
}
}

if (minIndent == int.MaxValue || minIndent == 0)
minIndent = 0;

// Remove the common indentation from all lines
var result = new StringBuilder();
for (int i = start; i <= end; i++)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line))
{
if (i < end) // Don't add newline for last empty line
result.Append(lineEnding);
}
else
{
var dedentedLine = line.Length > minIndent ? line[minIndent..] : string.Empty;
result.Append(dedentedLine);
if (i < end)
result.Append(lineEnding);
}
}

return result.ToString();
}

[GeneratedRegex(@"\r\n|\r|\n")]
private static partial Regex NewLineExpr();
}
}
Loading