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

namespace ktsu.Frontmatter.Test;

using System.Collections.Generic;

using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Regression tests for #183: a string that starts or ends with whitespace must be written so that the
/// next read gets the same string back. YAML strips that whitespace from a plain scalar.
/// </summary>
[TestClass]
public class EdgeWhitespaceRoundTripTests
{
[TestMethod]
[DataRow("tab\t", DisplayName = "Trailing tab")]
[DataRow("x \t", DisplayName = "Trailing space and tab")]
[DataRow("x\t ", DisplayName = "Trailing tab and space")]
[DataRow("sp ", DisplayName = "Trailing space")]
[DataRow("\ttab", DisplayName = "Leading tab")]
[DataRow(" sp", DisplayName = "Leading space")]
public void AddFrontmatter_ValueWithEdgeWhitespace_RoundTrips(string value)
{
string document = Frontmatter.AddFrontmatter("Body", new Dictionary<string, object> { ["k"] = value, ["z"] = "1" });

Assert.AreEqual(value, Frontmatter.ExtractFrontmatter(document)!["k"]);
}

[TestMethod]
public void CombineFrontmatter_QuotedValueEndingInTab_StaysQuoted()
{
string input = "---\nk: \"tab\\t\"\nz: 1\n---\nBody\n";

string combined = Frontmatter.CombineFrontmatter(input, FrontmatterNaming.AsIs, FrontmatterOrder.AsIs, FrontmatterMergeStrategy.None);

Assert.AreEqual("tab\t", Frontmatter.ExtractFrontmatter(combined)!["k"]);
}

[TestMethod]
public void SerializeYamlObject_ValueEndingInTab_ParsesBackUnchanged()
{
string yaml = YamlSerializer.SerializeYamlObject(new Dictionary<string, object> { ["k"] = "tab\t", ["z"] = "1" });

Assert.IsTrue(YamlSerializer.TryParseYamlObject(yaml, out Dictionary<string, object>? parsed));
Assert.AreEqual("tab\t", parsed["k"]);
}

[TestMethod]
public void SerializeYamlObject_PlainValue_StaysPlain()
{
string yaml = YamlSerializer.SerializeYamlObject(new Dictionary<string, object> { ["k"] = "plain value" });

Assert.AreEqual("k: plain value", yaml.Trim());
}
}
21 changes: 21 additions & 0 deletions Frontmatter/YamlSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.EventEmitters;
using YamlDotNet.Serialization.NamingConventions;

/// <summary>
Expand Down Expand Up @@ -43,6 +44,7 @@
.WithNamingConvention(NullNamingConvention.Instance)
.ConfigureDefaultValuesHandling(DefaultValuesHandling.Preserve)
.WithQuotingNecessaryStrings()
.WithEventEmitter(next => new EdgeWhitespaceQuotingEventEmitter(next))
.Build();

/// <summary>
Expand All @@ -51,7 +53,7 @@
/// <param name="input">The YAML string to parse.</param>
/// <param name="result">When this method returns, contains the deserialized dictionary if parsing succeeded, or null if parsing failed.</param>
/// <returns>true if the YAML was successfully parsed; otherwise, false.</returns>
public static bool TryParseYamlObject(string input, [NotNullWhen(true)] out Dictionary<string, object>? result)

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

Check warning on line 56 in Frontmatter/YamlSerializer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.
{
result = null;

Expand Down Expand Up @@ -214,6 +216,25 @@
public int ValueCount { get; set; }
}

/// <summary>
/// Double-quotes any string that starts or ends with whitespace. YAML strips that whitespace from a
/// plain scalar, and <c>WithQuotingNecessaryStrings</c> does not quote a string that ends in a tab, so
/// without this the value would come back changed on the next read.
/// </summary>
private sealed class EdgeWhitespaceQuotingEventEmitter(IEventEmitter nextEmitter) : ChainedEventEmitter(nextEmitter)
{
public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter)
{
if (eventInfo.Source.Value is string { Length: > 0 } text
&& (char.IsWhiteSpace(text[0]) || char.IsWhiteSpace(text[^1])))
{
eventInfo.Style = ScalarStyle.DoubleQuoted;
}

base.Emit(eventInfo, emitter);
}
}

/// <summary>
/// Compares objects by reference. ReferenceEqualityComparer is not available on .NET Standard.
/// </summary>
Expand Down
Loading