Content.FrontMatter and the remaining Markdown body." /> Content.FrontMatter and the remaining Markdown body." /> Content.FrontMatter and the remaining Markdown body." />
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

NameDescription
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

NameTypeDescription
markdownstringThe 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
}
Was this page helpful?