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
| Name | Description |
|---|---|
ComponentParser() |
Methods
| Name | Description |
|---|---|
TryContinue(BlockProcessor processor, Block block) override |
|
TryOpen(BlockProcessor processor) override |
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;
}
}