src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs (324 lines of code) (raw):
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information
using System.Diagnostics.CodeAnalysis;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.Myst.InlineParsers;
using Elastic.Markdown.Myst.InlineParsers.Substitution;
using Elastic.Markdown.Myst.Settings;
using Elastic.Markdown.Slices.Directives;
using Markdig;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using RazorSlices;
using YamlDotNet.Core;
namespace Elastic.Markdown.Myst.Directives;
/// <summary>
/// An HTML renderer for a <see cref="DirectiveBlock"/>.
/// </summary>
/// <seealso cref="HtmlObjectRenderer{CustomContainer}" />
public class DirectiveHtmlRenderer(MarkdownParser markdownParser) : HtmlObjectRenderer<DirectiveBlock>
{
protected override void Write(HtmlRenderer renderer, DirectiveBlock directiveBlock)
{
_ = renderer.EnsureLine();
switch (directiveBlock)
{
case MermaidBlock mermaidBlock:
WriteMermaid(renderer, mermaidBlock);
return;
case FigureBlock imageBlock:
WriteFigure(renderer, imageBlock);
return;
case ImageBlock imageBlock:
WriteImage(renderer, imageBlock);
return;
case DropdownBlock dropdownBlock:
WriteDropdown(renderer, dropdownBlock);
return;
case AdmonitionBlock admonitionBlock:
WriteAdmonition(renderer, admonitionBlock);
return;
case VersionBlock versionBlock:
WriteVersion(renderer, versionBlock);
return;
case TabSetBlock tabSet:
WriteTabSet(renderer, tabSet);
return;
case TabItemBlock tabItem:
WriteTabItem(renderer, tabItem);
return;
case LiteralIncludeBlock literalIncludeBlock:
WriteLiteralIncludeBlock(renderer, literalIncludeBlock);
return;
case IncludeBlock includeBlock:
if (includeBlock.Literal)
WriteLiteralIncludeBlock(renderer, includeBlock);
else
WriteIncludeBlock(renderer, includeBlock, markdownParser);
return;
case SettingsBlock settingsBlock:
WriteSettingsBlock(renderer, settingsBlock, markdownParser);
return;
case StepperBlock stepperBlock:
WriteStepperBlock(renderer, stepperBlock, markdownParser);
return;
case StepBlock stepBlock:
WriteStepBlock(renderer, stepBlock, markdownParser);
return;
default:
// if (!string.IsNullOrEmpty(directiveBlock.Info) && !directiveBlock.Info.StartsWith('{'))
// WriteCode(renderer, directiveBlock);
// else if (!string.IsNullOrEmpty(directiveBlock.Info))
// WriteAdmonition(renderer, directiveBlock);
// else
WriteChildren(renderer, directiveBlock);
break;
}
}
private static void WriteImage(HtmlRenderer renderer, ImageBlock block)
{
var imageUrl = block.ImageUrl;
var slice = Image.Create(new ImageViewModel
{
Label = block.Label,
Align = block.Align,
Alt = block.Alt,
Height = block.Height,
Scale = block.Scale,
Target = block.Target,
Width = block.Width,
Screenshot = block.Screenshot,
ImageUrl = imageUrl,
});
RenderRazorSlice(slice, renderer, block);
}
private static void WriteStepperBlock(HtmlRenderer renderer, StepperBlock block, MarkdownParser _)
{
var slice = Stepper.Create(new StepperViewModel());
RenderRazorSlice(slice, renderer, block);
}
private static void WriteStepBlock(HtmlRenderer renderer, StepBlock block, MarkdownParser _)
{
var slice = Step.Create(new StepViewModel
{
Title = block.Title,
Anchor = block.Anchor
});
RenderRazorSlice(slice, renderer, block);
}
private static void WriteFigure(HtmlRenderer renderer, ImageBlock block)
{
var imageUrl = block.ImageUrl != null &&
(block.ImageUrl.StartsWith("/_static") || block.ImageUrl.StartsWith("_static"))
? $"{block.Build.UrlPathPrefix}/{block.ImageUrl.TrimStart('/')}"
: block.ImageUrl;
var slice = Figure.Create(new ImageViewModel
{
Label = block.Label,
Align = block.Align,
Alt = block.Alt,
Height = block.Height,
Scale = block.Scale,
Target = block.Target,
Width = block.Width,
Screenshot = block.Screenshot,
ImageUrl = imageUrl,
});
RenderRazorSlice(slice, renderer, block);
}
private static void WriteChildren(HtmlRenderer renderer, DirectiveBlock directiveBlock) =>
renderer.WriteChildren(directiveBlock);
private static void WriteVersion(HtmlRenderer renderer, VersionBlock block)
{
var slice = Slices.Directives.Version.Create(new VersionViewModel
{
Directive = block.Directive,
Title = block.Title,
VersionClass = block.Class
});
RenderRazorSlice(slice, renderer, block);
}
private static void WriteAdmonition(HtmlRenderer renderer, AdmonitionBlock block)
{
var slice = Admonition.Create(new AdmonitionViewModel
{
Directive = block.Admonition,
CrossReferenceName = block.CrossReferenceName,
Classes = block.Classes,
Title = block.Title,
Open = block.DropdownOpen.GetValueOrDefault() ? "open" : null
});
RenderRazorSlice(slice, renderer, block);
}
private static void WriteDropdown(HtmlRenderer renderer, DropdownBlock block)
{
var slice = Dropdown.Create(new AdmonitionViewModel
{
Directive = block.Admonition,
CrossReferenceName = block.CrossReferenceName,
Classes = block.Classes,
Title = block.Title,
Open = block.DropdownOpen.GetValueOrDefault() ? "open" : null
});
RenderRazorSlice(slice, renderer, block);
}
private static void WriteTabSet(HtmlRenderer renderer, TabSetBlock block)
{
var slice = TabSet.Create(new TabSetViewModel());
RenderRazorSlice(slice, renderer, block);
}
private static void WriteTabItem(HtmlRenderer renderer, TabItemBlock block)
{
var slice = TabItem.Create(new TabItemViewModel
{
Index = block.Index,
Title = block.Title,
TabSetIndex = block.TabSetIndex,
SyncKey = block.SyncKey,
TabSetGroupKey = block.TabSetGroupKey
});
RenderRazorSlice(slice, renderer, block);
}
private static void WriteMermaid(HtmlRenderer renderer, MermaidBlock block)
{
var slice = Mermaid.Create(new MermaidViewModel());
RenderRazorSliceRawContent(slice, renderer, block);
}
private static void WriteLiteralIncludeBlock(HtmlRenderer renderer, IncludeBlock block)
{
if (!block.Found || block.IncludePath is null)
return;
var file = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath);
var content = block.Build.ReadFileSystem.File.ReadAllText(file.FullName);
if (string.IsNullOrEmpty(block.Language))
_ = renderer.Write(content);
else
{
var slice = Code.Create(new CodeViewModel
{
CrossReferenceName = null,
Language = block.Language,
Caption = null,
ApiCallHeader = null
});
RenderRazorSlice(slice, renderer, content);
}
}
private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block, MarkdownParser parser)
{
if (!block.Found || block.IncludePath is null)
return;
var snippet = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath);
var parentPath = block.Context.MarkdownParentPath ?? block.Context.MarkdownSourcePath;
var document = parser.ParseSnippetAsync(snippet, parentPath, block.Context.YamlFrontMatter, default).GetAwaiter().GetResult();
var html = document.ToHtml(parser.Pipeline);
_ = renderer.Write(html);
}
private static void WriteSettingsBlock(HtmlRenderer renderer, SettingsBlock block, MarkdownParser parser)
{
if (!block.Found || block.IncludePath is null)
return;
var file = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath);
YamlSettings? settings;
try
{
var yaml = file.FileSystem.File.ReadAllText(file.FullName);
settings = YamlSerialization.Deserialize<YamlSettings>(yaml);
}
catch (YamlException e)
{
block.EmitError("Can not be parsed as a valid settings file", e.InnerException ?? e);
return;
}
catch (Exception e)
{
block.EmitError("Can not be parsed as a valid settings file", e);
return;
}
var slice = Slices.Directives.Settings.Create(new SettingsViewModel
{
SettingsCollection = settings,
RenderMarkdown = s =>
{
var document = parser.ParseStringAsync(s, block.IncludeFrom, block.Context.YamlFrontMatter);
var html = document.ToHtml(parser.Pipeline);
return html;
}
});
RenderRazorSliceNoContent(slice, renderer);
}
[SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")]
private static void RenderRazorSlice<T>(RazorSlice<T> slice, HtmlRenderer renderer, string contents)
{
var html = slice.RenderAsync().GetAwaiter().GetResult();
var blocks = html.Split("[CONTENT]", 2, StringSplitOptions.RemoveEmptyEntries);
_ = renderer
.Write(blocks[0])
.Write(contents)
.Write(blocks[1]);
}
[SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")]
private static void RenderRazorSlice<T>(RazorSlice<T> slice, HtmlRenderer renderer, DirectiveBlock obj)
{
var html = slice.RenderAsync().GetAwaiter().GetResult();
var blocks = html.Split("[CONTENT]", 2, StringSplitOptions.RemoveEmptyEntries);
_ = renderer.Write(blocks[0]);
renderer.WriteChildren(obj);
_ = renderer.Write(blocks[1]);
}
[SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")]
private static void RenderRazorSliceNoContent<T>(RazorSlice<T> slice, HtmlRenderer renderer)
{
var html = slice.RenderAsync().GetAwaiter().GetResult();
_ = renderer.Write(html);
}
[SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")]
private static void RenderRazorSliceRawContent<T>(RazorSlice<T> slice, HtmlRenderer renderer, DirectiveBlock obj)
{
var html = slice.RenderAsync().GetAwaiter().GetResult();
var blocks = html.Split("[CONTENT]", 2, StringSplitOptions.RemoveEmptyEntries);
_ = renderer.Write(blocks[0]);
foreach (var o in obj)
Render(o);
_ = renderer.Write(blocks[1]);
void RenderLeaf(LeafBlock p)
{
_ = renderer.WriteLeafRawLines(p, true, false, false);
renderer.EnableHtmlForInline = false;
foreach (var oo in p.Inline ?? [])
{
if (oo is SubstitutionLeaf sl)
_ = renderer.Write(sl.Replacement);
else if (oo is LiteralInline li)
renderer.Write(li);
else if (oo is LineBreakInline)
_ = renderer.WriteLine();
else if (oo is Role r)
{
_ = renderer.Write(new string(r.DelimiterChar, r.DelimiterCount));
renderer.WriteChildren(r);
}
else
_ = renderer.Write($"(LeafBlock: {oo.GetType().Name}");
}
renderer.EnableHtmlForInline = true;
}
void RenderListBlock(ListBlock l)
{
foreach (var bb in l)
{
if (bb is LeafBlock lbi)
RenderLeaf(lbi);
else if (bb is ListItemBlock ll)
{
_ = renderer.Write(ll.TriviaBefore);
_ = renderer.Write("-");
foreach (var lll in ll)
Render(lll);
}
else
_ = renderer.Write($"(ListBlock: {l.GetType().Name}");
}
}
void Render(Block o)
{
if (o is LeafBlock p)
RenderLeaf(p);
else if (o is ListBlock l)
RenderListBlock(l);
else
_ = renderer.Write($"(Block: {o.GetType().Name}");
}
}
}