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
63 changes: 63 additions & 0 deletions Frontmatter.Test/BodyIndentationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Frontmatter.Test;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Tests that rewriting a document keeps the indentation of the body's first content line, so a body that
/// opens with an indented code block is not turned into a prose paragraph.
/// </summary>
[TestClass]
public class BodyIndentationTests
{
private static readonly string Nl = Environment.NewLine;

private static readonly string IndentedBody = $" int x = 1;{Nl} int y = 2;{Nl}{Nl}Text";

private static string Document() => $"---{Nl}title: T{Nl}---{Nl}{IndentedBody}{Nl}";

[TestMethod]
public void ExtractBody_IndentedCodeBlockFirst_KeepsIndentation() =>
Assert.AreEqual(IndentedBody, Frontmatter.ExtractBody(Document()));

[TestMethod]
public void RemoveFrontmatter_IndentedCodeBlockFirst_KeepsIndentation() =>
Assert.AreEqual($"{IndentedBody}{Nl}", Frontmatter.RemoveFrontmatter(Document()));

[TestMethod]
public void ReplaceFrontmatter_IndentedCodeBlockFirst_KeepsIndentation()
{
string result = Frontmatter.ReplaceFrontmatter(Document(), new Dictionary<string, object> { ["title"] = "U" });

Assert.AreEqual(IndentedBody, Frontmatter.ExtractBody(result));
}

[TestMethod]
public void AddFrontmatter_ExistingFrontmatterAndIndentedCodeBlockFirst_KeepsIndentation()
{
string result = Frontmatter.AddFrontmatter(Document(), new Dictionary<string, object> { ["date"] = 2024 });

Assert.AreEqual(IndentedBody, Frontmatter.ExtractBody(result));
}

[TestMethod]
public void AddFrontmatter_NoFrontmatterAndIndentedCodeBlockFirst_KeepsIndentation()
{
string result = Frontmatter.AddFrontmatter($" code{Nl}{Nl}Text{Nl}", new Dictionary<string, object> { ["title"] = "U" });

Assert.AreEqual($" code{Nl}{Nl}Text", Frontmatter.ExtractBody(result));
}

[TestMethod]
public void ExtractBody_TabIndentedFirstLineAfterBlankLines_DropsOnlyTheBlankLines()
{
string input = $"---{Nl}title: T{Nl}---{Nl}{Nl} {Nl}\tindented{Nl}{Nl}";

Assert.AreEqual("\tindented", Frontmatter.ExtractBody(input));
}

[TestMethod]
public void ExtractBody_WhitespaceOnlyBody_ReturnsEmpty() =>
Assert.AreEqual(string.Empty, Frontmatter.ExtractBody($"---{Nl}title: T{Nl}---{Nl} {Nl}\t{Nl}"));
}
34 changes: 30 additions & 4 deletions Frontmatter/Frontmatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
}

List<Dictionary<string, object>> frontmatterObjects = ExtractFrontmatterObjects(input, out _);
return frontmatterObjects.Count > 0 ? frontmatterObjects.First() : null;

Check warning on line 139 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Indexing at 0 should be used instead of the "Enumerable" extension method "First"

Check warning on line 139 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Indexing at 0 should be used instead of the "Enumerable" extension method "First"
}

/// <summary>
Expand Down Expand Up @@ -198,7 +198,7 @@

string yamlFrontmatter = YamlSerializer.SerializeYamlObject(frontmatter).Trim();
string nl = Environment.NewLine;
return $"{FrontmatterDelimiter}{nl}{yamlFrontmatter}{nl}{FrontmatterDelimiter}{nl}{input.Trim()}{nl}";
return $"{FrontmatterDelimiter}{nl}{yamlFrontmatter}{nl}{FrontmatterDelimiter}{nl}{TrimBody(input)}{nl}";
}

/// <summary>
Expand All @@ -220,7 +220,7 @@
ExtractFrontmatterObjects(input, out string body);
string yamlFrontmatter = YamlSerializer.SerializeYamlObject(frontmatter).Trim();
string nl = Environment.NewLine;
return $"{FrontmatterDelimiter}{nl}{yamlFrontmatter}{nl}{FrontmatterDelimiter}{nl}{body.Trim()}{nl}";
return $"{FrontmatterDelimiter}{nl}{yamlFrontmatter}{nl}{FrontmatterDelimiter}{nl}{TrimBody(body)}{nl}";
}

/// <summary>
Expand All @@ -239,7 +239,7 @@
}

ExtractFrontmatterObjects(input, out string body);
return body.Trim() + Environment.NewLine;
return TrimBody(body) + Environment.NewLine;
}

/// <summary>
Expand All @@ -253,7 +253,7 @@
Ensure.NotNull(input);

ExtractFrontmatterObjects(input, out string body);
return body.Trim();
return TrimBody(body);
}

/// <summary>
Expand Down Expand Up @@ -283,7 +283,7 @@
}

// Then add any remaining properties that weren't in the standard order
foreach (KeyValuePair<string, object> property in frontmatter)

Check warning on line 286 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 286 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (!sortedFrontmatter.ContainsKey(property.Key))
{
Expand Down Expand Up @@ -333,7 +333,7 @@
continue;
}

if (YamlSerializer.TryParseYamlObject(section, out Dictionary<string, object>? frontmatterObject) && frontmatterObject != null)

Check warning on line 336 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Change this condition so that it does not always evaluate to 'True'.

Check warning on line 336 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Change this condition so that it does not always evaluate to 'True'.
{
frontmatterObjects.Add(frontmatterObject);
}
Expand Down Expand Up @@ -478,7 +478,7 @@
lines.Add((start, i));
if (c == '\r' && i + 1 < input.Length && input[i + 1] == '\n')
{
i++;

Check warning on line 481 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 481 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.
}

start = i + 1;
Expand All @@ -488,6 +488,32 @@
return lines;
}

/// <summary>
/// Trims a document body for output, dropping the blank lines before its first content line and the
/// whitespace after its last. Unlike <see cref="string.Trim()"/>, it keeps the first content line's
/// indentation, so a body that opens with an indented code block or nested list content survives.
/// </summary>
/// <param name="body">The body to trim.</param>
/// <returns>The body without leading blank lines or trailing whitespace.</returns>
private static string TrimBody(string body)
{
int start = 0;
for (int i = 0; i < body.Length; i++)
{
char c = body[i];
if (c == '\n')
{
start = i + 1;
}
else if (!char.IsWhiteSpace(c))
{
break;
}
}

return body[start..].TrimEnd();
}

/// <summary>
/// Checks whether a line is a frontmatter delimiter, allowing trailing whitespace.
/// </summary>
Expand All @@ -513,7 +539,7 @@
}

// Then, add properties from dictionary b that don't exist in a
foreach (KeyValuePair<string, object> kvp in b)

Check warning on line 542 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 542 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (!combinedFrontmatterObject.ContainsKey(kvp.Key))
{
Expand Down
Loading