有时候要把和llm的内容保存下来,md本身也可以存,但是一些内容上传或者分享给别人还是会用到pdf,本身就有个测试AI编程能力的工具站,就想着直接做个工具看看。
之前完全没有前端处理生成pdf的经验,然后就vibe coding了一下,gemini给了不少方案:
- html canvas(这个最后保存的不能选择,就是图片);
- markdownit+pdfmake(这个纯坑爹, gemini是直接把html直接写进去,然后发现是纯文本。。。用pdfmake需要自己做个AST,把markdown转成它的格式写入。。。放弃了)
最后头疼。。。想着到底怎么样保存pdf才能可以选择文本呢
然后发现。。沃日 直接用浏览器的打印不就好了
然后和qwen3 coder友好交流了好几个小时,终于捣鼓出来了。。。
https://merchmindai.net/zh/tools/markdown-to-pdf

然后点击转换直接打开一个新网页,弹出打印选项,我发现不同浏览器兼容性不一样,用edge打开的网页里是能显示html的,但是一些浏览器是空白(不影响打印)

还有就是一定要选择为"另存为PDF",不然默认的microsoft pdf那个一样是没法选中的

核心代码送上:
// Extract HTML generation logic
const generateHtmlContent = async (
markdownText: string,
css: string,
selectedTheme: "white" | "black",
title?: string
) => {
try {
// Dynamically import markdown-it and highlight.js for rendering
const [mdModule, hljsModule] = await Promise.all([
import("markdown-it"),
import("highlight.js"),
]);
const MarkdownIt = mdModule.default;
const hljs = hljsModule.default;
// Initialize markdown-it with highlight.js support
const md = new MarkdownIt({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value;
} catch (__) {}
}
return ""; // use external default escaping
},
});
const htmlContent = md.render(markdownText);
const themeClass =
selectedTheme === "white" ? "theme-white" : "theme-black";
// Create the HTML content
const html = document.createElement("html");
const head = document.createElement("head");
const body = document.createElement("body");
if (title) {
const titleElement = document.createElement("title");
titleElement.textContent = title;
head.appendChild(titleElement);
}
// Add theme-specific CSS
const themeStyle = document.createElement("style");
themeStyle.textContent = css;
head.appendChild(themeStyle);
// Add highlight.js CSS
const hljsLink = document.createElement("link");
hljsLink.rel = "stylesheet";
hljsLink.href =
selectedTheme === "white"
? "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css"
: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css";
head.appendChild(hljsLink);
body.className = themeClass;
const div = document.createElement("div");
div.className = "markdown-body";
div.innerHTML = htmlContent;
body.appendChild(div);
html.appendChild(head);
html.appendChild(body);
return html.outerHTML;
} catch (error) {
console.error("Error generating HTML content:", error);
throw error;
}
};
const handlePrint = async () => {
if (!markdown.trim()) {
toast({
title: t("pdf.toastErrorTitle"),
description: t("pdf.toastErrorDescription"),
variant: "destructive",
});
return;
}
setIsPrinting(true);
try {
// Create a new window for printing
const printWindow = window.open("", "_blank");
if (!printWindow) {
toast({
title: t("pdf.toastErrorTitle"),
description: t("pdf.toastPopupBlocked"),
variant: "destructive",
});
setIsPrinting(false);
return;
}
const htmlContent = await generateHtmlContent(
markdown,
customCss,
theme,
fileName
);
// Add print script
const printScript = `
<script>
window.onload = function() {
setTimeout(function() {
window.print();
}, 500);
};
</script>
`;
const finalHtml = htmlContent.replace("</body>", `${printScript}</body>`);
// Write to the print window
printWindow.document.open();
printWindow.document.write(finalHtml);
printWindow.document.close();
setIsPrinting(false);
} catch (error) {
console.error("Error preparing print content:", error);
toast({
title: t("pdf.toastErrorTitle"),
description: t("pdf.toastErrorDescription"),
variant: "destructive",
});
setIsPrinting(false);
}
};