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)
+ }
+}