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
25 changes: 25 additions & 0 deletions Frontmatter.Test/StackedBlockPreservationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object>? 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<string, object>? 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";
Expand Down
10 changes: 8 additions & 2 deletions Frontmatter/Frontmatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@
/// Extracts frontmatter from a markdown document.
/// </summary>
/// <param name="input">The markdown document content as a string.</param>
/// <remarks>
/// Consecutive frontmatter blocks form one header, so their properties are combined, and the first
/// block to define a key wins.
/// </remarks>
/// <returns>A dictionary containing the frontmatter properties, or null if no frontmatter is found.</returns>
/// <exception cref="ArgumentNullException">Thrown when input is null.</exception>
public static Dictionary<string, object>? ExtractFrontmatter(string input)
Expand All @@ -137,8 +141,10 @@
return null;
}

List<Dictionary<string, object>> 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<string, object> frontmatter = CombineAllFrontmatterObjects(ExtractFrontmatterObjects(input, out _));
return frontmatter.Count > 0 ? frontmatter : null;
}

/// <summary>
Expand Down Expand Up @@ -301,7 +307,7 @@
}

// Then add any remaining properties that weren't in the standard order
foreach (KeyValuePair<string, object> property in frontmatter)

Check warning on line 310 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 310 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 310 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 310 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (!sortedFrontmatter.ContainsKey(property.Key))
{
Expand Down Expand Up @@ -351,7 +357,7 @@
continue;
}

if (YamlSerializer.TryParseYamlObject(section, out Dictionary<string, object>? frontmatterObject) && frontmatterObject != null)

Check warning on line 360 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Change this condition so that it does not always evaluate to 'True'.

Check warning on line 360 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Change this condition so that it does not always evaluate to 'True'.
{
frontmatterObjects.Add(frontmatterObject);
}
Expand Down Expand Up @@ -499,7 +505,7 @@
lines.Add((start, i));
if (c == '\r' && i + 1 < input.Length && input[i + 1] == '\n')
{
i++;

Check warning on line 508 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.

Check warning on line 508 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Do not update the stop condition variable 'i' in the body of the for loop.
}

start = i + 1;
Expand Down Expand Up @@ -631,7 +637,7 @@
}

// Then, add properties from dictionary b that don't exist in a
foreach (KeyValuePair<string, object> kvp in b)

Check warning on line 640 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method

Check warning on line 640 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Loops should be simplified using the "Where" LINQ method
{
if (!combinedFrontmatterObject.ContainsKey(kvp.Key))
{
Expand Down
Loading