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;
}