🎨 Improve exporting document HTML (#16219)

* 🎨 The browser-side supports exporting document HTML

fix https://github.com/siyuan-note/siyuan/issues/16213

* 修复导出 HTML 时引入资源没有使用相对路径,修复文档导出 HTML/PDF 时缺失图标

fix https://github.com/siyuan-note/siyuan/issues/16217

fix https://github.com/siyuan-note/siyuan/issues/16216 01

* 修复文档导出 HTML/PDF 时冗余图标

fix https://github.com/siyuan-note/siyuan/issues/16216 02
This commit is contained in:
Jeffrey Chen 2025-10-28 09:28:56 +08:00 committed by GitHub
parent bca1f1eda6
commit 90a447f914
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 241 additions and 40 deletions

View file

@ -760,7 +760,7 @@ export const exportMd = (id: string) => {
icon: "iconPDF",
ignore: !isInAndroid() && !isInHarmony(),
click: () => {
const msId = showMessage(window.siyuan.languages.exporting);
const msgId = showMessage(window.siyuan.languages.exporting);
const localData = window.siyuan.storage[Constants.LOCAL_EXPORTPDF];
fetchPost("/api/export/exportPreviewHTML", {
id,
@ -775,10 +775,25 @@ export const exportMd = (id: string) => {
}
setTimeout(() => {
hideMessage(msId);
hideMessage(msgId);
}, 3000);
});
}
}, {
id: "exportHTML_SiYuan",
label: "HTML (SiYuan)",
iconClass: "ft__error",
icon: "iconHTML5",
click: () => {
saveExport({type: "html", id});
}
}, {
id: "exportHTML_Markdown",
label: "HTML (Markdown)",
icon: "iconHTML5",
click: () => {
saveExport({type: "htmlmd", id});
}
},
/// #endif
]

View file

@ -24,8 +24,43 @@ const getPluginStyle = async () => {
return css;
};
const getIconScript = (servePath: string) => {
const isBuiltInIcon = ["ant", "material"].includes(window.siyuan.config.appearance.icon);
const html = isBuiltInIcon ? "" : `<script src="${servePath}appearance/icons/material/icon.js?v=${Constants.SIYUAN_VERSION}"></script>`;
return html + `<script src="${servePath}appearance/icons/${window.siyuan.config.appearance.icon}/icon.js?v=${Constants.SIYUAN_VERSION}"></script>`;
}
export const saveExport = (option: IExportOptions) => {
/// #if !BROWSER
/// #if BROWSER
if (["html", "htmlmd"].includes(option.type)) {
const msgId = showMessage(window.siyuan.languages.exporting, -1);
// 浏览器环境:先调用 API 生成资源文件,再在前端生成完整的 HTML
const url = option.type === "htmlmd" ? "/api/export/exportMdHTML" : "/api/export/exportHTML";
fetchPost(url, {
id: option.id,
pdf: false,
removeAssets: false,
merge: true,
savePath: ""
}, async exportResponse => {
const html = await onExport(exportResponse, undefined, option);
fetchPost("/api/export/exportBrowserHTML", {
folder: exportResponse.data.folder,
html: html,
name: exportResponse.data.name
}, zipResponse => {
hideMessage(msgId);
if (zipResponse.code === -1) {
showMessage(window.siyuan.languages._kernel[14] + ": " + zipResponse.msg, 0, "error");
return;
}
window.open(zipResponse.data.zip);
showMessage(window.siyuan.languages.exported);
});
});
return;
}
/// #else
if (option.type === "pdf") {
if (window.siyuan.config.appearance.mode === 1) {
confirmDialog(window.siyuan.languages.pdfTip, window.siyuan.languages.pdfConfirm, () => {
@ -92,11 +127,12 @@ const getSnippetCSS = () => {
/// #if !BROWSER
const renderPDF = async (id: string) => {
const localData = window.siyuan.storage[Constants.LOCAL_EXPORTPDF];
const servePath = window.location.protocol + "//" + window.location.host;
const servePathWithoutTrailingSlash = window.location.protocol + "//" + window.location.host;
const servePath = servePathWithoutTrailingSlash + "/";
const isDefault = (window.siyuan.config.appearance.mode === 1 && window.siyuan.config.appearance.themeDark === "midnight") || (window.siyuan.config.appearance.mode === 0 && window.siyuan.config.appearance.themeLight === "daylight");
let themeStyle = "";
if (!isDefault) {
themeStyle = `<link rel="stylesheet" type="text/css" id="themeStyle" href="${servePath}/appearance/themes/${window.siyuan.config.appearance.themeLight}/theme.css?${Constants.SIYUAN_VERSION}"/>`;
themeStyle = `<link rel="stylesheet" type="text/css" id="themeStyle" href="${servePath}appearance/themes/${window.siyuan.config.appearance.themeLight}/theme.css?${Constants.SIYUAN_VERSION}"/>`;
}
const currentWindowId = await ipcRenderer.invoke(Constants.SIYUAN_GET, {
cmd: "getContentsId",
@ -105,15 +141,15 @@ const renderPDF = async (id: string) => {
const html = `<!DOCTYPE html>
<html lang="${window.siyuan.config.appearance.lang}" data-theme-mode="light" data-light-theme="${window.siyuan.config.appearance.themeLight}" data-dark-theme="${window.siyuan.config.appearance.themeDark}">
<head>
<base href="${servePath}/">
<base href="${servePath}">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="stylesheet" type="text/css" id="baseStyle" href="${servePath}/stage/build/export/base.css?v=${Constants.SIYUAN_VERSION}"/>
<link rel="stylesheet" type="text/css" id="themeDefaultStyle" href="${servePath}/appearance/themes/daylight/theme.css?v=${Constants.SIYUAN_VERSION}"/>
<script src="${servePath}/stage/protyle/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}"></script>
<link rel="stylesheet" type="text/css" id="baseStyle" href="${servePath}stage/build/export/base.css?v=${Constants.SIYUAN_VERSION}"/>
<link rel="stylesheet" type="text/css" id="themeDefaultStyle" href="${servePath}appearance/themes/daylight/theme.css?v=${Constants.SIYUAN_VERSION}"/>
<script src="${servePath}stage/protyle/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}"></script>
${themeStyle}
<title>${window.siyuan.languages.export} PDF</title>
<style>
@ -297,11 +333,11 @@ const renderPDF = async (id: string) => {
</div>
</div>
<div style="zoom:${localData.scale || 1}" id="preview">
<div class="fn__loading" style="left:0;height:100vh"><img width="48px" src="${servePath}/stage/loading-pure.svg"></div>
<div class="fn__loading" style="left:0;height:100vh"><img width="48px" src="${servePath}stage/loading-pure.svg"></div>
</div>
<script src="${servePath}/appearance/icons/${window.siyuan.config.appearance.icon}/icon.js?${Constants.SIYUAN_VERSION}"></script>
<script src="${servePath}/stage/build/export/protyle-method.js?${Constants.SIYUAN_VERSION}"></script>
<script src="${servePath}/stage/protyle/js/lute/lute.min.js?${Constants.SIYUAN_VERSION}"></script>
${getIconScript(servePath)}
<script src="${servePath}stage/build/export/protyle-method.js?${Constants.SIYUAN_VERSION}"></script>
<script src="${servePath}stage/protyle/js/lute/lute.min.js?${Constants.SIYUAN_VERSION}"></script>
<script>
const previewElement = document.getElementById('preview');
const fixBlockWidth = () => {
@ -339,7 +375,7 @@ const renderPDF = async (id: string) => {
item.parentElement.style.width = Math.min(item.parentElement.clientWidth, width) + "px";
item.removeAttribute('data-render');
})
Protyle.highlightRender(previewElement, "${servePath}/stage/protyle", document.querySelector("#scale").value);
Protyle.highlightRender(previewElement, "${servePath}stage/protyle", document.querySelector("#scale").value);
previewElement.querySelectorAll('[data-type="NodeMathBlock"]').forEach((item) => {
// 超级块内不能移除 width https://github.com/siyuan-note/siyuan/issues/14318
item.removeAttribute('data-render');
@ -347,7 +383,7 @@ const renderPDF = async (id: string) => {
previewElement.querySelectorAll('[data-type="NodeCodeBlock"][data-subtype="mermaid"] svg').forEach((item) => {
item.style.maxHeight = width * 1.414 + "px";
})
Protyle.mathRender(previewElement, "${servePath}/stage/protyle", true);
Protyle.mathRender(previewElement, "${servePath}stage/protyle", true);
previewElement.querySelectorAll("table").forEach(item => {
if (item.clientWidth > item.parentElement.clientWidth) {
item.style.zoom = (item.parentElement.clientWidth / item.clientWidth).toFixed(2) - 0.01;
@ -404,7 +440,7 @@ const renderPDF = async (id: string) => {
}, 300);
}
const fetchPost = (url, data, cb) => {
fetch("${servePath}" + url, {
fetch("${servePathWithoutTrailingSlash}" + url, {
method: "POST",
body: JSON.stringify(data)
}).then((response) => {
@ -426,14 +462,14 @@ const renderPDF = async (id: string) => {
item.insertAdjacentHTML("beforeend", "<hr style='margin:0;border:0'>");
}
})
Protyle.mermaidRender(wysElement, "${servePath}/stage/protyle");
Protyle.flowchartRender(wysElement, "${servePath}/stage/protyle");
Protyle.graphvizRender(wysElement, "${servePath}/stage/protyle");
Protyle.chartRender(wysElement, "${servePath}/stage/protyle");
Protyle.mindmapRender(wysElement, "${servePath}/stage/protyle");
Protyle.abcRender(wysElement, "${servePath}/stage/protyle");
Protyle.mermaidRender(wysElement, "${servePath}stage/protyle");
Protyle.flowchartRender(wysElement, "${servePath}stage/protyle");
Protyle.graphvizRender(wysElement, "${servePath}stage/protyle");
Protyle.chartRender(wysElement, "${servePath}stage/protyle");
Protyle.mindmapRender(wysElement, "${servePath}stage/protyle");
Protyle.abcRender(wysElement, "${servePath}stage/protyle");
Protyle.htmlRender(wysElement);
Protyle.plantumlRender(wysElement, "${servePath}/stage/protyle");
Protyle.plantumlRender(wysElement, "${servePath}stage/protyle");
}
fetchPost("/api/export/exportPreviewHTML", {
id: "${id}",
@ -495,7 +531,7 @@ const renderPDF = async (id: string) => {
});
const watermarkElement = actionElement.querySelector('#watermark');
const refreshPreview = () => {
previewElement.innerHTML = '<div class="fn__loading" style="left:0;height: 100vh"><img width="48px" src="${servePath}/stage/loading-pure.svg"></div>'
previewElement.innerHTML = '<div class="fn__loading" style="left:0;height: 100vh"><img width="48px" src="${servePath}stage/loading-pure.svg"></div>'
fetchPost("/api/export/exportPreviewHTML", {
id: "${id}",
keepFold: keepFoldElement.checked,
@ -664,7 +700,8 @@ export const onExport = async (data: IWebSocketData, filePath: string, exportOpt
mode = 1;
}
const isDefault = (window.siyuan.config.appearance.mode === 1 && window.siyuan.config.appearance.themeDark === "midnight") || (window.siyuan.config.appearance.mode === 0 && window.siyuan.config.appearance.themeLight === "daylight");
const servePath = window.location.protocol + "//" + window.location.host;
const isLocalExport = typeof filePath !== "undefined";
const servePath = isLocalExport ? "" : window.location.protocol + "//" + window.location.host + "/";
let themeStyle = "";
if (!isDefault) {
themeStyle = `<link rel="stylesheet" type="text/css" id="themeStyle" href="${servePath}appearance/themes/${themeName}/theme.css?${Constants.SIYUAN_VERSION}"/>`;
@ -674,15 +711,15 @@ export const onExport = async (data: IWebSocketData, filePath: string, exportOpt
const html = `<!DOCTYPE html>
<html lang="${window.siyuan.config.appearance.lang}" data-theme-mode="${getThemeMode()}" data-light-theme="${window.siyuan.config.appearance.themeLight}" data-dark-theme="${window.siyuan.config.appearance.themeDark}">
<head>
<base href="${servePath}/">
<base href="${servePath}">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="stylesheet" type="text/css" id="baseStyle" href="${servePath}/stage/build/export/base.css?v=${Constants.SIYUAN_VERSION}"/>
<link rel="stylesheet" type="text/css" id="themeDefaultStyle" href="${servePath}/appearance/themes/${themeName}/theme.css?v=${Constants.SIYUAN_VERSION}"/>
<script src="${servePath}/stage/protyle/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}"></script>
<link rel="stylesheet" type="text/css" id="baseStyle" href="${servePath}stage/build/export/base.css?v=${Constants.SIYUAN_VERSION}"/>
<link rel="stylesheet" type="text/css" id="themeDefaultStyle" href="${servePath}appearance/themes/${themeName}/theme.css?v=${Constants.SIYUAN_VERSION}"/>
<script src="${servePath}stage/protyle/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}"></script>
${themeStyle}
<title>${data.data.name}</title>
<!-- Exported by SiYuan v${Constants.SIYUAN_VERSION} -->
@ -696,9 +733,9 @@ export const onExport = async (data: IWebSocketData, filePath: string, exportOpt
<body>
<div class="${["htmlmd", "word"].includes(exportOption.type) ? "b3-typography" : "protyle-wysiwyg" + (window.siyuan.config.editor.displayBookmarkIcon ? " protyle-wysiwyg--attr" : "")}"
style="${isInAndroid() || isInHarmony() ? "margin: 0 16px;" : "max-width: 800px;margin: 0 auto;"}" id="preview">${data.data.content}</div>
<script src="${servePath}/appearance/icons/${window.siyuan.config.appearance.icon}/icon.js?v=${Constants.SIYUAN_VERSION}"></script>
<script src="${servePath}/stage/build/export/protyle-method.js?v=${Constants.SIYUAN_VERSION}"></script>
<script src="${servePath}/stage/protyle/js/lute/lute.min.js?v=${Constants.SIYUAN_VERSION}"></script>
${getIconScript(servePath)}
<script src="${servePath}stage/build/export/protyle-method.js?v=${Constants.SIYUAN_VERSION}"></script>
<script src="${servePath}stage/protyle/js/lute/lute.min.js?v=${Constants.SIYUAN_VERSION}"></script>
<script>
${minWidthHtml};
window.siyuan = {
@ -736,7 +773,7 @@ style="${isInAndroid() || isInHarmony() ? "margin: 0 16px;" : "max-width: 800px;
})
});
</script></body></html>`;
// 移动端导出 pdf
// 移动端导出 pdf、浏览器导出 HTML
if (typeof filePath === "undefined") {
return html;
}

View file

@ -198,7 +198,7 @@ export const initAssets = () => {
});
};
export const setInlineStyle = async (set = true, servePath = "../../..") => {
export const setInlineStyle = async (set = true, servePath = "../../../") => {
const height = Math.floor(window.siyuan.config.editor.fontSize * 1.625);
let style;
// Emojis Reset: 字体中包含了 emoji需重置
@ -206,7 +206,7 @@ export const setInlineStyle = async (set = true, servePath = "../../..") => {
if (isMac() || isIPad() || isIPhone()) {
style = `@font-face {
font-family: "Emojis Additional";
src: url(${servePath}/appearance/fonts/Noto-COLRv1-2.047/Noto-COLRv1.woff2) format("woff2");
src: url(${servePath}appearance/fonts/Noto-COLRv1-2.047/Noto-COLRv1.woff2) format("woff2");
unicode-range: U+1fae9, U+1fac6, U+1fabe, U+1fadc, U+e50a, U+1fa89, U+1fadf, U+1f1e6-1f1ff, U+1fa8f;
}
@font-face {
@ -232,7 +232,7 @@ export const setInlineStyle = async (set = true, servePath = "../../..") => {
if (isWin11Browser) {
style = `@font-face {
font-family: "Emojis Additional";
src: url(${servePath}/appearance/fonts/Noto-COLRv1-2.047/Noto-COLRv1.woff2) format("woff2");
src: url(${servePath}appearance/fonts/Noto-COLRv1-2.047/Noto-COLRv1.woff2) format("woff2");
unicode-range: U+1fae9, U+1fac6, U+1fabe, U+1fadc, U+e50a, U+1fa89, U+1fadf, U+1f1e6-1f1ff, U+1f3f4, U+e0067, U+e0062,
U+e0065, U+e006e, U+e007f, U+e0073, U+e0063, U+e0074, U+e0077, U+e006c;
size-adjust: 85%;
@ -254,7 +254,7 @@ export const setInlineStyle = async (set = true, servePath = "../../..") => {
} else {
style = `@font-face {
font-family: "Emojis Reset";
src: url(${servePath}/appearance/fonts/Noto-COLRv1-2.047/Noto-COLRv1.woff2) format("woff2");
src: url(${servePath}appearance/fonts/Noto-COLRv1-2.047/Noto-COLRv1.woff2) format("woff2");
unicode-range: U+1f170-1f171, U+1f17e, U+1f17f, U+1f21a, U+1f22f, U+1f232-1f23a, U+1f250, U+1f251, U+1f32b, U+1f3bc,
U+1f411, U+1f42d, U+1f42e, U+1f431, U+1f435, U+1f441, U+1f4a8, U+1f4ab, U+1f525, U+1f600-1f60d, U+1f60f-1f623,
U+1f625-1f62b, U+1f62d-1f63f, U+1F643, U+1F640, U+1f79, U+1f8f, U+1fa79, U+1fae4, U+1fae9, U+1fac6, U+1fabe, U+1fadf,
@ -267,7 +267,7 @@ export const setInlineStyle = async (set = true, servePath = "../../..") => {
}
@font-face {
font-family: "Emojis";
src: url(${servePath}/appearance/fonts/Noto-COLRv1-2.047/Noto-COLRv1.woff2) format("woff2"),
src: url(${servePath}appearance/fonts/Noto-COLRv1-2.047/Noto-COLRv1.woff2) format("woff2"),
local("Segoe UI Emoji"),
local("Segoe UI Symbol"),
local("Apple Color Emoji"),

View file

@ -20,6 +20,7 @@ import (
"io"
"mime"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
@ -30,6 +31,7 @@ import (
"github.com/88250/lute/parse"
"github.com/gin-gonic/gin"
"github.com/mssola/useragent"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
@ -489,6 +491,21 @@ func exportMdHTML(c *gin.Context) {
id := arg["id"].(string)
savePath := arg["savePath"].(string)
savePath = strings.TrimSpace(savePath)
if savePath == "" {
folderName := "htmlmd-" + id + "-" + util.CurrentTimeSecondsStr()
tmpDir := filepath.Join(util.TempDir, "export", folderName)
name, content := model.ExportMarkdownHTML(id, tmpDir, false, false)
ret.Data = map[string]interface{}{
"id": id,
"name": name,
"content": content,
"folder": folderName,
}
return
}
name, content := model.ExportMarkdownHTML(id, savePath, false, false)
ret.Data = map[string]interface{}{
"id": id,
@ -527,6 +544,62 @@ func exportTempContent(c *gin.Context) {
}
}
func exportBrowserHTML(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
folder := arg["folder"].(string)
htmlContent := arg["html"].(string)
name := arg["name"].(string)
tmpDir := filepath.Join(util.TempDir, "export", folder)
htmlPath := filepath.Join(tmpDir, "index.html")
if err := filelock.WriteFile(htmlPath, []byte(htmlContent)); err != nil {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = nil
return
}
zipFileName := util.FilterFileName(name) + ".zip"
zipPath := filepath.Join(util.TempDir, "export", zipFileName)
zip, err := gulu.Zip.Create(zipPath)
if err != nil {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = nil
return
}
err = zip.AddDirectory("", tmpDir, func(string) {})
if err != nil {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = nil
return
}
if err = zip.Close(); err != nil {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = nil
return
}
os.RemoveAll(tmpDir)
zipURL := "/export/" + url.PathEscape(filepath.Base(zipPath))
ret.Data = map[string]interface{}{
"zip": zipURL,
}
}
func exportPreviewHTML(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
@ -590,6 +663,21 @@ func exportHTML(c *gin.Context) {
if nil != arg["merge"] {
merge = arg["merge"].(bool)
}
savePath = strings.TrimSpace(savePath)
if savePath == "" {
folderName := "html-" + id + "-" + util.CurrentTimeSecondsStr()
tmpDir := filepath.Join(util.TempDir, "export", folderName)
name, content, _ := model.ExportHTML(id, tmpDir, pdf, false, keepFold, merge)
ret.Data = map[string]interface{}{
"id": id,
"name": name,
"content": content,
"folder": folderName,
}
return
}
name, content, _ := model.ExportHTML(id, savePath, pdf, false, keepFold, merge)
ret.Data = map[string]interface{}{
"id": id,

View file

@ -325,6 +325,7 @@ func ServeAPI(ginServer *gin.Engine) {
ginServer.Handle("POST", "/api/export/exportData", model.CheckAuth, model.CheckAdminRole, exportData)
ginServer.Handle("POST", "/api/export/exportDataInFolder", model.CheckAuth, model.CheckAdminRole, exportDataInFolder)
ginServer.Handle("POST", "/api/export/exportTempContent", model.CheckAuth, model.CheckAdminRole, exportTempContent)
ginServer.Handle("POST", "/api/export/exportBrowserHTML", model.CheckAuth, model.CheckAdminRole, exportBrowserHTML)
ginServer.Handle("POST", "/api/export/export2Liandi", model.CheckAuth, model.CheckAdminRole, model.CheckReadonly, export2Liandi)
ginServer.Handle("POST", "/api/export/exportReStructuredText", model.CheckAuth, model.CheckAdminRole, exportReStructuredText)
ginServer.Handle("POST", "/api/export/exportAsciiDoc", model.CheckAuth, model.CheckAdminRole, exportAsciiDoc)

View file

@ -766,7 +766,8 @@ func ExportMarkdownHTML(id, savePath string, docx, merge bool) (name, dom string
if 1 == Conf.Appearance.Mode {
theme = Conf.Appearance.ThemeDark
}
srcs = []string{"icons", "themes/" + theme}
// 复制主题文件夹
srcs = []string{"themes/" + theme}
appearancePath := util.AppearancePath
if util.IsSymlinkPath(util.AppearancePath) {
// Support for symlinked theme folder when exporting HTML https://github.com/siyuan-note/siyuan/issues/9173
@ -787,6 +788,35 @@ func ExportMarkdownHTML(id, savePath string, docx, merge bool) (name, dom string
}
}
// 只复制图标文件夹中的 icon.js 文件
iconName := Conf.Appearance.Icon
// 如果使用的不是内建图标ant 或 material需要复制 material 作为后备
if iconName != "ant" && iconName != "material" && iconName != "" {
srcIconFile := filepath.Join(appearancePath, "icons", "material", "icon.js")
toIconDir := filepath.Join(savePath, "appearance", "icons", "material")
if err := os.MkdirAll(toIconDir, 0755); err != nil {
logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err)
return
}
toIconFile := filepath.Join(toIconDir, "icon.js")
if err := filelock.Copy(srcIconFile, toIconFile); err != nil {
logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err)
}
}
// 复制当前使用的图标文件
if iconName != "" {
srcIconFile := filepath.Join(appearancePath, "icons", iconName, "icon.js")
toIconDir := filepath.Join(savePath, "appearance", "icons", iconName)
if err := os.MkdirAll(toIconDir, 0755); err != nil {
logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err)
return
}
toIconFile := filepath.Join(toIconDir, "icon.js")
if err := filelock.Copy(srcIconFile, toIconFile); err != nil {
logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err)
}
}
// 复制自定义表情图片
emojis := emojisInTree(tree)
for _, emoji := range emojis {
@ -930,7 +960,8 @@ func ExportHTML(id, savePath string, pdf, image, keepFold, merge bool) (name, do
if 1 == Conf.Appearance.Mode {
theme = Conf.Appearance.ThemeDark
}
srcs = []string{"icons", "themes/" + theme}
// 复制主题文件夹
srcs = []string{"themes/" + theme}
appearancePath := util.AppearancePath
if util.IsSymlinkPath(util.AppearancePath) {
// Support for symlinked theme folder when exporting HTML https://github.com/siyuan-note/siyuan/issues/9173
@ -949,6 +980,35 @@ func ExportHTML(id, savePath string, pdf, image, keepFold, merge bool) (name, do
}
}
// 只复制图标文件夹中的 icon.js 文件
iconName := Conf.Appearance.Icon
// 如果使用的不是内建图标ant 或 material需要复制 material 作为后备
if iconName != "ant" && iconName != "material" && iconName != "" {
srcIconFile := filepath.Join(appearancePath, "icons", "material", "icon.js")
toIconDir := filepath.Join(savePath, "appearance", "icons", "material")
if err := os.MkdirAll(toIconDir, 0755); err != nil {
logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err)
return
}
toIconFile := filepath.Join(toIconDir, "icon.js")
if err := filelock.Copy(srcIconFile, toIconFile); err != nil {
logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err)
}
}
// 复制当前使用的图标文件
if iconName != "" {
srcIconFile := filepath.Join(appearancePath, "icons", iconName, "icon.js")
toIconDir := filepath.Join(savePath, "appearance", "icons", iconName)
if err := os.MkdirAll(toIconDir, 0755); err != nil {
logging.LogErrorf("mkdir [%s] failed: %s", toIconDir, err)
return
}
toIconFile := filepath.Join(toIconDir, "icon.js")
if err := filelock.Copy(srcIconFile, toIconFile); err != nil {
logging.LogWarnf("copy icon file from [%s] to [%s] failed: %s", srcIconFile, toIconFile, err)
}
}
// 复制自定义表情图片
emojis := emojisInTree(tree)
for _, emoji := range emojis {