Skip to content

Commit 5a7564c

Browse files
Improve Markdown PDF visual rendering (#1984)
* Improve Markdown PDF visual rendering * Address Markdown PDF chart review feedback * Address follow-up Markdown PDF chart feedback * Keep Markdown PDF panel rendering visual-safe * Preserve Chart.js point data in Markdown PDF charts * Harden Markdown PDF visual fallbacks * Harden Markdown PDF chart data alignment * Harden Markdown PDF drawable chart fallbacks * Harden Markdown PDF scatter chart validation * Honor Chart.js visibility in Markdown PDF charts * Honor Chart.js title and mixed data semantics * Handle stacked chart rendering edge cases * Preserve Chart.js dataset semantics * Unify Markdown visual themes across exports * Handle unsupported Chart.js chart features * Address Markdown visual review feedback * Address Markdown visual theme review feedback * Polish markdown visual theme review feedback * Address Markdown chart review issues * Fix markdown visual theme review gaps * Fix delayed markdown chart review gaps * Address latest markdown PDF review feedback * Address shared theme review feedback * Reject unsupported Chart.js PDF semantics
1 parent 131b98e commit 5a7564c

47 files changed

Lines changed: 8661 additions & 330 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

OfficeIMO.Examples/Markdown/DomainDetective.Report.Markdown.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ static string Slug(string text) {
241241
BackToTopText = "Back to top",
242242
ThemeToggle = true
243243
};
244-
md.SaveHtml(htmlPath, htmlOptions);
244+
md.SaveAsHtml(htmlPath, htmlOptions);
245245

246246
// Optional sidebar variant (left, outline chrome) for comparison
247247
// Variants to showcase options
@@ -252,7 +252,7 @@ void Variant(string suffix, System.Action<TocOptions> configureToc, System.Actio
252252
var h = new HtmlOptions { Kind = HtmlKind.Document, Title = $"Domain Detective — {suffix}", Style = HtmlStyle.Word, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true };
253253
configureHtml?.Invoke(h);
254254
var path = Path.Combine(mdFolder, $"DomainDetective.Report.{suffix.Replace(' ', '.').Replace('/', '-')}.html");
255-
v.SaveHtml(path, h);
255+
v.SaveAsHtml(path, h);
256256
Console.WriteLine($"✓ Variant saved: {path}");
257257
}
258258

@@ -275,7 +275,7 @@ void Variant(string suffix, System.Action<TocOptions> configureToc, System.Actio
275275
vMd.Add(b);
276276
}
277277
var htmlPathCollapsible = Path.Combine(mdFolder, "DomainDetective.Report.Top.Collapsible.html");
278-
vMd.SaveHtml(htmlPathCollapsible, new HtmlOptions { Kind = HtmlKind.Document, Title = "Domain Detective — Top Collapsible", Style = HtmlStyle.Word, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true });
278+
vMd.SaveAsHtml(htmlPathCollapsible, new HtmlOptions { Kind = HtmlKind.Document, Title = "Domain Detective — Top Collapsible", Style = HtmlStyle.Word, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true });
279279

280280
Console.WriteLine($"✓ Markdown saved: {mdPath}");
281281
Console.WriteLine($"✓ HTML saved: {htmlPath}");

OfficeIMO.Examples/Markdown/Markdown03_Html_FragmentsAndStyles.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public static void Example(string folderPath, bool open = false) {
3232

3333
// Full document with external CSS sidecar
3434
var externalPath = Path.Combine(outDir, "Sample.Document.External.html");
35-
doc.SaveHtml(externalPath, new HtmlOptions { Title = "Sample External", Style = HtmlStyle.Clean, CssDelivery = CssDelivery.ExternalFile });
35+
doc.SaveAsHtml(externalPath, new HtmlOptions { Title = "Sample External", Style = HtmlStyle.Clean, CssDelivery = CssDelivery.ExternalFile });
3636
Console.WriteLine($"✓ HTML document (external CSS): {externalPath}");
3737

3838
// Online mode referencing a CDN CSS, and Offline mode that downloads and inlines it

OfficeIMO.Examples/Markdown/Markdown04_TocLayoutsAndThemes.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,8 @@ public static void Example_Toc_ScrollSpy_Long_IndigoTheme(string folderPath, boo
8181
}
8282
}
8383
File.WriteAllText(mdPath, md.ToMarkdown(), Encoding.UTF8);
84-
var indigo = new ThemeColors {
85-
AccentLight = "#4f46e5", AccentDark = "#8b9cfb",
86-
HeadingLight = "#111827", HeadingDark = "#e5e7eb",
87-
TocBgLight = "#eef2ff", TocBorderLight = "#c7d2fe",
88-
TocBgDark = "#1f2937", TocBorderDark = "#374151",
89-
ActiveLinkLight = "#4338ca", ActiveLinkDark = "#a5b4fc"
90-
};
91-
var html = md.ToHtmlDocument(new HtmlOptions { Title = "ScrollSpy Indigo", Style = HtmlStyle.Word, Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, Theme = indigo });
84+
var indigo = MarkdownVisualTheme.WordLike().WithColorScheme(MarkdownColorSchemeKind.Indigo);
85+
var html = md.ToHtmlDocument(new HtmlOptions { Title = "ScrollSpy Indigo", Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, VisualTheme = indigo });
9286
File.WriteAllText(htmlPath, html, Encoding.UTF8);
9387
Console.WriteLine($"✓ HTML: {htmlPath}");
9488
}

OfficeIMO.Examples/Markdown/Markdown05_ThemesGallery.cs

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -56,53 +56,29 @@ public static void Example_Themes(string folderPath, bool open) {
5656
File.WriteAllText(md4, baseDoc.ToMarkdown(), Encoding.UTF8);
5757
File.WriteAllText(html4, baseDoc.ToHtmlDocument(new HtmlOptions { Title = "Theme: GitHub Auto", Style = HtmlStyle.GithubAuto, Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true }), Encoding.UTF8);
5858

59-
// Custom Indigo accents (toggle enabled)
59+
// Shared Indigo accents (toggle enabled)
6060
var (md5, html5) = Paths(folderPath, "Markdown_Theme_Indigo");
61-
var indigo = new ThemeColors {
62-
AccentLight = "#4f46e5", AccentDark = "#8b9cfb",
63-
HeadingLight = "#111827", HeadingDark = "#e5e7eb",
64-
TocBgLight = "#eef2ff", TocBorderLight = "#c7d2fe",
65-
TocBgDark = "#1f2937", TocBorderDark = "#374151",
66-
ActiveLinkLight = "#4338ca", ActiveLinkDark = "#a5b4fc"
67-
};
61+
var indigo = MarkdownVisualTheme.Report().WithColorScheme(MarkdownColorSchemeKind.Indigo);
6862
File.WriteAllText(md5, baseDoc.ToMarkdown(), Encoding.UTF8);
69-
File.WriteAllText(html5, baseDoc.ToHtmlDocument(new HtmlOptions { Title = "Theme: Indigo", Style = HtmlStyle.GithubAuto, Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, Theme = indigo }), Encoding.UTF8);
63+
File.WriteAllText(html5, baseDoc.ToHtmlDocument(new HtmlOptions { Title = "Theme: Indigo", Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, VisualTheme = indigo }), Encoding.UTF8);
7064

71-
// Custom Blue accents
65+
// Shared Blue accents
7266
var (md6, html6) = Paths(folderPath, "Markdown_Theme_Blue");
73-
var blue = new ThemeColors {
74-
AccentLight = "#1d4ed8", AccentDark = "#60a5fa",
75-
HeadingLight = "#0f172a", HeadingDark = "#e5e7eb",
76-
TocBgLight = "#eff6ff", TocBorderLight = "#bfdbfe",
77-
TocBgDark = "#111827", TocBorderDark = "#1f2937",
78-
ActiveLinkLight = "#1e40af", ActiveLinkDark = "#93c5fd"
79-
};
67+
var blue = MarkdownVisualTheme.Report().WithColorScheme(MarkdownColorSchemeKind.Blue);
8068
File.WriteAllText(md6, baseDoc.ToMarkdown(), Encoding.UTF8);
81-
File.WriteAllText(html6, baseDoc.ToHtmlDocument(new HtmlOptions { Title = "Theme: Blue", Style = HtmlStyle.GithubAuto, Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, Theme = blue }), Encoding.UTF8);
69+
File.WriteAllText(html6, baseDoc.ToHtmlDocument(new HtmlOptions { Title = "Theme: Blue", Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, VisualTheme = blue }), Encoding.UTF8);
8270

83-
// Custom Emerald accents
71+
// Shared Emerald accents
8472
var (md7, html7) = Paths(folderPath, "Markdown_Theme_Emerald");
85-
var emerald = new ThemeColors {
86-
AccentLight = "#059669", AccentDark = "#34d399",
87-
HeadingLight = "#064e3b", HeadingDark = "#d1fae5",
88-
TocBgLight = "#ecfdf5", TocBorderLight = "#a7f3d0",
89-
TocBgDark = "#052e23", TocBorderDark = "#065f46",
90-
ActiveLinkLight = "#047857", ActiveLinkDark = "#6ee7b7"
91-
};
73+
var emerald = MarkdownVisualTheme.Report().WithColorScheme(MarkdownColorSchemeKind.Emerald);
9274
File.WriteAllText(md7, baseDoc.ToMarkdown(), Encoding.UTF8);
93-
File.WriteAllText(html7, baseDoc.ToHtmlDocument(new HtmlOptions { Title = "Theme: Emerald", Style = HtmlStyle.GithubAuto, Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, Theme = emerald }), Encoding.UTF8);
75+
File.WriteAllText(html7, baseDoc.ToHtmlDocument(new HtmlOptions { Title = "Theme: Emerald", Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, VisualTheme = emerald }), Encoding.UTF8);
9476

95-
// Custom Rose accents
77+
// Shared Rose accents
9678
var (md8, html8) = Paths(folderPath, "Markdown_Theme_Rose");
97-
var rose = new ThemeColors {
98-
AccentLight = "#e11d48", AccentDark = "#fb7185",
99-
HeadingLight = "#111827", HeadingDark = "#fde2e2",
100-
TocBgLight = "#fff1f2", TocBorderLight = "#fecdd3",
101-
TocBgDark = "#1f2937", TocBorderDark = "#4b5563",
102-
ActiveLinkLight = "#be123c", ActiveLinkDark = "#fda4af"
103-
};
79+
var rose = MarkdownVisualTheme.Report().WithColorScheme(MarkdownColorSchemeKind.Rose);
10480
File.WriteAllText(md8, baseDoc.ToMarkdown(), Encoding.UTF8);
105-
File.WriteAllText(html8, baseDoc.ToHtmlDocument(new HtmlOptions { Title = "Theme: Rose", Style = HtmlStyle.GithubAuto, Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, Theme = rose }), Encoding.UTF8);
81+
File.WriteAllText(html8, baseDoc.ToHtmlDocument(new HtmlOptions { Title = "Theme: Rose", Kind = HtmlKind.Document, ThemeToggle = true, IncludeAnchorLinks = true, BackToTopLinks = true, VisualTheme = rose }), Encoding.UTF8);
10682

10783
Console.WriteLine($"✓ HTML (Clean): {html1}");
10884
Console.WriteLine($"✓ HTML (Word): {html1b}");
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using System;
2+
using System.IO;
3+
using OfficeIMO.Markdown;
4+
using OfficeIMO.Markdown.Pdf;
5+
using OfficeIMO.Word.Markdown;
6+
7+
namespace OfficeIMO.Examples.Markdown {
8+
internal static class Markdown10_VisualThemesAcrossFormats {
9+
public static void Example_SharedVisualTheme(string folderPath, bool openWord) {
10+
Console.WriteLine("[*] Markdown: Shared visual theme across HTML, PDF, and Word");
11+
string mdFolder = Path.Combine(folderPath, "Markdown");
12+
Directory.CreateDirectory(mdFolder);
13+
14+
MarkdownVisualTheme theme = MarkdownVisualTheme.Report()
15+
.WithColorScheme(MarkdownColorSchemeKind.Emerald)
16+
.WithColors(accent: "SeaGreen", heading: "#064e3b")
17+
.WithTable(table => {
18+
table.BorderWidth = 0.9;
19+
table.CellPaddingX = 8;
20+
table.CellPaddingY = 6;
21+
table.UseRowStripes = true;
22+
});
23+
24+
MarkdownDoc doc = MarkdownDoc.Create()
25+
.FrontMatter(new { title = "Shared Theme Report", theme = "report", scheme = "emerald" })
26+
.H1("Shared Theme Report")
27+
.Toc(o => {
28+
o.MinLevel = 2;
29+
o.MaxLevel = 3;
30+
o.Layout = TocLayout.Panel;
31+
o.Title = "Contents";
32+
}, placeAtTop: true)
33+
.P("One Markdown AST can now drive HTML, PDF, and Word with a single shared visual theme.")
34+
.H2("Evidence")
35+
.Ul(ul => ul
36+
.Item("Headings use the same accent family.")
37+
.Item("Tables share borders, header colors, padding, and row stripes.")
38+
.Item("Code, quotes, callouts, and links follow the same palette."))
39+
.Table(t => t
40+
.Headers("Surface", "Save API", "Theme source")
41+
.Row("Markdown", "SaveAsMarkdown", "semantic source")
42+
.Row("HTML", "SaveAsHtml", "HtmlOptions.VisualTheme")
43+
.Row("PDF", "SaveAsPdf", "MarkdownPdfSaveOptions.Theme")
44+
.Row("Word", "ToWordDocument", "MarkdownToWordOptions.Theme"))
45+
.Callout("success", "Consistent visuals", "The same theme object controls the conversion-specific renderer details.")
46+
.H2("Code")
47+
.Code("csharp", """
48+
var theme = MarkdownVisualTheme.Report()
49+
.WithColorScheme(MarkdownColorSchemeKind.Emerald)
50+
.WithColors(accent: "SeaGreen", heading: "#064e3b")
51+
.WithTable(table => table.BorderWidth = 0.9);
52+
53+
doc.SaveAsHtml(htmlPath, new HtmlOptions { VisualTheme = theme });
54+
doc.SaveAsPdf(pdfPath, new MarkdownPdfSaveOptions { Theme = theme });
55+
using var word = doc.ToWordDocument(new MarkdownToWordOptions { Theme = theme });
56+
word.SaveAs(docxPath);
57+
""");
58+
59+
string mdPath = Path.Combine(mdFolder, "Markdown_SharedVisualTheme.md");
60+
string htmlPath = Path.Combine(mdFolder, "Markdown_SharedVisualTheme.html");
61+
string pdfPath = Path.Combine(mdFolder, "Markdown_SharedVisualTheme.pdf");
62+
string docxPath = Path.Combine(mdFolder, "Markdown_SharedVisualTheme.docx");
63+
64+
File.WriteAllText(mdPath, doc.ToMarkdown(), Encoding.UTF8);
65+
doc.SaveAsHtml(htmlPath, new HtmlOptions {
66+
Title = "Shared Theme Report",
67+
Kind = HtmlKind.Document,
68+
VisualTheme = theme,
69+
ThemeToggle = true,
70+
IncludeAnchorLinks = true,
71+
BackToTopLinks = true
72+
});
73+
doc.SaveAsPdf(pdfPath, new MarkdownPdfSaveOptions {
74+
Theme = theme
75+
});
76+
77+
using var word = doc.ToWordDocument(new MarkdownToWordOptions {
78+
Theme = theme,
79+
FontFamily = "Aptos"
80+
});
81+
word.SaveAs(docxPath, openWord);
82+
83+
Console.WriteLine($"✓ Markdown: {mdPath}");
84+
Console.WriteLine($"✓ HTML: {htmlPath}");
85+
Console.WriteLine($"✓ PDF: {pdfPath}");
86+
Console.WriteLine($"✓ Word: {docxPath}");
87+
}
88+
}
89+
}

OfficeIMO.Examples/OfficeIMO.Examples.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<ProjectReference Include="..\\OfficeIMO.GoogleWorkspace\\OfficeIMO.GoogleWorkspace.csproj" />
1717
<ProjectReference Include="..\\OfficeIMO.Pdf\\OfficeIMO.Pdf.csproj" />
1818
<ProjectReference Include="..\\OfficeIMO.Markdown\\OfficeIMO.Markdown.csproj" />
19+
<ProjectReference Include="..\\OfficeIMO.Markdown.Pdf\\OfficeIMO.Markdown.Pdf.csproj" />
1920
<ProjectReference Include="..\\OfficeIMO.Word\\OfficeIMO.Word.csproj" />
2021
<ProjectReference Include="..\\OfficeIMO.Word.GoogleDocs\\OfficeIMO.Word.GoogleDocs.csproj" />
2122
<ProjectReference Include="..\\OfficeIMO.Word.Pdf\\OfficeIMO.Word.Pdf.csproj" />

OfficeIMO.Examples/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ static void Main(string[] args) {
357357
// Markdown.Markdown04_TocLayoutsAndThemes.Example_Toc_ScrollSpy_Long_IndigoTheme(folderPath, false);
358358
// // Markdown: Built-in HTML style gallery
359359
// Markdown.Markdown05_ThemesGallery.Example_Themes(folderPath, false);
360+
// // Markdown: One shared visual theme across Markdown, HTML, PDF, and Word
361+
// Markdown.Markdown10_VisualThemesAcrossFormats.Example_SharedVisualTheme(folderPath, false);
360362
// // Markdown: Custom parser/AST/HTML extensions
361363
// Markdown.Markdown07_Custom_Extensions.Example_Custom_Extensions(folderPath, false);
362364
// // Markdown: Delegate-based custom block parser extensions

OfficeIMO.Markdown.Pdf/MarkdownPdfConverterExtensions.Blocks.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ private static void RenderBlock(PdfCore.PdfDocument pdf, IMarkdownBlock block, M
1414
RenderHeading(pdf, heading, document, visualTheme);
1515
break;
1616
case ParagraphBlock paragraph:
17-
RenderParagraph(pdf, paragraph.Inlines, visualTheme);
17+
RenderParagraph(pdf, paragraph.Inlines, options, visualTheme);
1818
break;
1919
case OrderedListBlock ordered:
2020
RenderOrderedList(pdf, ordered, document, options, visualTheme);
@@ -29,7 +29,7 @@ private static void RenderBlock(PdfCore.PdfDocument pdf, IMarkdownBlock block, M
2929
RenderCodeBlock(pdf, code, visualTheme);
3030
break;
3131
case SemanticFencedBlock semantic:
32-
RenderSemanticFencedBlock(pdf, semantic, visualTheme);
32+
RenderSemanticFencedBlock(pdf, semantic, options, visualTheme);
3333
break;
3434
case CalloutBlock callout:
3535
RenderCalloutBlock(pdf, callout, document, options, visualTheme);
@@ -53,7 +53,7 @@ private static void RenderBlock(PdfCore.PdfDocument pdf, IMarkdownBlock block, M
5353
pdf.HR();
5454
break;
5555
case ImageBlock image:
56-
RenderImageBlock(pdf, image, options);
56+
RenderImageBlock(pdf, image, options, visualTheme);
5757
break;
5858
case FrontMatterBlock frontMatter:
5959
RenderFrontMatter(pdf, frontMatter, document, options, visualTheme);
@@ -91,14 +91,27 @@ private static void RenderHeading(PdfCore.PdfDocument pdf, HeadingBlock heading,
9191
pdf.H3(text);
9292
} else {
9393
double fontSize = heading.Level == 4 ? 13D : 11.5D;
94+
PdfCore.PdfColor headingColor = ResolveManualHeadingColor(visualTheme);
9495
pdf.Paragraph(builder => {
9596
builder.Bold(true).FontSize(fontSize);
96-
AppendInlines(builder, heading.Inlines, CreateInlineStyle(visualTheme).With(bold: true, fontSize: fontSize));
97+
AppendInlines(builder, heading.Inlines, CreateInlineStyle(visualTheme).With(bold: true, color: headingColor, fontSize: fontSize));
9798
}, style: new PdfCore.PdfParagraphStyle { SpacingBefore = 8, SpacingAfter = 4, KeepWithNext = true });
9899
}
99100
}
100101

101-
private static void RenderParagraph(PdfCore.PdfDocument pdf, InlineSequence inlines, MarkdownPdfVisualTheme visualTheme) {
102+
private static PdfCore.PdfColor ResolveManualHeadingColor(MarkdownPdfVisualTheme visualTheme) {
103+
PdfCore.PdfHeadingStyles? headingStyles = visualTheme.DocumentThemeSnapshot?.HeadingStyles;
104+
return headingStyles?.Level3?.Color ??
105+
headingStyles?.Level2?.Color ??
106+
headingStyles?.Level1?.Color ??
107+
visualTheme.DocumentHeaderTitleColorSnapshot;
108+
}
109+
110+
private static void RenderParagraph(PdfCore.PdfDocument pdf, InlineSequence inlines, MarkdownPdfSaveOptions options, MarkdownPdfVisualTheme visualTheme) {
111+
if (TryRenderImageOnlyParagraph(pdf, inlines, options, visualTheme)) {
112+
return;
113+
}
114+
102115
if (IsEmpty(inlines)) {
103116
return;
104117
}
@@ -243,7 +256,7 @@ private static void RenderListChildren(PdfCore.PdfDocument pdf, IReadOnlyList<Li
243256
for (int i = 0; i < items.Count; i++) {
244257
ListItem item = items[i];
245258
for (int paragraphIndex = 0; paragraphIndex < item.AdditionalParagraphs.Count; paragraphIndex++) {
246-
RenderParagraph(pdf, item.AdditionalParagraphs[paragraphIndex], visualTheme);
259+
RenderParagraph(pdf, item.AdditionalParagraphs[paragraphIndex], options, visualTheme);
247260
}
248261

249262
for (int childIndex = 0; childIndex < item.ChildBlocks.Count; childIndex++) {

0 commit comments

Comments
 (0)