Class
Sealed
public sealed class TabGroupParser : BlockParser
Namespace: Moka.Docs.Parsing.Markdown
Parses
=== "Title" tab blocks.Inheritance
Inherits from: BlockParser
Constructors
| Name | Description |
|---|---|
TabGroupParser() |
Creates a new tab group parser. |
TabGroupParser()
TabGroupParser.TabGroupParser()
Creates a new tab group parser.
Methods
| Name | Description |
|---|---|
TryContinue(BlockProcessor processor, Block block) override |
|
TryOpen(BlockProcessor processor) override |
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];
}
}