, ::: steps, ::: link-cards, and ::: code-group blocks." /> , ::: steps, ::: link-cards, and ::: code-group blocks." /> , ::: steps, ::: link-cards, and ::: code-group blocks." />
Class Sealed
public sealed class ComponentParser : BlockParser

Namespace: Moka.Docs.Parsing.Markdown

Parses ::: card{title="..." icon="..." variant="..."}, ::: steps, ::: link-cards, and ::: code-group blocks.

Inheritance

Inherits from: BlockParser

Constructors

NameDescription
ComponentParser()

Methods

Type Relationships
classDiagram
                    style ComponentParser fill:#f9f,stroke:#333,stroke-width:2px
                    ComponentParser --|> BlockParser : inherits
                
View Source
#endregion
#region Parser
/// <summary>
///     Parses <c>::: card{title="..." icon="..." variant="..."}</c>, <c>::: steps</c>,
///     <c>::: link-cards</c>, and <c>::: code-group</c> blocks.
/// </summary>
public sealed class ComponentParser : BlockParser
{
    private static readonly HashSet<string> _validTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        "card",
        "steps",
        "link-cards",
        "code-group"
    };
    public ComponentParser()
    {
        OpeningCharacters = [':'];
    }

    /// <inheritdoc/>
    public override BlockState TryOpen(BlockProcessor processor)
    {
        if (processor.IsCodeIndent)
        {
            return BlockState.None;
        }

        StringSlice line = processor.Line;
        int start = line.Start;
        if (line.CurrentChar != ':')
        {
            return BlockState.None;
        }

        int colons = MarkdigHelpers.CountAndSkipChar(ref line, ':');
        if (colons < 3)
        {
            return BlockState.None;
        }

        line.TrimStart();
        string remaining = line.ToString().Trim();
        if (string.IsNullOrEmpty(remaining))
        {
            return BlockState.None;
        }

        // Parse type and optional attributes: card{title="..." icon="..."}
        int braceIndex = remaining.IndexOf('{');
        string typeName;
        string? attrString = null;
        if (braceIndex > 0)
        {
            typeName = remaining[..braceIndex].Trim();
            int closeBrace = remaining.LastIndexOf('}');
            if (closeBrace > braceIndex)
            {
                attrString = remaining[(braceIndex + 1)..closeBrace];
            }
        }
        else
        {
            // Could have a space-separated rest (but for components we only use the type)
            int spaceIndex = remaining.IndexOf(' ');
            typeName = spaceIndex > 0 ? remaining[..spaceIndex] : remaining;
        }

        if (!_validTypes.Contains(typeName))
        {
            return BlockState.None;
        }

        Dictionary<string, string> attrs = ParseAttributes(attrString);
        ContainerBlock block = typeName.ToLowerInvariant() switch
        {
            "card" => new CardBlock(this)
            {
                Title = attrs.GetValueOrDefault("title"),
                Icon = attrs.GetValueOrDefault("icon"),
                Variant = attrs.GetValueOrDefault("variant", "default"),
                Span = new SourceSpan(start, line.End),
                Column = processor.Column
            },
            "steps" => new StepsBlock(this)
            {
                Span = new SourceSpan(start, line.End),
                Column = processor.Column
            },
            "link-cards" => new LinkCardsBlock(this)
            {
                Span = new SourceSpan(start, line.End),
                Column = processor.Column
            },
            "code-group" => new CodeGroupBlock(this)
            {
                Span = new SourceSpan(start, line.End),
                Column = processor.Column
            },
            _ => throw new InvalidOperationException($"Unexpected component type: {typeName}")};
        processor.NewBlocks.Push(block);
        return BlockState.ContinueDiscard;
    }

    /// <inheritdoc/>
    public override BlockState TryContinue(BlockProcessor processor, Block block)
    {
        if (block is not ContainerBlock)
        {
            return BlockState.Continue;
        }

        StringSlice line = processor.Line;
        // Check for closing :::
        if (line.CurrentChar == ':')
        {
            StringSlice saved = line;
            int colons = MarkdigHelpers.CountAndSkipChar(ref line, ':');
            if (colons >= 3)
            {
                string after = line.ToString().Trim();
                if (string.IsNullOrEmpty(after))
                {
                    block.UpdateSpanEnd(line.End);
                    return BlockState.BreakDiscard;
                }
            }

            processor.Line = saved;
        }

        return BlockState.Continue;
    }

    /// <summary>
    ///     Parses key="value" pairs from an attribute string.
    /// </summary>
    private static Dictionary<string, string> ParseAttributes(string? attrString)
    {
        var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        if (string.IsNullOrWhiteSpace(attrString))
        {
            return result;
        }

        ReadOnlySpan<char> span = attrString.AsSpan();
        while (span.Length > 0)
        {
            span = span.TrimStart();
            if (span.Length == 0)
            {
                break;
            }

            // Find key
            int eqIndex = span.IndexOf('=');
            if (eqIndex <= 0)
            {
                break;
            }

            string key = span[..eqIndex].Trim().ToString();
            span = span[(eqIndex + 1)..].TrimStart();
            // Find value (quoted)
            if (span.Length > 0 && (span[0] == '"' || span[0] == '\''))
            {
                char quote = span[0];
                span = span[1..];
                int endQuote = span.IndexOf(quote);
                if (endQuote < 0)
                {
                    break;
                }

                string value = span[..endQuote].ToString();
                result[key] = value;
                span = span[(endQuote + 1)..];
            }
            else
            {
                // Unquoted value — read until space
                int spaceIdx = span.IndexOf(' ');
                if (spaceIdx < 0)
                {
                    result[key] = span.ToString();
                    break;
                }

                result[key] = span[..spaceIdx].ToString();
                span = span[spaceIdx..];
            }
        }

        return result;
    }
}
Was this page helpful?