From 03d3a84d1a1f8311a54fa20e0f935d8b6b0fd1be Mon Sep 17 00:00:00 2001 From: Matthew Edmondson Date: Thu, 8 Oct 2026 22:35:03 +0000 Subject: [PATCH] Return every stacked block's properties from ExtractFrontmatter [patch] ExtractBody, RemoveFrontmatter and AddFrontmatter treat consecutive frontmatter blocks as one header, but ExtractFrontmatter returned only the first block, so a later block's properties were in neither the extracted frontmatter nor the body. Fold every block the way AddFrontmatter does, so the first block to define a key wins. Fixes #169 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019MupvoiQ9WM1vUJoCzYJ6W --- .../StackedBlockPreservationTests.cs | 25 +++++++++++++++++++ Frontmatter/Frontmatter.cs | 10 ++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Frontmatter.Test/StackedBlockPreservationTests.cs b/Frontmatter.Test/StackedBlockPreservationTests.cs index 480af95..c1410a4 100644 --- a/Frontmatter.Test/StackedBlockPreservationTests.cs +++ b/Frontmatter.Test/StackedBlockPreservationTests.cs @@ -41,6 +41,31 @@ public void AddFrontmatter_StackedBlocksRepeatAKey_FirstBlockWins() Assert.AreEqual("First", frontmatter["title"]); } + [TestMethod] + public void ExtractFrontmatter_TwoStackedBlocks_ReturnsPropertiesFromEveryBlock() + { + const string input = "---\ntitle: Hello\n---\n---\nauthor: Jane\n---\n# Body\n"; + + Dictionary? frontmatter = Frontmatter.ExtractFrontmatter(input); + + Assert.IsNotNull(frontmatter); + Assert.AreEqual("Hello", frontmatter["title"]); + Assert.AreEqual("Jane", frontmatter["author"], "The second block is header to ExtractBody, so ExtractFrontmatter must return it"); + Assert.AreEqual("# Body", Frontmatter.ExtractBody(input)); + } + + [TestMethod] + public void ExtractFrontmatter_StackedBlocksRepeatAKey_FirstBlockWins() + { + const string input = "---\ntitle: First\n---\n---\ntitle: Second\nauthor: Jane\n---\nbody\n"; + + Dictionary? frontmatter = Frontmatter.ExtractFrontmatter(input); + + Assert.IsNotNull(frontmatter); + Assert.AreEqual("First", frontmatter["title"]); + Assert.AreEqual("Jane", frontmatter["author"]); + } + // The first block is always read as frontmatter, so an unparseable first block followed by one that // parses is the case where the unreadable text sits in the header rather than the body. private const string UnreadableFirstBlock = "---\nkey: [unclosed\n---\n---\ntitle: T\n---\nbody\n"; diff --git a/Frontmatter/Frontmatter.cs b/Frontmatter/Frontmatter.cs index 9907721..1cdba09 100644 --- a/Frontmatter/Frontmatter.cs +++ b/Frontmatter/Frontmatter.cs @@ -126,6 +126,10 @@ public static string CombineFrontmatter(string input, FrontmatterNaming property /// Extracts frontmatter from a markdown document. /// /// The markdown document content as a string. + /// + /// Consecutive frontmatter blocks form one header, so their properties are combined, and the first + /// block to define a key wins. + /// /// A dictionary containing the frontmatter properties, or null if no frontmatter is found. /// Thrown when input is null. public static Dictionary? ExtractFrontmatter(string input) @@ -137,8 +141,10 @@ public static string CombineFrontmatter(string input, FrontmatterNaming property return null; } - List> frontmatterObjects = ExtractFrontmatterObjects(input, out _); - return frontmatterObjects.Count > 0 ? frontmatterObjects.First() : null; + // Stacked blocks are one header, as AddFrontmatter and ExtractBody treat them, so fold them all + // and let an earlier block win a repeated key. + Dictionary frontmatter = CombineAllFrontmatterObjects(ExtractFrontmatterObjects(input, out _)); + return frontmatter.Count > 0 ? frontmatter : null; } ///