tab blocks." /> tab blocks." /> tab blocks." />
Class Sealed
public sealed class TabGroupParser : BlockParser

Namespace: Moka.Docs.Parsing.Markdown

Parses === "Title" tab blocks.

Inheritance

Inherits from: BlockParser

Constructors

NameDescription
TabGroupParser() Creates a new tab group parser.

TabGroupParser()

TabGroupParser.TabGroupParser()

Creates a new tab group parser.

Methods

Type Relationships
classDiagram
                    style TabGroupParser fill:#f9f,stroke:#333,stroke-width:2px
                    TabGroupParser --|> BlockParser : inherits
                
View Source
/// <summary>
///     Parses <c>=== "Title"</c> tab blocks.
/// </summary>
public sealed class TabGroupParser : BlockParser
{
    private static int _groupCounter;
    /// <summary>Creates a new tab group parser.</summary>
    public TabGroupParser()
    {
        OpeningCharacters = ['='];
    }

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

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

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

        line.TrimStart();
        string remaining = line.ToString().Trim();
        // Must have a quoted title: === "Tab Title"
        string? title = ExtractQuotedTitle(remaining);
        if (title is null)
        {
            return BlockState.None;
        }

        string groupId = $"tabs-{Interlocked.Increment(ref _groupCounter)}";
        var group = new TabGroupBlock(this)
        {
            GroupId = groupId,
            Span = new SourceSpan(start, line.End),
            Column = processor.Column
        };
        var firstTab = new TabItemBlock(this)
        {
            Title = title,
            IsFirst = true,
            Span = new SourceSpan(start, line.End),
            Column = processor.Column
        };
        group.Add(firstTab);
        processor.NewBlocks.Push(group);
        processor.NewBlocks.Push(firstTab);
        return BlockState.ContinueDiscard;
    }

    /// <inheritdoc/>
    public override BlockState TryContinue(BlockProcessor processor, Block block)
    {
        // We handle continuation for both TabGroupBlock and TabItemBlock
        if (block is TabItemBlock)
        {
            return TryContinueTabItem(processor, block);
        }

        if (block is TabGroupBlock)
        {
            return TryContinueTabGroup(processor, block);
        }

        return BlockState.Continue;
    }

    private BlockState TryContinueTabItem(BlockProcessor processor, Block block)
    {
        StringSlice line = processor.Line;
        // Check for closing === (no title — end of tab group)
        if (line.CurrentChar == '=')
        {
            StringSlice saved = line;
            int equals = MarkdigHelpers.CountAndSkipChar(ref line, '=');
            if (equals >= 3)
            {
                string remaining = line.ToString().Trim();
                // Closing === (no title)
                if (string.IsNullOrEmpty(remaining))
                {
                    block.UpdateSpanEnd(line.End);
                    return BlockState.BreakDiscard;
                }

                // New tab === "Title"
                string? title = ExtractQuotedTitle(remaining);
                if (title is not null)
                {
                    // Close current tab, open new one
                    var tabGroup = block.Parent as TabGroupBlock;
                    var newTab = new TabItemBlock(this)
                    {
                        Title = title,
                        IsFirst = false,
                        Span = new SourceSpan(line.Start, line.End),
                        Column = processor.Column
                    };
                    tabGroup?.Add(newTab);
                    processor.Close(block);
                    processor.NewBlocks.Push(newTab);
                    return BlockState.ContinueDiscard;
                }
            }

            processor.Line = saved;
        }

        return BlockState.Continue;
    }

    private static BlockState TryContinueTabGroup(BlockProcessor processor, Block block)
    {
        // The tab group continues as long as its child tabs continue
        return BlockState.Continue;
    }

    private static string? ExtractQuotedTitle(string text)
    {
        text = text.Trim();
        if (text.Length < 2)
        {
            return null;
        }

        char quote = text[0];
        if (quote != '"' && quote != '\'')
        {
            return null;
        }

        int endQuote = text.IndexOf(quote, 1);
        if (endQuote < 0)
        {
            return null;
        }

        return text[1..endQuote];
    }
}
Was this page helpful?