Skip to content

Instantly share code, notes, and snippets.

@flew2bits
Created March 12, 2023 14:27
Show Gist options
  • Save flew2bits/e7087605c73ee3723f892bcdc5a5f288 to your computer and use it in GitHub Desktop.
Save flew2bits/e7087605c73ee3723f892bcdc5a5f288 to your computer and use it in GitHub Desktop.
Updated Markdown component for QuestPDF
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