-
-
Save dario-l/d619c1003c219f3bf6d7c9c9af5d91d9 to your computer and use it in GitHub Desktop.
Updated Markdown component for QuestPDF
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Markdig; | |
using Markdig.Syntax; | |
using Markdig.Syntax.Inlines; | |
using QuestPDF.Fluent; | |
using QuestPDF.Helpers; | |
using QuestPDF.Infrastructure; | |
namespace PdfExperiments; | |
public enum BlockType | |
{ | |
None, | |
Heading1, | |
Heading2, | |
Heading3, | |
Heading4, | |
Heading5, | |
Heading, | |
Paragraph, | |
BlockQuote, | |
BlockQuoteLine, | |
OrderedList, | |
UnorderedList, | |
ListItem, | |
Rule, | |
Other, | |
} | |
public record InlineStylers | |
{ | |
public static readonly InlineStylers DefaultStylers = new(TextStyle.Default, s => s.Bold(), s => s.Italic()); | |
private InlineStylers(TextStyle? @default, Func<TextStyle, TextStyle>? bold, Func<TextStyle, TextStyle>? italic) | |
{ | |
Default = @default; | |
Bold = bold; | |
Italic = italic; | |
} | |
public TextStyle? Default { get; init; } | |
public Func<TextStyle, TextStyle>? Bold { get; init; } | |
public Func<TextStyle, TextStyle>? Italic { get; init; } | |
} | |
public record BlockStyler(Func<IContainer, IContainer> ContainerStyler); | |
public record StyledBlockStyler(Func<IContainer, IContainer> ContainerStyler, TextStyle DefaultTextStyle) | |
:BlockStyler(ContainerStyler); | |
public record LeafBlockStyler(Func<IContainer, IContainer> ContainerStyler, InlineStylers TextStylers) | |
: BlockStyler(ContainerStyler); | |
public record BlockRenderer | |
(Func<IContainer, IContainer> ContainerStyler, Action<object, IContainer> Renderer) : BlockStyler(ContainerStyler); | |
public class MarkdownComponent : IComponent | |
{ | |
private readonly MarkdownDocument _parsed; | |
private readonly Stack<BlockType> _blockStack = new(); | |
private static readonly (BlockType[] Path, BlockStyler Styler)[] DefaultStylers = | |
{ | |
(Array.Empty<BlockType>(), new BlockStyler(c => c)), | |
(new[] { BlockType.Rule }, new BlockRenderer(c => c.PaddingVertical(4.5f).PaddingHorizontal(144), | |
(_, c) => | |
c.LineHorizontal(.5f).LineColor(Colors.Grey.Darken2))), | |
(new[] { BlockType.OrderedList }, new StyledBlockStyler(c => c.PaddingRight(9), TextStyle.Default.FontSize(14))), | |
(new[] { BlockType.UnorderedList }, new StyledBlockStyler(c => c.PaddingRight(9), TextStyle.Default.FontSize(14))), | |
(new[] { BlockType.ListItem, BlockType.Paragraph }, new LeafBlockStyler(c => c.PaddingBottom(2.25f), | |
InlineStylers.DefaultStylers with { Default = TextStyle.Default.FontSize(14)})), | |
(new[] { BlockType.Heading1 }, new LeafBlockStyler( | |
c => c.PaddingBottom(9).BorderBottom(1).BorderColor(Colors.Black), | |
InlineStylers.DefaultStylers with | |
{ | |
Default = new TextStyle().FontFamily(Fonts.TimesNewRoman).Bold().FontSize(24), | |
Bold = s => s.Black() | |
})), | |
(new[] { BlockType.Heading2 }, new LeafBlockStyler( | |
c => c.PaddingBottom(4.5f).BorderBottom(0.5f).BorderColor(Colors.Grey.Medium), | |
InlineStylers.DefaultStylers with | |
{ | |
Default = new TextStyle().FontFamily(Fonts.TimesNewRoman).Bold().FontSize(20), | |
Bold = s => s.ExtraBold() | |
})), | |
(new[] { BlockType.Heading3 }, new LeafBlockStyler( | |
c => c.PaddingBottom(9), | |
InlineStylers.DefaultStylers with | |
{ | |
Default = new TextStyle().FontFamily(Fonts.TimesNewRoman).Bold().FontSize(18), | |
Bold = s => s.ExtraBold() | |
})), | |
(new[] { BlockType.Heading4 }, new LeafBlockStyler( | |
c => c.PaddingBottom(9), | |
InlineStylers.DefaultStylers with | |
{ | |
Default = new TextStyle().Bold().FontSize(16), | |
Bold = s => s.ExtraBold() | |
})), | |
(new[] { BlockType.Heading5 }, new LeafBlockStyler( | |
c => c.PaddingBottom(9), | |
InlineStylers.DefaultStylers with | |
{ | |
Default = new TextStyle().Bold().FontSize(14), | |
Bold = s => s.ExtraBold() | |
})), | |
(new[] { BlockType.BlockQuote }, new BlockStyler(c => | |
c | |
.PaddingRight(72) | |
.PaddingVertical(9) | |
.BorderLeft(4) | |
.BorderColor(Colors.Grey.Medium) | |
.PaddingLeft(9) | |
)), | |
(new[] { BlockType.BlockQuote, BlockType.BlockQuote }, new BlockStyler(c => | |
c.PaddingHorizontal(4.5f).BorderLeft(2).BorderColor(Colors.Grey.Lighten1).PaddingLeft(9))), | |
(new[] { BlockType.BlockQuote, BlockType.Paragraph }, new LeafBlockStyler(c => | |
c.PaddingVertical(9), | |
InlineStylers.DefaultStylers with | |
{ | |
Default = new TextStyle().FontSize(18).Italic(), Italic = ts => ts.Italic(false) | |
})), | |
(new[] { BlockType.BlockQuote, BlockType.Heading4 }, new LeafBlockStyler( | |
c => c, InlineStylers.DefaultStylers with { Default = TextStyle.Default.Italic().Bold().FontSize(18) })), | |
(new[] { BlockType.Paragraph }, new LeafBlockStyler(c => c.PaddingBottom(9), | |
InlineStylers.DefaultStylers with { Default = new TextStyle().FontSize(15) })) | |
}; | |
public MarkdownComponent(string markdown) | |
{ | |
_parsed = Markdown.Parse(markdown); | |
} | |
private (Func<IContainer, IContainer>, InlineStylers, Action<object, IContainer> Renderer) GetStyler() | |
{ | |
var blocks = _blockStack.Reverse().ToArray(); | |
var firstMatch = DefaultStylers.OrderByDescending(s => s.Path.Length).Where(s => s.Path.Length != 0) | |
.FirstOrDefault(d => d.Path.SequenceEqual(blocks.TakeLast(d.Path.Length))); | |
var styler = firstMatch.Styler ?? DefaultStylers[0].Styler; | |
return styler switch | |
{ | |
BlockRenderer renderer => (renderer.ContainerStyler, InlineStylers.DefaultStylers, renderer.Renderer), | |
StyledBlockStyler blockStyler => (blockStyler.ContainerStyler, | |
InlineStylers.DefaultStylers with {Default = blockStyler.DefaultTextStyle}, (_, _) => { }), | |
LeafBlockStyler leafStyler => (leafStyler.ContainerStyler, leafStyler.TextStylers, (_, _) => { }), | |
(ContainerStyler: var containerStyler) => (containerStyler, InlineStylers.DefaultStylers, (_, _) => { }), | |
_ => throw new InvalidOperationException("Could not find valid styler") | |
}; | |
} | |
public void Compose(IContainer container) | |
{ | |
container.Column(columnDescriptor => | |
{ | |
foreach (var block in _parsed) | |
{ | |
RenderBlock(columnDescriptor, block); | |
} | |
}); | |
} | |
private static BlockType GetBlockType(object? block) | |
=> block switch | |
{ | |
HeadingBlock { Level: 1 } => BlockType.Heading1, | |
HeadingBlock { Level: 2 } => BlockType.Heading2, | |
HeadingBlock { Level: 3 } => BlockType.Heading3, | |
HeadingBlock { Level: 4 } => BlockType.Heading4, | |
HeadingBlock { Level: 5 } => BlockType.Heading5, | |
HeadingBlock => BlockType.Heading, | |
ParagraphBlock => BlockType.Paragraph, | |
QuoteBlock => BlockType.BlockQuote, | |
QuoteBlockLine => BlockType.BlockQuoteLine, | |
ThematicBreakBlock => BlockType.Rule, | |
ListBlock { IsOrdered: true } => BlockType.OrderedList, | |
ListBlock { IsOrdered: false } => BlockType.UnorderedList, | |
ListItemBlock => BlockType.ListItem, | |
_ => BlockType.None | |
}; | |
private void RenderBlock(ColumnDescriptor columnDescriptor, Block? block) | |
{ | |
if (block is null) return; | |
var blockType = GetBlockType(block); | |
if (blockType is BlockType.None) return; | |
_blockStack.Push(blockType); | |
var (blockStyler, textStylers, renderer) = GetStyler(); | |
switch (block) | |
{ | |
case HeadingBlock heading: | |
{ | |
if (heading.Inline is null) break; | |
var item = columnDescriptor.Item(); | |
blockStyler(item).Text(t => RenderInline(t, heading.Inline, textStylers)); | |
break; | |
} | |
case ParagraphBlock paragraph: | |
{ | |
if (paragraph.Inline is null) break; | |
var blockItem = columnDescriptor.Item(); | |
blockStyler(blockItem).Text(textDescriptor => | |
{ | |
RenderInline(textDescriptor, paragraph.Inline, textStylers); | |
}); | |
break; | |
} | |
case QuoteBlock quote: | |
if (!quote.Any()) break; | |
var blockQuoteItem = blockStyler(columnDescriptor.Item()); | |
blockQuoteItem.Column(column => | |
{ | |
foreach (var line in quote) | |
{ | |
RenderBlock(column, line); | |
} | |
}); | |
break; | |
case ListBlock list: | |
if (blockType is BlockType.OrderedList) | |
{ | |
var index = int.Parse(list.OrderedStart!); | |
foreach (var item in list) | |
{ | |
columnDescriptor.Item().Row(row => | |
{ | |
blockStyler(row.AutoItem()).Text($"{index}.").Style(textStylers.Default!); | |
row.RelativeItem().Column(listColumn => RenderBlock(listColumn, item)); | |
index++; | |
}); | |
} | |
} | |
else | |
{ | |
foreach (var item in list) | |
{ | |
columnDescriptor.Item().Row(row => | |
{ | |
blockStyler(row.AutoItem()).Text("\x2022").Style(textStylers.Default!); | |
row.RelativeItem().Column(listColumn => RenderBlock(listColumn, item)); | |
}); | |
} | |
} | |
break; | |
case ListItemBlock listItem: | |
foreach (var child in listItem) | |
{ | |
RenderBlock(columnDescriptor, child); | |
} | |
break; | |
case ThematicBreakBlock rule: | |
renderer(rule, blockStyler(columnDescriptor.Item())); | |
break; | |
default: | |
Console.WriteLine($"{blockType}: {block.GetType().Name}"); | |
break; | |
} | |
_blockStack.Pop(); | |
} | |
private static void RenderInline(TextDescriptor text, Inline inline, InlineStylers textStylers, | |
TextStyle? textStyle = null) | |
{ | |
textStyle ??= textStylers.Default ?? TextStyle.Default; | |
switch (inline) | |
{ | |
case LiteralInline literal: | |
text.Span(literal.Content.ToString()).Style(textStyle); | |
break; | |
case EmphasisInline emphasis when emphasis.Any(): | |
{ | |
foreach (var child in emphasis) | |
{ | |
switch (emphasis.DelimiterCount) | |
{ | |
case 1: | |
RenderInline(text, child, textStylers, textStylers.Italic!(textStyle)); | |
break; | |
case 2: | |
RenderInline(text, child, textStylers, textStylers.Bold!(textStyle)); | |
break; | |
default: | |
RenderInline(text, child, textStylers, textStyle); | |
break; | |
} | |
} | |
break; | |
} | |
case ContainerInline container: | |
foreach (var item in container) | |
{ | |
RenderInline(text, item, textStylers, textStyle); | |
} | |
break; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment