Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions MarkdownRenderer/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project>
<PropertyGroup>
<_MarkdownRendererThorVgArch Condition="'$(Platform)' == '' or '$(Platform)' == 'AnyCPU' or '$(Platform)' == 'Any CPU'">x64</_MarkdownRendererThorVgArch>
<_MarkdownRendererThorVgArch Condition="'$(_MarkdownRendererThorVgArch)' == '' and ('$(Platform)' == 'x86' or '$(PlatformTarget)' == 'x86' or '$(RuntimeIdentifier)' == 'win-x86')">x86</_MarkdownRendererThorVgArch>
<_MarkdownRendererThorVgArch Condition="'$(_MarkdownRendererThorVgArch)' == '' and ('$(Platform)' == 'x64' or '$(PlatformTarget)' == 'x64' or '$(RuntimeIdentifier)' == 'win-x64')">x64</_MarkdownRendererThorVgArch>
<_MarkdownRendererThorVgArch Condition="'$(_MarkdownRendererThorVgArch)' == '' and ('$(Platform)' == 'ARM64' or '$(Platform)' == 'arm64' or '$(PlatformTarget)' == 'ARM64' or '$(PlatformTarget)' == 'arm64' or '$(RuntimeIdentifier)' == 'win-arm64')">arm64</_MarkdownRendererThorVgArch>
<_MarkdownRendererThorVgSource>$(MSBuildThisFileDirectory)MarkdownRenderer\native\win-$(_MarkdownRendererThorVgArch)\thorvg.dll</_MarkdownRendererThorVgSource>
</PropertyGroup>

<Target Name="CopyMarkdownRendererThorVgNativeAssetToOutputRoot"
AfterTargets="Build"
Condition="'$(_MarkdownRendererThorVgArch)' != '' and Exists('$(_MarkdownRendererThorVgSource)') and '$(OutDir)' != ''">
<Copy SourceFiles="$(_MarkdownRendererThorVgSource)"
DestinationFiles="$(OutDir)thorvg.dll"
SkipUnchangedFiles="true" />
</Target>
</Project>
81 changes: 74 additions & 7 deletions MarkdownRenderer/MarkdownRenderer.Gfm/GfmChildBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using Markdig.Extensions.Abbreviations;
using MarkdownRenderer.Layout;
using MarkdownRenderer.Layout.Boxes;
using MarkdownRenderer.Theming;
Expand All @@ -18,6 +19,7 @@ internal static void PopulateChildren(StackBox stack, ContainerBlock container,
{
foreach (var child in container)
{
context.CancellationToken.ThrowIfCancellationRequested();
var box = TryBuildBlock(child, context);
if (box is not null) stack.Add(box);
}
Expand All @@ -26,14 +28,18 @@ internal static void PopulateChildren(StackBox stack, ContainerBlock container,
/// <summary>Attempts to build a <see cref="BlockBox"/> for a Markdig block node.</summary>
internal static BlockBox? TryBuildBlock(Block block, MarkdownLayoutContext context)
{
using var attrScope = context.PushMarkdownAttributes(block);
BlockBox? box = null;

// Check registry first so registered custom renderers are honoured.
if (context.Registry.TryGetRenderer(block.GetType(), out var renderer) && renderer is not null)
{
var custom = renderer.BuildBlock(block, context);
if (custom is not null) return custom;
if (custom is not null)
box = custom;
}

return block switch
box ??= block switch
{
ParagraphBlock p => BuildLeaf(p, context, MarkdownElementKeys.Body),
HeadingBlock h => BuildLeaf(h, context, h.Level switch
Expand All @@ -48,6 +54,15 @@ internal static void PopulateChildren(StackBox stack, ContainerBlock container,
ContainerBlock cb => BuildContainer(cb, context),
_ => null
};

if (box is not null)
{
if (box.BlockIndex == 0)
box.BlockIndex = context.NextBlockIndex();
context.RegisterMarkdownAttributes(block, box.BlockIndex);
}

return box;
}

private static InlineContainerBox BuildLeaf(LeafBlock leaf, MarkdownLayoutContext context, string elementKey)
Expand All @@ -61,7 +76,11 @@ private static InlineContainerBox BuildLeaf(LeafBlock leaf, MarkdownLayoutContex

private static StackBox BuildContainer(ContainerBlock cb, MarkdownLayoutContext context)
{
var stack = new StackBox();
var stack = new StackBox
{
FlowDirection = context.FlowDirection,
};
stack.BlockIndex = context.NextBlockIndex();
PopulateChildren(stack, cb, context);
return stack;
}
Expand All @@ -71,17 +90,25 @@ internal static void AddInlines(InlineContainerBox box, ContainerInline inlines,
bool skippedFirst = skipFirstIf is null;
foreach (var i in inlines)
{
box.Context.CancellationToken.ThrowIfCancellationRequested();
if (!skippedFirst)
{
skippedFirst = true;
if (skipFirstIf!(i)) continue;
}
var run = BuildInline(i);
if (run is not null) box.Add(run);
int aliasStart = box.Context.StyleAliasCount;
using var inlineAttrs = box.Context.PushMarkdownAttributes(i);
var run = BuildInline(i, box.Context);
if (run is not null)
{
run.SetStyleAliases(box.Context.CreateStyleAliasSnapshotFrom(aliasStart));
box.Context.RegisterMarkdownAttributes(i, box.BlockIndex);
box.Add(run);
}
}
}

private static InlineRun? BuildInline(Inline inline) => inline switch
private static InlineRun? BuildInline(Inline inline, MarkdownLayoutContext context) => inline switch
{
LiteralInline lit => new TextRun(lit.Content.ToString())
{
Expand All @@ -93,6 +120,17 @@ internal static void AddInlines(InlineContainerBox box, ContainerInline inlines,
SourceSpan = new MarkdownRenderer.SourceSpan(ci.Span.Start, ci.Span.Length)
},
EmphasisInline emph => BuildEmphasis(emph),
LinkInline link => BuildLink(link, context),
AbbreviationInline abbreviation => new AbbreviationRun(
abbreviation.Abbreviation?.Label ?? string.Empty,
abbreviation.Abbreviation?.Text.ToString() ?? string.Empty)
{
SourceSpan = new MarkdownRenderer.SourceSpan(abbreviation.Span.Start, abbreviation.Span.Length)
},
AutolinkInline al => new LinkRun(al.Url, al.Url)
{
SourceSpan = new MarkdownRenderer.SourceSpan(al.Span.Start, al.Span.Length)
},
LineBreakInline => new LineBreakRun
{
SourceSpan = new MarkdownRenderer.SourceSpan(inline.Span.Start, inline.Span.Length)
Expand All @@ -106,8 +144,16 @@ private static InlineRun BuildEmphasis(EmphasisInline emph)
var sb = new StringBuilder();
FlattenInlines(emph, sb);
var span = new MarkdownRenderer.SourceSpan(emph.Span.Start, emph.Span.Length);
if (emph.DelimiterChar == '~')
if (emph.DelimiterChar == '~' && emph.DelimiterCount >= 2)
return new StrikethroughRun(sb.ToString()) { SourceSpan = span };
if (emph.DelimiterChar == '~')
return new SubscriptRun(sb.ToString()) { SourceSpan = span };
if (emph.DelimiterChar == '^')
return new SuperscriptRun(sb.ToString()) { SourceSpan = span };
if (emph.DelimiterChar == '+')
return new InsertedRun(sb.ToString()) { SourceSpan = span };
if (emph.DelimiterChar == '=')
return new MarkedRun(sb.ToString()) { SourceSpan = span };
return emph.DelimiterCount >= 2
? new StrongRun(sb.ToString()) { SourceSpan = span }
: new EmphasisRun(sb.ToString()) { SourceSpan = span };
Expand All @@ -124,6 +170,26 @@ private static TextRun FlattenAsTextRun(ContainerInline ci)
};
}

private static InlineRun BuildLink(LinkInline link, MarkdownLayoutContext context)
{
if (link.IsImage)
{
var alt = new StringBuilder();
FlattenInlines(link, alt);
return new InlineImageRun(context, alt.Length > 0 ? alt.ToString() : "image", link.Url ?? string.Empty, link.Title)
{
SourceSpan = new MarkdownRenderer.SourceSpan(link.Span.Start, link.Span.Length)
};
}

var text = new StringBuilder();
FlattenInlines(link, text);
return new LinkRun(text.ToString(), link.Url ?? string.Empty, link.Title)
{
SourceSpan = new MarkdownRenderer.SourceSpan(link.Span.Start, link.Span.Length)
};
}

internal static void FlattenInlines(ContainerInline container, StringBuilder sb)
{
foreach (var child in container)
Expand All @@ -132,6 +198,7 @@ internal static void FlattenInlines(ContainerInline container, StringBuilder sb)
{
case LiteralInline lit: sb.Append(lit.Content.ToString()); break;
case CodeInline ci: sb.Append(ci.Content); break;
case AbbreviationInline ab: sb.Append(ab.Abbreviation?.Label ?? string.Empty); break;
case LineBreakInline: sb.Append('\n'); break;
case ContainerInline c2: FlattenInlines(c2, sb); break;
}
Expand Down
58 changes: 58 additions & 0 deletions MarkdownRenderer/MarkdownRenderer.Gfm/GfmExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
using Markdig;
using Markdig.Extensions.DefinitionLists;
using Markdig.Extensions.Figures;
using Markdig.Extensions.Footnotes;
using Markdig.Extensions.Tables;
using Markdig.Syntax;
using MarkdownRenderer.Controls;
using MarkdownRenderer.Hosting;
using MarkdownRenderer.Gfm.Renderers;
using MarkdownRenderer.Parsing;
using MarkdownRenderer.Theming;

namespace MarkdownRenderer.Gfm;

Expand All @@ -14,8 +19,15 @@ namespace MarkdownRenderer.Gfm;
/// </summary>
public static class GfmExtensions
{
/// <summary>
/// Adds GitHub-flavored markdown parsing and rendering support to a registry.
/// </summary>
/// <param name="registry">Registry to configure.</param>
/// <returns>The same registry for fluent chaining.</returns>
public static MarkdownExtensionRegistry UseGitHubFlavoredMarkdown(this MarkdownExtensionRegistry registry)
{
if (registry is null) throw new System.ArgumentNullException(nameof(registry));

registry.ConfigurePipeline(p =>
{
p.UsePipeTables();
Expand All @@ -34,4 +46,50 @@ public static MarkdownExtensionRegistry UseGitHubFlavoredMarkdown(this MarkdownE

return registry;
}

/// <summary>
/// Configures a control builder to use GitHub-flavored markdown.
/// </summary>
/// <param name="builder">Builder to configure.</param>
/// <returns>The same builder for fluent chaining.</returns>
public static MarkdownRendererControlBuilder UseGitHubFlavoredMarkdown(this MarkdownRendererControlBuilder builder)
{
if (builder is null) throw new System.ArgumentNullException(nameof(builder));
return builder.ConfigureExtensions(registry => registry.UseGitHubFlavoredMarkdown());
}

/// <summary>
/// Adds Markdown Extra style features that are not part of GitHub-flavored markdown:
/// definition lists, abbreviations, and figure/caption blocks.
/// </summary>
/// <param name="registry">Registry to configure.</param>
/// <returns>The same registry for fluent chaining.</returns>
public static MarkdownExtensionRegistry UseMarkdownExtra(this MarkdownExtensionRegistry registry)
{
if (registry is null) throw new System.ArgumentNullException(nameof(registry));

registry.ConfigurePipeline(p =>
{
p.UseDefinitionLists();
p.UseAbbreviations();
p.UseFigures();
});

registry.RegisterRenderer<DefinitionList>(new DefinitionListRenderer());
registry.RegisterRenderer<Figure>(new FigureRenderer());

return registry;
}

/// <summary>
/// Configures a control builder to use Markdown Extra style features that are
/// intentionally kept separate from the strict GFM helper.
/// </summary>
/// <param name="builder">Builder to configure.</param>
/// <returns>The same builder for fluent chaining.</returns>
public static MarkdownRendererControlBuilder UseMarkdownExtra(this MarkdownRendererControlBuilder builder)
{
if (builder is null) throw new System.ArgumentNullException(nameof(builder));
return builder.ConfigureExtensions(registry => registry.UseMarkdownExtra());
}
}
32 changes: 32 additions & 0 deletions MarkdownRenderer/MarkdownRenderer.Gfm/GfmMarkdownRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using MarkdownRenderer.Controls;
using MarkdownRenderer.Hosting;
using MarkdownRenderer.Theming;

namespace MarkdownRenderer.Gfm;

/// <summary>
/// Convenience factory for creating a renderer with GitHub-flavored markdown enabled.
/// </summary>
public static class GfmMarkdownRenderer
{
/// <summary>
/// Creates a renderer configured with GitHub-flavored markdown support.
/// </summary>
/// <param name="markdown">Initial markdown source text.</param>
/// <param name="theme">Theme to assign, or null to use the renderer default.</param>
/// <param name="embedFactory">Embed factory to assign, or null to disable hosted block embeds.</param>
/// <param name="isSelectionEnabled">True to enable text selection.</param>
/// <returns>A new configured renderer control.</returns>
public static MarkdownRendererControl CreateDefault(
string? markdown = null,
MarkdownTheme? theme = null,
IMarkdownEmbedFactory? embedFactory = null,
bool isSelectionEnabled = true)
=> new MarkdownRendererControlBuilder()
.UseGitHubFlavoredMarkdown()
.WithMarkdown(markdown)
.WithTheme(theme)
.WithEmbedFactory(embedFactory)
.WithSelectionEnabled(isSelectionEnabled)
.Build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,23 @@
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<WarningsAsErrors>$(WarningsAsErrors);IL2026;IL2046;IL2050;IL2055;IL2057;IL2058;IL2059;IL2060;IL2062;IL2063;IL2064;IL2065;IL2066;IL2067;IL2068;IL2069;IL2070;IL2071;IL2072;IL2073;IL2074;IL2075;IL2076;IL2077;IL2078;IL2079;IL2080;IL2081;IL2082;IL2083;IL2084;IL2085;IL2086;IL2087;IL2088;IL2089;IL2090;IL2091;IL2092;IL2093;IL2094;IL2095;IL2096;IL2097;IL2098;IL2099;IL2100;IL2101;IL2102;IL2103;IL2104;IL2105;IL2106;IL2107;IL2108;IL2109;IL2110;IL2111;IL2112;IL2113;IL2114;IL2115;IL2116;IL3050;IL3051;IL3052;IL3053;IL3054;IL3055;IL3056</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<PackageId>MarkdownRenderer.Gfm</PackageId>
<Version>0.1.0</Version>
<Authors>nerocui</Authors>
<Description>GitHub-flavored markdown extensions for MarkdownRenderer, plus opt-in Markdown Extra helpers for definition lists, abbreviations, and figures.</Description>
<PackageTags>markdown;winui;win2d;markdig;windows;svg</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/JitHubApp/JitHubV2</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageProjectUrl>https://github.com/JitHubApp/JitHubV2/tree/main/docs/markdown-renderer</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon-192.png</PackageIcon>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\MarkdownRenderer\MarkdownRenderer.csproj" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="" Visible="false" />
<None Include="..\..\docs\markdown-renderer\README.md" Pack="true" PackagePath="README.md" Visible="false" />
<None Include="..\..\JitHub.Web\wwwroot\icon-192.png" Pack="true" PackagePath="icon-192.png" Visible="false" />
</ItemGroup>
</Project>
Loading
Loading