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
71 changes: 71 additions & 0 deletions Frontmatter.Test/DelimiterLineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,75 @@ public void HasFrontmatter_DelimiterWithoutLineEnding_IsNotFrontmatter()
Assert.IsFalse(Frontmatter.HasFrontmatter("\uFEFF"));
Assert.IsFalse(Frontmatter.HasFrontmatter(string.Empty));
}

private static readonly string PandocDocument =
$"---{Nl}title: A{Nl}...{Nl}# Heading{Nl}{Nl}Important paragraph.{Nl}{Nl}---{Nl}{Nl}More text{Nl}";

[TestMethod]
public void ExtractFrontmatter_BlockClosedByDocumentEndMarker_ReadsTheBlock()
{
Dictionary<string, object>? frontmatter = Frontmatter.ExtractFrontmatter(PandocDocument);

Assert.IsNotNull(frontmatter);
Assert.HasCount(1, frontmatter);
Assert.AreEqual("A", frontmatter["title"]);
}

[TestMethod]
public void ExtractBody_BlockClosedByDocumentEndMarker_KeepsTheBodyBeforeALaterRule()
{
string body = Frontmatter.ExtractBody(PandocDocument);

Assert.AreEqual($"# Heading{Nl}{Nl}Important paragraph.{Nl}{Nl}---{Nl}{Nl}More text", body);
}

[TestMethod]
public void RemoveFrontmatter_BlockClosedByDocumentEndMarker_KeepsTheBodyBeforeALaterRule()
{
string result = Frontmatter.RemoveFrontmatter(PandocDocument);

Assert.AreEqual($"# Heading{Nl}{Nl}Important paragraph.{Nl}{Nl}---{Nl}{Nl}More text{Nl}", result);
}

[TestMethod]
public void ReplaceFrontmatter_BlockClosedByDocumentEndMarker_ReplacesOnlyTheBlock()
{
string result = Frontmatter.ReplaceFrontmatter(PandocDocument, new() { { "title", "B" } });

Assert.AreEqual($"---{Nl}title: B{Nl}---{Nl}# Heading{Nl}{Nl}Important paragraph.{Nl}{Nl}---{Nl}{Nl}More text{Nl}", result);
}

[TestMethod]
public void ExtractFrontmatter_BlockClosedByDocumentEndMarkerWithNoLaterRule_ReadsTheBlock()
{
string input = $"---{Nl}title: A{Nl}... {Nl}Body{Nl}";

Dictionary<string, object>? frontmatter = Frontmatter.ExtractFrontmatter(input);

Assert.IsNotNull(frontmatter);
Assert.AreEqual("A", frontmatter["title"]);
Assert.AreEqual("Body", Frontmatter.ExtractBody(input));
}

[TestMethod]
public void ExtractFrontmatter_DocumentEndMarkerOnTheFirstLine_IsNotAnOpener()
{
string input = $"...{Nl}title: A{Nl}---{Nl}Body{Nl}";

Assert.IsFalse(Frontmatter.HasFrontmatter(input));
Assert.IsNull(Frontmatter.ExtractFrontmatter(input));
Assert.AreEqual(input, Frontmatter.RemoveFrontmatter(input));
}

[TestMethod]
public void ExtractFrontmatter_IndentedDocumentEndMarkerInsideAValue_DoesNotCloseTheBlock()
{
string input = $"---{Nl}notes: |{Nl} ...{Nl}title: A{Nl}---{Nl}Body{Nl}";

Dictionary<string, object>? frontmatter = Frontmatter.ExtractFrontmatter(input);

Assert.IsNotNull(frontmatter);
Assert.AreEqual("A", frontmatter["title"]);
Assert.AreEqual("Body", Frontmatter.ExtractBody(input));
}
}
17 changes: 15 additions & 2 deletions Frontmatter/Frontmatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
/// The delimiter that marks the beginning and end of a frontmatter section.
/// </summary>
private const string FrontmatterDelimiter = "---";
private const string DocumentEndMarker = "...";

/// <summary>
/// The byte order mark a document may begin with when it was decoded without stripping it.
Expand Down Expand Up @@ -136,7 +137,7 @@
}

List<Dictionary<string, object>> frontmatterObjects = ExtractFrontmatterObjects(input, out _);
return frontmatterObjects.Count > 0 ? frontmatterObjects.First() : null;

Check warning on line 140 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Indexing at 0 should be used instead of the "Enumerable" extension method "First"

Check warning on line 140 in Frontmatter/Frontmatter.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Indexing at 0 should be used instead of the "Enumerable" extension method "First"
}

/// <summary>
Expand Down Expand Up @@ -283,7 +284,7 @@
}

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

Check warning on line 287 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 287 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 @@ -333,7 +334,7 @@
continue;
}

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

Check warning on line 337 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 337 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 @@ -368,7 +369,9 @@
/// <remarks>
/// Delimiters are recognised only as whole lines, so a <c>---</c> inside a value or a markdown
/// horizontal rule in the body is never mistaken for one. The first line opens a block, which closes
/// at the next delimiter line, including one at the very end of the document. Further blocks are
/// at the next delimiter line, including one at the very end of the document. YAML's document end
/// marker <c>...</c>, which Pandoc metadata blocks use, also closes a block but never opens one; a
/// rewritten document always closes its frontmatter with <c>---</c>. Further blocks are
/// consumed only while each opens on the line straight after the previous one closed and holds
/// frontmatter rather than body text (see <see cref="IsFollowOnBlock"/>); the body is everything after
/// the last block consumed.
Expand All @@ -390,12 +393,13 @@
List<(int Start, int End)> lines = SplitLines(input);
lines[0] = (OpeningLineStart(input), lines[0].End);
bool IsDelimiterAt(int index) => IsDelimiterLine(input[lines[index].Start..lines[index].End]);
bool IsCloserAt(int index) => IsClosingDelimiterLine(input[lines[index].Start..lines[index].End]);

int next = 0;
while (next < lines.Count && IsDelimiterAt(next))
{
int close = next + 1;
while (close < lines.Count && !IsDelimiterAt(close))
while (close < lines.Count && !IsCloserAt(close))
{
close++;
}
Expand Down Expand Up @@ -478,7 +482,7 @@
lines.Add((start, i));
if (c == '\r' && i + 1 < input.Length && input[i + 1] == '\n')
{
i++;

Check warning on line 485 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 485 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 @@ -521,6 +525,15 @@
/// <returns>True if the line is a frontmatter delimiter, false otherwise.</returns>
private static bool IsDelimiterLine(string line) => line.TrimEnd() == FrontmatterDelimiter;

/// <summary>
/// Checks whether a line can close a frontmatter block: a delimiter, or YAML's document end marker
/// <c>...</c>, either allowing trailing whitespace.
/// </summary>
/// <param name="line">The line to check, without its line ending.</param>
/// <returns>True if the line closes a frontmatter block, false otherwise.</returns>
private static bool IsClosingDelimiterLine(string line) =>
IsDelimiterLine(line) || line.TrimEnd() == DocumentEndMarker;

/// <summary>
/// Combines two frontmatter dictionaries into a single dictionary.
/// </summary>
Expand All @@ -539,7 +552,7 @@
}

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

Check warning on line 555 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 555 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