diff --git a/Frontmatter.Test/StandardNamingCollisionTests.cs b/Frontmatter.Test/StandardNamingCollisionTests.cs
new file mode 100644
index 0000000..fddf5a7
--- /dev/null
+++ b/Frontmatter.Test/StandardNamingCollisionTests.cs
@@ -0,0 +1,80 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Frontmatter.Test;
+
+using System.Collections.Generic;
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+///
+/// Regression tests for #136: when two keys standardize to the same name, standard naming must keep
+/// both values instead of letting one overwrite the other.
+///
+[TestClass]
+public class StandardNamingCollisionTests
+{
+ [TestMethod]
+ public void DefaultOptions_AuthorAndCreatorListOfDifferentType_KeepsBothValues()
+ {
+ const string input = "---\nauthor: Alice\ncreator:\n - Bob\n---\nbody\n";
+
+ Dictionary? frontmatter = Frontmatter.ExtractFrontmatter(Frontmatter.CombineFrontmatter(input));
+
+ Assert.IsNotNull(frontmatter);
+ Assert.AreEqual("Alice", frontmatter["author"]);
+ Assert.IsTrue(frontmatter.TryGetValue("creator", out object? creator), "creator's list must survive alongside author");
+ CollectionAssert.AreEqual(new object[] { "Bob" }, (System.Collections.ICollection)creator);
+ }
+
+ [TestMethod]
+ public void NoMerge_AuthorAndCreator_KeepsBothValues()
+ {
+ const string input = "---\nauthor: Alice\ncreator: Bob\n---\nbody\n";
+
+ string result = Frontmatter.CombineFrontmatter(input, FrontmatterNaming.Standard, FrontmatterOrder.AsIs, FrontmatterMergeStrategy.None);
+ Dictionary? frontmatter = Frontmatter.ExtractFrontmatter(result);
+
+ Assert.IsNotNull(frontmatter);
+ Assert.AreEqual("Alice", frontmatter["author"]);
+ Assert.AreEqual("Bob", frontmatter["creator"]);
+ }
+
+ [TestMethod]
+ public void NoMerge_TitleCasedAndLowerCased_KeepsBothValues()
+ {
+ const string input = "---\nTitle: A\ntitle: B\n---\nbody\n";
+
+ string result = Frontmatter.CombineFrontmatter(input, FrontmatterNaming.Standard, FrontmatterOrder.AsIs, FrontmatterMergeStrategy.None);
+ Dictionary? frontmatter = Frontmatter.ExtractFrontmatter(result);
+
+ Assert.IsNotNull(frontmatter);
+ Assert.AreEqual("A", frontmatter["Title"]);
+ Assert.AreEqual("B", frontmatter["title"]);
+ }
+
+ [TestMethod]
+ public void StandardizePropertyNames_AliasBeforeStandardName_KeepsAliasUnchanged()
+ {
+ Dictionary result = NameStandardizer.StandardizePropertyNames(new Dictionary
+ {
+ ["creator"] = "Bob",
+ ["author"] = "Alice",
+ });
+
+ Assert.HasCount(2, result);
+ Assert.AreEqual("Bob", result["creator"]);
+ Assert.AreEqual("Alice", result["author"]);
+ }
+
+ [TestMethod]
+ public void StandardizePropertyNames_LoneAlias_IsStillRenamed()
+ {
+ Dictionary result = NameStandardizer.StandardizePropertyNames(new Dictionary
+ {
+ ["creator"] = "Bob",
+ });
+
+ Assert.HasCount(1, result);
+ Assert.AreEqual("Bob", result["author"]);
+ }
+}
diff --git a/Frontmatter/NameStandardizer.cs b/Frontmatter/NameStandardizer.cs
index b9c69bc..903933c 100644
--- a/Frontmatter/NameStandardizer.cs
+++ b/Frontmatter/NameStandardizer.cs
@@ -27,59 +27,48 @@ internal static Dictionary StandardizePropertyNames(Dictionary standardizedFrontmatter = [];
+ KeyValuePair[] properties = [.. frontmatter];
- foreach (KeyValuePair property in frontmatter)
+ for (int i = 0; i < properties.Length; i++)
{
- // Skip if it's already a standard property name
- if (Array.Exists(standardProperties, p => string.Equals(p, property.Key, StringComparison.OrdinalIgnoreCase)))
- {
- standardizedFrontmatter[property.Key.ToLowerInvariant()] = property.Value;
- continue;
- }
+ KeyValuePair property = properties[i];
+ string standardName = GetStandardName(property.Key, standardProperties);
- // Check if we've already processed this property name before
- if (PropertyNameCache.TryGetValue(property.Key, out string? mappedName))
- {
- // If the mapped name is the same as the original, it means we previously determined
- // there's no good match, so keep the original
- if (mappedName == property.Key)
- {
- standardizedFrontmatter[property.Key] = property.Value;
- }
- else
- {
- // Use the previously matched property name
- standardizedFrontmatter[mappedName] = property.Value;
- }
+ // Two keys can resolve to the same standard name (author + creator, Title + title). Only
+ // rename a key when its standard name is free: not already written, and not the exact name
+ // of a key still to come. Otherwise keep the original key, so neither value is lost.
+ bool isFree = standardName == property.Key ||
+ (!standardizedFrontmatter.ContainsKey(standardName) &&
+ !properties.Skip(i + 1).Any(later => later.Key == standardName));
- continue;
- }
+ standardizedFrontmatter[isFree ? standardName : property.Key] = property.Value;
+ }
- // Try to find a match in known property mappings
- string? knownMapping = FindKnownPropertyMapping(property.Key);
- if (knownMapping != null)
- {
- PropertyNameCache.TryAdd(property.Key, knownMapping);
- standardizedFrontmatter[knownMapping] = property.Value;
- continue;
- }
+ return standardizedFrontmatter;
+ }
- // Try to find a match by removing common prefixes and suffixes
- string normalizedKey = NormalizePropertyName(property.Key);
- string? standardMatch = FindStandardPropertyMatch(normalizedKey, standardProperties);
- if (standardMatch != null)
- {
- PropertyNameCache.TryAdd(property.Key, standardMatch);
- standardizedFrontmatter[standardMatch] = property.Value;
- continue;
- }
+ private static string GetStandardName(string key, string[] standardProperties)
+ {
+ // Already a standard property name, possibly in a different case
+ if (Array.Exists(standardProperties, p => string.Equals(p, key, StringComparison.OrdinalIgnoreCase)))
+ {
+ return key.ToLowerInvariant();
+ }
- // If no match found, preserve the original property name
- PropertyNameCache.TryAdd(property.Key, property.Key);
- standardizedFrontmatter[property.Key] = property.Value;
+ // Check if we've already processed this property name before
+ if (PropertyNameCache.TryGetValue(key, out string? mappedName))
+ {
+ return mappedName;
}
- return standardizedFrontmatter;
+ // Try to find a match in known property mappings, then by removing common prefixes and
+ // suffixes. If no match is found, preserve the original property name.
+ string standardName = FindKnownPropertyMapping(key)
+ ?? FindStandardPropertyMatch(NormalizePropertyName(key), standardProperties)
+ ?? key;
+
+ PropertyNameCache.TryAdd(key, standardName);
+ return standardName;
}
private static string? FindKnownPropertyMapping(string key)