起因
这是网友经常问俺的问题。
其实俺也经常完成这样的功能。
回答
其实这个有很多种方案,要根据实际的需求来分析。有简单的html 复杂的html。
先从最简单的html开始
先看结果

其实这个AI生成的markdown的内容,俺把markdown转成了html进行显示,另外也转成了pdf 。这种简单的html 可以使用 iTextSharp 完成。
NuGet:
iTextSharp 5.5.13.1
itextsharp.xmlworker 5.5.13.1
调用很简单
MarkdownToHtmlConverter hh = new MarkdownToHtmlConverter();
string html = hh.Convert(AI_answer);
var converter = new HtmlToPdfConverter();
byte[] buff= converter.ConvertHtmlStringToPdf(styledHtml);
有点图的html
俺用Devexpress ,不仅可以生成pdf ,还可以生成pdf,类似word,但是对环境没有依赖。
也支撑
public static void Html2Docx(string htm, Stream stream)
{
byte[] bytes = Encoding.UTF8.GetBytes(htm);
RichEditDocumentServer server = new RichEditDocumentServer();
MemoryStream ms = new MemoryStream(bytes);
ms.Position = 0;
server.LoadDocument(ms, DocumentFormat.Html);
server.Document.Unit = DevExpress.Office.DocumentUnit.Millimeter;
server.Document.Sections[0].Margins.Left =10f;
server.Document.Sections[0].Margins.Right = 10f;
server.Document.Sections[0].Margins.Top = 10f;
server.Document.Sections[0].Margins.Bottom = 10f;
server.SaveDocument(stream, DocumentFormat.OpenXml);
server.Dispose();
ms.Dispose();
}
public static void Html2PDF(string htm, Stream stream)
{
byte[] bytes = Encoding.UTF8.GetBytes(htm);
RichEditDocumentServer server = new RichEditDocumentServer();
MemoryStream ms = new MemoryStream(bytes);
ms.Position = 0;
server.LoadDocument(ms, DocumentFormat.Html);
server.Document.Unit = DevExpress.Office.DocumentUnit.Millimeter;
server.Document.Sections[0].Margins.Left = 10f;
server.Document.Sections[0].Margins.Right = 10f;
server.Document.Sections[0].Margins.Top = 10f;
server.Document.Sections[0].Margins.Bottom = 10f;
server.ExportToPdf(stream);
server.Dispose();
ms.Dispose();
}
复杂的html
使用PuppeteerSharp
var page = await browser.NewPageAsync();
await page.SetContentAsync(sb.ToString());
var pdfOptions = new PdfOptions
{
Format = PuppeteerSharp.Media.PaperFormat.A4,
PrintBackground = true,
MarginOptions = new PuppeteerSharp.Media.MarginOptions
{
Top = "20px",
Bottom = "20px",
Left = "20px",
Right = "20px"
}
};
byte[] pdfBytes = await page.PdfDataAsync(pdfOptions);
System.IO.File.WriteAllBytes(fn, pdfBytes);
await page.DisposeAsync();
这里需要主要一点 ,如果是在web服务中使用,可以先手工下载好Chrome相关文件。
然后 手工指定目录,因为有时web服务中 会下载chorme失败。
browser = Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true, // 无头模式
ExecutablePath = System.IO.Path.Combine(webdir, "bin", "Chrome", "Win64-124.0.6367.201", "chrome-win64", "chrome.exe")
}).Result;
总结
以上3种都可以在Web服务和winform桌面上使用。
有了第3种为啥,还有使用前2种,这个主要是一个平衡的问题。第3种是比较强大,但是占用的计算资源也多。对付第1种那样的简单内容,就是杀鸡有牛刀了
代码
cs
public class MarkdownToHtmlConverter
{
public string Convert(string markdownText)
{
if (string.IsNullOrEmpty(markdownText))
return string.Empty;
try
{
string html = markdownText;
// 1. 处理无序列表(ul/li)核心逻辑
// 匹配以 "- " 或 "* " 开头的列表项,支持多行列表
html = ProcessUnorderedLists(html);
// 2. 基础 Markdown 语法转换(补充功能)
html = ConvertHeadings(html); // 标题(# ~ ######)
html = ConvertBold(html); // 粗体(**文本**)
html = ConvertItalic(html); // 斜体(*文本* 或 _文本_)
html = ConvertLineBreaks(html); // 换行(两个空格+回车 或 单独回车)
html = ConvertParagraphs(html); // 段落(空行分隔的文本块)
html = html.Replace("</li><br />", "</li>");
return html;
}
catch (Exception ex)
{
throw new InvalidOperationException("Markdown 转换 HTML 失败", ex);
}
}
private string ProcessUnorderedLists(string text)
{
// 正则匹配连续的无序列表项(以 -/* 开头,换行分隔)
var listRegex = new Regex(@"(^|\n)(-|\*) (.+?)(\n(?!(-|\*) ))", RegexOptions.Singleline | RegexOptions.Multiline);
// 先将连续列表项包裹成 ul,再替换单个 li
text = listRegex.Replace(text, match =>
{
string listContent = match.Value.Trim();
// 替换单个列表项为 li 标签
string liContent = Regex.Replace(listContent, @"(-|\*) (.+?)(\n|$)", "<li>$2</li>$3");
return $"{match.Groups[1].Value}<ul>{liContent}</ul>{Environment.NewLine}";
});
// 处理单行列表项(非连续列表)
text = Regex.Replace(text, @"(^|\n)(-|\*) (.+?)(\n|$)", "$1<ul><li>$3</li></ul>$4", RegexOptions.Multiline);
return text;
}
/// <summary>
/// 转换标题(# 到 ###### 对应 h1 到 h6)
/// </summary>
private string ConvertHeadings(string text)
{
for (int i = 6; i >= 1; i--)
{
string hash = new string('#', i);
text = Regex.Replace(text, $"^{hash} (.+?)$", $"<h{i}>$1</h{i}>", RegexOptions.Multiline);
}
return text;
}
/// <summary>
/// 转换粗体(**文本**)
/// </summary>
private string ConvertBold(string text)
{
return Regex.Replace(text, @"\*\*(.+?)\*\*", "<strong>$1</strong>");
}
/// <summary>
/// 转换斜体(*文本* 或 _文本_)
/// </summary>
private string ConvertItalic(string text)
{
text = Regex.Replace(text, @"\*(.+?)\*", "<em>$1</em>");
text = Regex.Replace(text, @"_(.+?_)_", "<em>$1</em>");
return text;
}
/// <summary>
/// 转换换行符
/// </summary>
private string ConvertLineBreaks(string text)
{
// 匹配两个空格+换行 或 单独换行
return Regex.Replace(text, @"(\s{2}|\n)", "<br />");
}
/// <summary>
/// 转换段落(空行分隔的文本块)
/// </summary>
private string ConvertParagraphs(string text)
{
return Regex.Replace(text, @"^(?!<h|<ul|<li|<strong|<em|<br)(.+?)$", "<p>$1</p>", RegexOptions.Multiline);
}
}