diff --git a/Frontmatter.Test/UnclosedOpeningRuleTests.cs b/Frontmatter.Test/UnclosedOpeningRuleTests.cs new file mode 100644 index 0000000..3d41546 --- /dev/null +++ b/Frontmatter.Test/UnclosedOpeningRuleTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Frontmatter.Test; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests that a document opening with a --- rule that never closes is treated as having no frontmatter. +/// +[TestClass] +public class UnclosedOpeningRuleTests +{ + private static readonly string Nl = Environment.NewLine; + + private static readonly string Document = $"---{Nl}{Nl}# Heading{Nl}{Nl}Text{Nl}"; + + [TestMethod] + public void HasFrontmatter_OpeningRuleNeverCloses_IsFalse() + { + Assert.IsFalse(Frontmatter.HasFrontmatter($"---{Nl}{Nl}# Heading{Nl}")); + Assert.IsFalse(Frontmatter.HasFrontmatter(Document)); + } + + [TestMethod] + public void ExtractFrontmatter_OpeningRuleNeverCloses_IsNull() + { + Assert.IsNull(Frontmatter.ExtractFrontmatter(Document)); + } + + [TestMethod] + public void AddFrontmatter_OpeningRuleNeverCloses_WritesAHeaderInFrontOfTheRule() + { + string result = Frontmatter.AddFrontmatter(Document, new Dictionary { ["title"] = "T" }); + + Assert.AreEqual($"---{Nl}title: T{Nl}---{Nl}---{Nl}{Nl}# Heading{Nl}{Nl}Text{Nl}", result); + + Dictionary? frontmatter = Frontmatter.ExtractFrontmatter(result); + Assert.IsNotNull(frontmatter); + Assert.HasCount(1, frontmatter); + Assert.AreEqual("T", frontmatter["title"]); + Assert.AreEqual($"---{Nl}{Nl}# Heading{Nl}{Nl}Text", Frontmatter.ExtractBody(result)); + } + + [TestMethod] + public void RemoveFrontmatter_OpeningRuleNeverCloses_LeavesTheDocumentAlone() + { + Assert.AreEqual(Document, Frontmatter.RemoveFrontmatter(Document)); + } +} diff --git a/Frontmatter/Frontmatter.cs b/Frontmatter/Frontmatter.cs index 76b1f83..f0b715f 100644 --- a/Frontmatter/Frontmatter.cs +++ b/Frontmatter/Frontmatter.cs @@ -144,12 +144,28 @@ public static string CombineFrontmatter(string input, FrontmatterNaming property /// Checks if a markdown document contains frontmatter. /// /// The markdown document content as a string. + /// + /// Only a closed block counts. A document that opens with a --- line that never closes, such as + /// a markdown thematic break, has no frontmatter, so writes a header in front + /// of it instead of leaving it alone. + /// /// True if the document contains frontmatter, false otherwise. /// Thrown when input is null. public static bool HasFrontmatter(string input) { Ensure.NotNull(input); + return TrySplitFrontmatterBlocks(input, out _, out _); + } + + /// + /// Checks whether the first line of a document is a frontmatter delimiter, whether or not a closing + /// delimiter follows it. + /// + /// The document to inspect. + /// True if the document's first line is a delimiter, false otherwise. + private static bool OpensWithDelimiter(string input) + { // The opening delimiter follows the same rule as the closing one, so trailing whitespace an editor // left behind does not hide the header. A leading byte order mark is skipped, since callers that // decode bytes or streams themselves keep it where File.ReadAllText would strip it. @@ -311,8 +327,9 @@ private static List> ExtractFrontmatterObjects(string /// The markdown document content as a string. /// Output parameter that will contain the markdown body without frontmatter. /// - /// True when the document opens with a frontmatter delimiter but a non-blank block failed to parse, or no - /// closed block could be found. Such text is missing from the returned objects but not from the document. + /// True when a non-blank frontmatter block failed to parse. Such text is missing from the returned objects + /// but not from the document. An opening delimiter that never closes is not frontmatter, so it is not + /// reported here. /// /// A collection of dictionaries representing each frontmatter section that parsed. private static List> ExtractFrontmatterObjects(string input, out string body, out bool hasUnreadableBlock) @@ -322,7 +339,6 @@ private static List> ExtractFrontmatterObjects(string if (!TrySplitFrontmatterBlocks(input, out List blocks, out body)) { - hasUnreadableBlock = HasFrontmatter(input); return frontmatterObjects; } @@ -385,7 +401,7 @@ private static bool TrySplitFrontmatterBlocks(string input, out List blo blocks = []; body = input; - if (!HasFrontmatter(input)) + if (!OpensWithDelimiter(input)) { return false; }