Class
Sealed
public sealed class CodeGroupRenderer : HtmlObjectRenderer<Moka.Docs.Parsing.Markdown.CodeGroupBlock>
Namespace: Moka.Docs.Parsing.Markdown
Renders a code-group block as a tabbed interface.
Inheritance
Inherits from: HtmlObjectRenderer<Moka.Docs.Parsing.Markdown.CodeGroupBlock>
Methods
| Name | Description |
|---|---|
Write(HtmlRenderer renderer, CodeGroupBlock block) override |
Type Relationships
classDiagram
style CodeGroupRenderer fill:#f9f,stroke:#333,stroke-width:2px
CodeGroupRenderer --|> CodeGroupBlock~ : inherits
View Source
/// <summary>
/// Renders a code-group block as a tabbed interface.
/// </summary>
public sealed class CodeGroupRenderer : HtmlObjectRenderer<CodeGroupBlock>
{
/// <inheritdoc/>
protected override void Write(HtmlRenderer renderer, CodeGroupBlock block)
{
string groupId = block.GroupId;
// Collect code blocks and their titles
var codeBlocks = new List<(string Title, FencedCodeBlock Code)>();
foreach (Block child in block)
{
if (child is FencedCodeBlock fenced)
{
// Try to get title from info string: ```csharp title="C#"
string info = fenced.Info ?? "";
string title = ExtractTitle(fenced.Arguments) ?? LanguageDisplayName(info);
codeBlocks.Add((title, fenced));
}
}
if (codeBlocks.Count == 0)
{
// Fallback: just render children normally
renderer.WriteChildren(block);
return;
}
renderer.EnsureLine();
renderer.Write($"<div class=\"tabs component-code-group\" data-tab-group=\"{groupId}\">");
renderer.WriteLine();
// Tab headers
renderer.Write("<div class=\"tab-headers\" role=\"tablist\">");
renderer.WriteLine();
for (int i = 0; i < codeBlocks.Count; i++)
{
string active = i == 0 ? " active" : "";
string selected = i == 0 ? "true" : "false";
renderer.Write($"<button class=\"tab-header{active}\" role=\"tab\" aria-selected=\"{selected}\" data-tab-index=\"{i}\">{codeBlocks[i].Title}</button>");
renderer.WriteLine();
}
renderer.Write("</div>");
renderer.WriteLine();
// Tab content panels — render each code block in its own panel
for (int i = 0; i < codeBlocks.Count; i++)
{
string activeClass = i == 0 ? " active" : "";
string hidden = i == 0 ? "" : " hidden";
renderer.Write($"<div class=\"tab-content{activeClass}\" role=\"tabpanel\"{hidden}>");
renderer.WriteLine();
// Render the fenced code block using the default code renderer
renderer.Write(codeBlocks[i].Code);
renderer.Write("</div>");
renderer.WriteLine();
}
renderer.Write("</div>");
renderer.WriteLine();
}
/// <summary>
/// Extracts title="..." from the arguments string of a fenced code block.
/// </summary>
private static string? ExtractTitle(string? arguments)
{
if (string.IsNullOrWhiteSpace(arguments))
{
return null;
}
const string prefix = "title=";
int idx = arguments.IndexOf(prefix, StringComparison.OrdinalIgnoreCase);
if (idx < 0)
{
return null;
}
string rest = arguments[(idx + prefix.Length)..];
if (rest.Length < 2)
{
return null;
}
char quote = rest[0];
if (quote != '"' && quote != '\'')
{
return null;
}
int endQuote = rest.IndexOf(quote, 1);
if (endQuote < 0)
{
return null;
}
return rest[1..endQuote];
}
/// <summary>
/// Returns a display-friendly language name.
/// </summary>
private static string LanguageDisplayName(string? lang)
{
if (string.IsNullOrWhiteSpace(lang))
{
return "Code";
}
return lang.ToLowerInvariant() switch
{
"csharp" or "cs" => "C#",
"fsharp" or "fs" => "F#",
"javascript" or "js" => "JavaScript",
"typescript" or "ts" => "TypeScript",
"python" or "py" => "Python",
"bash" or "sh" or "shell" => "Shell",
"xml" => "XML",
"json" => "JSON",
"yaml" or "yml" => "YAML",
"html" => "HTML",
"css" => "CSS",
"sql" => "SQL",
"powershell" or "ps1" => "PowerShell",
"dockerfile" => "Dockerfile",
"markdown" or "md" => "Markdown",
_ => lang
};
}
}