diff --git a/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfAutolinkInlineRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfAutolinkInlineRenderer.cs new file mode 100644 index 00000000..3cd22955 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfAutolinkInlineRenderer.cs @@ -0,0 +1,22 @@ +#nullable enable + +using Markdig.Syntax.Inlines; + +namespace Velopack.Packaging.Rtf.Inlines; + +/// +/// An RTF renderer for an . +/// +/// +public class RtfAutolinkInlineRenderer : RtfObjectRenderer +{ + public string? Rel { get; set; } + + protected override void Write(RtfRenderer renderer, AutolinkInline obj) + { + // Render as underlined, blue text (simulating a link in RTF) + renderer.Write("{\\ul\\cf1 "); + renderer.WriteEscape(obj.Url); + renderer.Write("}"); + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfCodeInlineRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfCodeInlineRenderer.cs new file mode 100644 index 00000000..cd154ad3 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfCodeInlineRenderer.cs @@ -0,0 +1,19 @@ +#nullable enable + +using Markdig.Syntax.Inlines; + +namespace Velopack.Packaging.Rtf.Inlines; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfCodeInlineRenderer : RtfObjectRenderer +{ + protected override void Write(RtfRenderer renderer, CodeInline obj) + { + renderer.Write("{\\fmodern "); + renderer.WriteEscape(obj.Content); + renderer.Write("}"); + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfDelimiterInlineRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfDelimiterInlineRenderer.cs new file mode 100644 index 00000000..8fd0edb1 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfDelimiterInlineRenderer.cs @@ -0,0 +1,18 @@ +#nullable enable + +using Markdig.Syntax.Inlines; + +namespace Velopack.Packaging.Rtf.Inlines; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfDelimiterInlineRenderer : RtfObjectRenderer +{ + protected override void Write(RtfRenderer renderer, DelimiterInline obj) + { + renderer.WriteEscape(obj.ToLiteral()); + renderer.WriteChildren(obj); + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfEmphasisInlineRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfEmphasisInlineRenderer.cs new file mode 100644 index 00000000..a4d648c3 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfEmphasisInlineRenderer.cs @@ -0,0 +1,32 @@ +#nullable enable + +using Markdig.Syntax.Inlines; + +namespace Velopack.Packaging.Rtf.Inlines; + +/// +/// An RTF renderer for an . +/// +/// +public class RtfEmphasisInlineRenderer : RtfObjectRenderer +{ + public delegate string? GetTagDelegate(EmphasisInline obj); + public GetTagDelegate? GetTag { get; set; } + + protected override void Write(RtfRenderer renderer, EmphasisInline obj) + { + // Use bold or italic for emphasis + var tag = GetTag?.Invoke(obj) ?? GetDefaultTag(obj); + if (tag == "b") + renderer.Write("{\\b "); + else if (tag == "i") + renderer.Write("{\\i "); + renderer.WriteChildren(obj); + if (tag == "b" || tag == "i") + renderer.Write("}"); + } + public static string? GetDefaultTag(EmphasisInline obj) + { + return obj.DelimiterChar == '*' || obj.DelimiterChar == '_' ? (obj.DelimiterCount == 2 ? "b" : "i") : null; + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfLineBreakInlineRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfLineBreakInlineRenderer.cs new file mode 100644 index 00000000..51c0c65b --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfLineBreakInlineRenderer.cs @@ -0,0 +1,21 @@ +#nullable enable + +using Markdig.Syntax.Inlines; + +namespace Velopack.Packaging.Rtf.Inlines; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfLineBreakInlineRenderer : RtfObjectRenderer +{ + public bool RenderAsHardlineBreak { get; set; } + + protected override void Write(RtfRenderer renderer, LineBreakInline obj) + { + if (renderer.IsLastInContainer) return; + renderer.Write("\\line "); // RTF line break + renderer.EnsureLine(); + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfLinkInlineRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfLinkInlineRenderer.cs new file mode 100644 index 00000000..0de9d959 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfLinkInlineRenderer.cs @@ -0,0 +1,30 @@ +#nullable enable + +using Markdig.Syntax.Inlines; + +namespace Velopack.Packaging.Rtf.Inlines; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfLinkInlineRenderer : RtfObjectRenderer +{ + public string? Rel { get; set; } + + protected override void Write(RtfRenderer renderer, LinkInline link) + { + if (link.IsImage) + { + // Skip images entirely in RTF output + return; + } + else + { + // Simulate link: underline, blue + renderer.Write("{\\ul\\cf1 "); + renderer.WriteChildren(link); + renderer.Write("}"); + } + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfLiteralInlineRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfLiteralInlineRenderer.cs new file mode 100644 index 00000000..749e1171 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/Inlines/RtfLiteralInlineRenderer.cs @@ -0,0 +1,17 @@ +#nullable enable + +using Markdig.Syntax.Inlines; + +namespace Velopack.Packaging.Rtf.Inlines; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfLiteralInlineRenderer : RtfObjectRenderer +{ + protected override void Write(RtfRenderer renderer, LiteralInline obj) + { + renderer.WriteEscape(ref obj.Content); + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/RtfBlockRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/RtfBlockRenderer.cs new file mode 100644 index 00000000..d70a2712 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/RtfBlockRenderer.cs @@ -0,0 +1,20 @@ +#nullable enable + +using Markdig.Syntax; + +namespace Velopack.Packaging.Rtf; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfBlockRenderer : RtfObjectRenderer +{ + protected override void Write(RtfRenderer renderer, HtmlBlock obj) + { + // Placeholder: Write the block as RTF raw text (should be adapted for real RTF output) + renderer.Write("{\\rtf1 "); + renderer.WriteLeafRawLines(obj, true, false); + renderer.Write("}\n"); + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/RtfCodeBlockRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/RtfCodeBlockRenderer.cs new file mode 100644 index 00000000..2d7abb02 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/RtfCodeBlockRenderer.cs @@ -0,0 +1,32 @@ +#nullable enable + +using Markdig.Syntax; + +namespace Velopack.Packaging.Rtf; + +/// +/// An RTF renderer for a and . +/// +/// +public class RtfCodeBlockRenderer : RtfObjectRenderer +{ + public bool OutputAttributesOnPre { get; set; } + + protected override void Write(RtfRenderer renderer, CodeBlock obj) + { + renderer.EnsureLine(); + if (obj is FencedCodeBlock { Info: string info }) + { + // For RTF, just render code in a monospaced block with a border + renderer.Write("{\\pard\\fmodern\\brdrb\\brdrs "); + renderer.WriteLeafRawLines(obj, true, true); + renderer.Write(" \\par}\n"); + } + else + { + renderer.Write("{\\pard\\fmodern "); + renderer.WriteLeafRawLines(obj, true, true); + renderer.Write(" \\par}\n"); + } + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/RtfHeadingRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/RtfHeadingRenderer.cs new file mode 100644 index 00000000..8ca10df7 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/RtfHeadingRenderer.cs @@ -0,0 +1,24 @@ +#nullable enable + +using Markdig.Syntax; + +namespace Velopack.Packaging.Rtf; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfHeadingRenderer : RtfObjectRenderer +{ + protected override void Write(RtfRenderer renderer, HeadingBlock obj) + { + // Map heading levels to RTF font sizes (example: h1 = 32pt, h2 = 28pt, ...) + int[] fontSizes = [36, 30, 24, 20, 16, 14]; + int level = obj.Level - 1; + int fontSize = (level >= 0 && level < fontSizes.Length) ? fontSizes[level] : 12; + + renderer.Write($"\\line{{\\pard\\b\\fs{fontSize} "); // RTF font size is half-points + renderer.WriteLeafInline(obj); + renderer.Write(" \\par}\n"); + } +} \ No newline at end of file diff --git a/src/vpk/Velopack.Packaging/Rtf/RtfListRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/RtfListRenderer.cs new file mode 100644 index 00000000..8a1a59e4 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/RtfListRenderer.cs @@ -0,0 +1,37 @@ +#nullable enable + +using Markdig.Syntax; + +namespace Velopack.Packaging.Rtf; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfListRenderer : RtfObjectRenderer +{ + protected override void Write(RtfRenderer renderer, ListBlock listBlock) + { + renderer.EnsureLine(); + if (listBlock.IsOrdered) + { + renderer.Write("{\\pard "); + } + else + { + renderer.Write("{\\pard "); + } + foreach (var item in listBlock) + { + var listItem = (ListItemBlock)item; + var previousImplicit = renderer.ImplicitParagraph; + renderer.ImplicitParagraph = !listBlock.IsLoose; + renderer.EnsureLine(); + renderer.Write("\\bullet "); // RTF bullet character + renderer.WriteChildren(listItem); + renderer.Write(" \\par"); + renderer.ImplicitParagraph = previousImplicit; + } + renderer.Write("}\n"); + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/RtfObjectRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/RtfObjectRenderer.cs new file mode 100644 index 00000000..0e85618a --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/RtfObjectRenderer.cs @@ -0,0 +1,16 @@ +#nullable enable + +using Markdig.Renderers; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; + +namespace Velopack.Packaging.Rtf; + +/// +/// A base class for RTF rendering and Markdown objects. +/// +/// The type of the object. +/// +public abstract class RtfObjectRenderer : MarkdownObjectRenderer where TObject : MarkdownObject +{ +} diff --git a/src/vpk/Velopack.Packaging/Rtf/RtfParagraphRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/RtfParagraphRenderer.cs new file mode 100644 index 00000000..78079ca1 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/RtfParagraphRenderer.cs @@ -0,0 +1,42 @@ +#nullable enable + +using Markdig.Syntax; + +namespace Velopack.Packaging.Rtf; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfParagraphRenderer : RtfObjectRenderer +{ + protected override void Write(RtfRenderer renderer, ParagraphBlock obj) + { + if (!renderer.ImplicitParagraph) + { + renderer.Write("{\\pard "); // RTF paragraph start + } + renderer.WriteLeafInline(obj); + if (!renderer.ImplicitParagraph) + { + renderer.Write(" \\par}"); // RTF paragraph end + + // Only write \line if next block is not a heading + bool nextIsHeading = false; + if (obj.Parent is ContainerBlock parent) + { + int index = parent.IndexOf(obj); + if (index >= 0 && index + 1 < parent.Count) + { + var next = parent[index + 1]; + nextIsHeading = next is Markdig.Syntax.HeadingBlock; + } + } + if (!nextIsHeading) + { + renderer.Write("\\line "); + } + renderer.EnsureLine(); + } + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/RtfQuoteBlockRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/RtfQuoteBlockRenderer.cs new file mode 100644 index 00000000..642a45f7 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/RtfQuoteBlockRenderer.cs @@ -0,0 +1,24 @@ +#nullable enable + +using Markdig.Syntax; + +namespace Velopack.Packaging.Rtf; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfQuoteBlockRenderer : RtfObjectRenderer +{ + protected override void Write(RtfRenderer renderer, QuoteBlock obj) + { + renderer.EnsureLine(); + renderer.Write("{\\pard\\li720 "); // Indent for quote + var savedImplicitParagraph = renderer.ImplicitParagraph; + renderer.ImplicitParagraph = false; + renderer.WriteChildren(obj); + renderer.ImplicitParagraph = savedImplicitParagraph; + renderer.Write(" \\par}\n"); + renderer.EnsureLine(); + } +} diff --git a/src/vpk/Velopack.Packaging/Rtf/RtfRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/RtfRenderer.cs new file mode 100644 index 00000000..fa08d5e6 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/RtfRenderer.cs @@ -0,0 +1,157 @@ +#nullable enable + +using Markdig.Helpers; +using Markdig.Renderers; +using Markdig.Syntax; + +namespace Velopack.Packaging.Rtf; + +/// +/// Default RTF renderer for a Markdown object. +/// +/// +public class RtfRenderer : TextRendererBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The writer. + public RtfRenderer(TextWriter writer) : base(writer) + { + // Default block renderers + ObjectRenderers.Add(new RtfCodeBlockRenderer()); + ObjectRenderers.Add(new RtfListRenderer()); + ObjectRenderers.Add(new RtfHeadingRenderer()); + ObjectRenderers.Add(new RtfBlockRenderer()); + ObjectRenderers.Add(new RtfParagraphRenderer()); + ObjectRenderers.Add(new RtfQuoteBlockRenderer()); + ObjectRenderers.Add(new RtfThematicBreakRenderer()); + + // Inline renderers + ObjectRenderers.Add(new Inlines.RtfAutolinkInlineRenderer()); + ObjectRenderers.Add(new Inlines.RtfCodeInlineRenderer()); + ObjectRenderers.Add(new Inlines.RtfDelimiterInlineRenderer()); + ObjectRenderers.Add(new Inlines.RtfEmphasisInlineRenderer()); + ObjectRenderers.Add(new Inlines.RtfLineBreakInlineRenderer()); + ObjectRenderers.Add(new Inlines.RtfLinkInlineRenderer()); + ObjectRenderers.Add(new Inlines.RtfLiteralInlineRenderer()); + } + + public bool ImplicitParagraph { get; set; } + + /// + /// Writes the lines of a + /// + /// The leaf block. + /// if set to true write end of lines. + /// if set to true escape the content for RTF + /// Only escape minimal RTF chars + /// This instance + public RtfRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool escape, bool softEscape = false) + { + if (leafBlock is null) throw new ArgumentNullException(nameof(leafBlock)); + var slices = leafBlock.Lines.Lines; + if (slices is not null) { + for (int i = 0; i < slices.Length; i++) { + ref StringSlice slice = ref slices[i].Slice; + if (slice.Text is null) { + break; + } + + if (!writeEndOfLines && i > 0) { + WriteLine(); + } + + ReadOnlySpan span = slice.AsSpan(); + if (escape) { + WriteEscape(span, softEscape); + } else { + Write(span); + } + + if (writeEndOfLines) { + WriteLine(); + } + } + } + + return this; + } + + /// + /// Writes the content escaped for RTF, converting Unicode to RTF Unicode escapes. + /// + /// The string content. + /// If true, only escape minimal RTF chars. + public void WriteEscape(string? content, bool softEscape = false) + { + if (content == null) return; + for (int i = 0; i < content.Length; i++) { + char c = content[i]; + if (char.IsHighSurrogate(c) && i + 1 < content.Length && char.IsLowSurrogate(content[i + 1])) { + int codepoint = char.ConvertToUtf32(c, content[i + 1]); + Write($"\\u{codepoint}?"); // RTF spec: use '?' as fallback char + i++; // skip low surrogate + } else if (c == '\\') Write("\\\\"); + else if (c == '{') Write("\\{"); + else if (c == '}') Write("\\}"); + else if (c < 0x20 || (c >= 0x7F && c <= 0x9F)) { } // skip control characters + else if (c <= 0x7F) // ASCII + Write(c); + else // BMP Unicode + Write($"\\u{(int) c}{c}"); + } + } + + /// + /// Writes the content escaped for RTF, converting Unicode to RTF Unicode escapes. + /// + /// The character span. + /// If true, only escape minimal RTF chars. + public void WriteEscape(ReadOnlySpan span, bool softEscape = false) + { + for (int i = 0; i < span.Length; i++) { + char c = span[i]; + if (char.IsHighSurrogate(c) && i + 1 < span.Length && char.IsLowSurrogate(span[i + 1])) { + int codepoint = char.ConvertToUtf32(c, span[i + 1]); + Write($"\\u{codepoint}?"); // RTF spec: use '?' as fallback char + i++; // skip low surrogate + } else if (c == '\\') Write("\\\\"); + else if (c == '{') Write("\\{"); + else if (c == '}') Write("\\}"); + else if (c < 0x20 || (c >= 0x7F && c <= 0x9F)) { } // skip control characters + else if (c <= 0x7F) // ASCII + Write(c); + else // BMP Unicode + Write($"\\u{(int) c}{c}"); + } + } + + public void WriteEscape(ref Markdig.Helpers.StringSlice slice, bool softEscape = false) + { + WriteEscape(slice.AsSpan(), softEscape); + } + + public void WriteEscape(Markdig.Helpers.StringSlice slice, bool softEscape = false) + { + WriteEscape(slice.AsSpan(), softEscape); + } + + /// + /// Writes the RTF document start block. + /// + public void WriteRtfStart() + { + WriteLine(@"{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1031{\fonttbl{\f0\fnil\fcharset0 Calibri;}}"); + WriteLine(@"{\colortbl ;\red0\green0\blue0;}"); + WriteLine(@"\viewkind4\uc1\pard\sa200\sl276\slmult1\f0\fs19\lang7"); + } + + /// + /// Writes the RTF document end block. + /// + public void WriteRtfEnd() + { + WriteLine("}"); + } +} \ No newline at end of file diff --git a/src/vpk/Velopack.Packaging/Rtf/RtfThematicBreakRenderer.cs b/src/vpk/Velopack.Packaging/Rtf/RtfThematicBreakRenderer.cs new file mode 100644 index 00000000..dfb34675 --- /dev/null +++ b/src/vpk/Velopack.Packaging/Rtf/RtfThematicBreakRenderer.cs @@ -0,0 +1,17 @@ +#nullable enable + +using Markdig.Syntax; + +namespace Velopack.Packaging.Rtf; + +/// +/// An RTF renderer for a . +/// +/// +public class RtfThematicBreakRenderer : RtfObjectRenderer +{ + protected override void Write(RtfRenderer renderer, ThematicBreakBlock obj) + { + renderer.Write("{\\pard\\qr\\sl0\\slmult1\\line}\n"); // RTF horizontal rule (simulated with a line break) + } +}