Class
Sealed
public sealed class FrontMatterExtractor
Namespace: Moka.Docs.Parsing.FrontMatter
Extracts and parses YAML front matter from the top of Markdown files. Returns the parsed Content.FrontMatter and the remaining Markdown body.
Methods
| Name | Description |
|---|---|
Extract(string markdown) |
Extracts front matter from a Markdown string. |
Extract(string markdown)
FrontMatterResult FrontMatterExtractor.Extract(string markdown)
Extracts front matter from a Markdown string.
Parameters
| Name | Type | Description |
|---|---|---|
markdown | string | The raw Markdown content (may include front matter). |
Returns: The extracted front matter and the Markdown body after the front matter block.
View Source
/// <summary>
/// Extracts and parses YAML front matter from the top of Markdown files.
/// Returns the parsed <see cref = "Core.Content.FrontMatter"/> and the remaining Markdown body.
/// </summary>
public sealed class FrontMatterExtractor
{
private const string _delimiter = "---";
/// <summary>
/// Extracts front matter from a Markdown string.
/// </summary>
/// <param name = "markdown">The raw Markdown content (may include front matter).</param>
/// <returns>The extracted front matter and the Markdown body after the front matter block.</returns>
public FrontMatterResult Extract(string markdown)
{
if (string.IsNullOrWhiteSpace(markdown))
{
return new FrontMatterResult(DefaultFrontMatter("Untitled"), "");
}
ReadOnlySpan<char> span = markdown.AsSpan().TrimStart();
// Must start with ---
if (!span.StartsWith(_delimiter))
{
return new FrontMatterResult(DefaultFrontMatter("Untitled"), markdown);
}
// Find the closing ---
ReadOnlySpan<char> afterFirstDelimiter = span[3..];
int closingIndex = FindClosingDelimiter(afterFirstDelimiter);
if (closingIndex < 0)
// No closing delimiter — treat entire content as body
{
return new FrontMatterResult(DefaultFrontMatter("Untitled"), markdown);
}
string yamlContent = afterFirstDelimiter[..closingIndex].ToString().Trim();
int bodyStart = 3 + closingIndex + 3; // skip both --- delimiters
string body = bodyStart < span.Length ? span[bodyStart..].ToString().TrimStart('\r', '\n') : "";
if (string.IsNullOrWhiteSpace(yamlContent))
{
return new FrontMatterResult(DefaultFrontMatter("Untitled"), body);
}
try
{
IDeserializer deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).IgnoreUnmatchedProperties().Build();
FrontMatterDto dto = deserializer.Deserialize<FrontMatterDto>(yamlContent);
Core.Content.FrontMatter frontMatter = MapFromDto(dto);
return new FrontMatterResult(frontMatter, body);
}
catch
{
// Malformed YAML — treat as no front matter
return new FrontMatterResult(DefaultFrontMatter("Untitled"), markdown);
}
}
#region Private Helpers
private static int FindClosingDelimiter(ReadOnlySpan<char> content)
{
int index = 0;
while (index < content.Length)
{
// Skip to next newline
int newlineIndex = content[index..].IndexOf('\n');
if (newlineIndex < 0)
{
break;
}
index += newlineIndex + 1;
// Check if next line starts with ---
ReadOnlySpan<char> remaining = content[index..];
ReadOnlySpan<char> trimmed = remaining.TrimStart([' ', '\t']);
if (trimmed.StartsWith(_delimiter))
{
// Verify it's just --- (possibly with trailing whitespace)
int lineEnd = trimmed.IndexOfAny('\r', '\n');
ReadOnlySpan<char> line = lineEnd >= 0 ? trimmed[..lineEnd] : trimmed;
if (line.TrimEnd().Length == 3)
{
return index;
}
}
}
return -1;
}
private static Core.Content.FrontMatter DefaultFrontMatter(string title) => new()
{
Title = title
};
private static Core.Content.FrontMatter MapFromDto(FrontMatterDto? dto)
{
if (dto is null)
{
return DefaultFrontMatter("Untitled");
}
return new Core.Content.FrontMatter
{
Title = string.IsNullOrWhiteSpace(dto.Title) ? "Untitled" : dto.Title,
Description = dto.Description ?? "",
Order = dto.Order,
Icon = dto.Icon,
Layout = dto.Layout ?? "default",
Tags = dto.Tags ?? [],
Visibility = ParseVisibility(dto.Visibility),
Toc = dto.Toc ?? true,
Expanded = dto.Expanded ?? true,
Route = dto.Route,
Version = dto.Version,
Requires = dto.Requires
};
}
private static PageVisibility ParseVisibility(string? value)
{
return value?.ToLowerInvariant() switch
{
"hidden" => PageVisibility.Hidden,
"draft" => PageVisibility.Draft,
_ => PageVisibility.Public
};
}
#endregion
}