A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 783 lines 37 kB view raw
1import {hideMessage, showMessage} from "../../dialog/message"; 2import {Constants} from "../../constants"; 3/// #if !BROWSER 4import {ipcRenderer} from "electron"; 5import * as fs from "fs"; 6import * as path from "path"; 7import {afterExport} from "./util"; 8/// #endif 9import {confirmDialog} from "../../dialog/confirmDialog"; 10import {getThemeMode, setInlineStyle} from "../../util/assets"; 11import {fetchPost, fetchSyncPost} from "../../util/fetch"; 12import {Dialog} from "../../dialog"; 13import {replaceLocalPath} from "../../editor/rename"; 14import {getScreenWidth, isInAndroid, isInHarmony, setStorageVal} from "../util/compatibility"; 15import {getFrontend} from "../../util/functions"; 16 17const getPluginStyle = async () => { 18 const response = await fetchSyncPost("/api/petal/loadPetals", {frontend: getFrontend()}); 19 let css = ""; 20 // 为加快启动速度,不进行 await 21 response.data.forEach((item: IPluginData) => { 22 css += item.css || ""; 23 }); 24 return css; 25}; 26 27const getIconScript = (servePath: string) => { 28 const isBuiltInIcon = ["ant", "material"].includes(window.siyuan.config.appearance.icon); 29 const html = isBuiltInIcon ? "" : `<script src="${servePath}appearance/icons/material/icon.js?v=${Constants.SIYUAN_VERSION}"></script>`; 30 return html + `<script src="${servePath}appearance/icons/${window.siyuan.config.appearance.icon}/icon.js?v=${Constants.SIYUAN_VERSION}"></script>`; 31}; 32 33export const saveExport = (option: IExportOptions) => { 34 /// #if BROWSER 35 if (["html", "htmlmd"].includes(option.type)) { 36 const msgId = showMessage(window.siyuan.languages.exporting, -1); 37 // 浏览器环境:先调用 API 生成资源文件,再在前端生成完整的 HTML 38 const url = option.type === "htmlmd" ? "/api/export/exportMdHTML" : "/api/export/exportHTML"; 39 fetchPost(url, { 40 id: option.id, 41 pdf: false, 42 removeAssets: false, 43 merge: true, 44 savePath: "" 45 }, async exportResponse => { 46 const html = await onExport(exportResponse, undefined, "", option); 47 fetchPost("/api/export/exportBrowserHTML", { 48 folder: exportResponse.data.folder, 49 html: html, 50 name: exportResponse.data.name 51 }, zipResponse => { 52 hideMessage(msgId); 53 if (zipResponse.code === -1) { 54 showMessage(window.siyuan.languages._kernel[14] + ": " + zipResponse.msg, 0, "error"); 55 return; 56 } 57 window.open(zipResponse.data.zip); 58 showMessage(window.siyuan.languages.exported); 59 }); 60 }); 61 return; 62 } 63 /// #else 64 if (option.type === "pdf") { 65 if (window.siyuan.config.appearance.mode === 1) { 66 confirmDialog(window.siyuan.languages.pdfTip, window.siyuan.languages.pdfConfirm, () => { 67 renderPDF(option.id); 68 }); 69 } else { 70 renderPDF(option.id); 71 } 72 } else if (option.type === "word") { 73 const localData = window.siyuan.storage[Constants.LOCAL_EXPORTWORD]; 74 const wordDialog = new Dialog({ 75 title: "Word " + window.siyuan.languages.config, 76 content: `<div class="b3-dialog__content"> 77 <label class="fn__flex b3-label"> 78 <div class="fn__flex-1"> 79 ${window.siyuan.languages.removeAssetsFolder} 80 </div> 81 <span class="fn__space"></span> 82 <input id="removeAssets" class="b3-switch" type="checkbox" ${localData.removeAssets ? "checked" : ""}> 83 </label> 84 <label class="fn__flex b3-label"> 85 <div class="fn__flex-1"> 86 ${window.siyuan.languages.mergeSubdocs} 87 </div> 88 <span class="fn__space"></span> 89 <input id="mergeSubdocs" class="b3-switch" type="checkbox" ${localData.mergeSubdocs ? "checked" : ""}> 90 </label> 91</div> 92<div class="b3-dialog__action"> 93 <button class="b3-button b3-button--cancel">${window.siyuan.languages.cancel}</button><div class="fn__space"></div> 94 <button class="b3-button b3-button--text">${window.siyuan.languages.confirm}</button> 95</div>`, 96 width: "520px", 97 }); 98 wordDialog.element.setAttribute("data-key", Constants.DIALOG_EXPORTWORD); 99 const btnsElement = wordDialog.element.querySelectorAll(".b3-button"); 100 btnsElement[0].addEventListener("click", () => { 101 wordDialog.destroy(); 102 }); 103 btnsElement[1].addEventListener("click", () => { 104 const removeAssets = (wordDialog.element.querySelector("#removeAssets") as HTMLInputElement).checked; 105 const mergeSubdocs = (wordDialog.element.querySelector("#mergeSubdocs") as HTMLInputElement).checked; 106 window.siyuan.storage[Constants.LOCAL_EXPORTWORD] = {removeAssets, mergeSubdocs}; 107 setStorageVal(Constants.LOCAL_EXPORTWORD, window.siyuan.storage[Constants.LOCAL_EXPORTWORD]); 108 getExportPath(option, removeAssets, mergeSubdocs); 109 wordDialog.destroy(); 110 }); 111 } else { 112 getExportPath(option, false, true); 113 } 114 /// #endif 115}; 116 117const getSnippetCSS = () => { 118 let snippetCSS = ""; 119 document.querySelectorAll("style").forEach((item) => { 120 if (item.id.startsWith("snippet")) { 121 snippetCSS += item.outerHTML; 122 } 123 }); 124 return snippetCSS; 125}; 126 127/// #if !BROWSER 128const renderPDF = async (id: string) => { 129 const localData = window.siyuan.storage[Constants.LOCAL_EXPORTPDF]; 130 const servePathWithoutTrailingSlash = window.location.protocol + "//" + window.location.host; 131 const servePath = servePathWithoutTrailingSlash + "/"; 132 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"); 133 let themeStyle = ""; 134 if (!isDefault) { 135 themeStyle = `<link rel="stylesheet" type="text/css" id="themeStyle" href="${servePath}appearance/themes/${window.siyuan.config.appearance.themeLight}/theme.css?${Constants.SIYUAN_VERSION}"/>`; 136 } 137 const currentWindowId = await ipcRenderer.invoke(Constants.SIYUAN_GET, { 138 cmd: "getContentsId", 139 }); 140 // data-theme-mode="light" https://github.com/siyuan-note/siyuan/issues/7379 141 const html = `<!DOCTYPE html> 142<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}"> 143<head> 144 <base href="${servePath}"> 145 <meta charset="utf-8"> 146 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 147 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> 148 <meta name="mobile-web-app-capable" content="yes"/> 149 <meta name="apple-mobile-web-app-status-bar-style" content="black"> 150 <link rel="stylesheet" type="text/css" id="baseStyle" href="${servePath}stage/build/export/base.css?v=${Constants.SIYUAN_VERSION}"/> 151 <link rel="stylesheet" type="text/css" id="themeDefaultStyle" href="${servePath}appearance/themes/daylight/theme.css?v=${Constants.SIYUAN_VERSION}"/> 152 <script src="${servePath}stage/protyle/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}"></script> 153 ${themeStyle} 154 <title>${window.siyuan.languages.export} PDF</title> 155 <style> 156 body { 157 margin: 0; 158 font-family: var(--b3-font-family); 159 } 160 161 #action { 162 width: 232px; 163 background: var(--b3-theme-surface); 164 padding: 16px 0; 165 position: fixed; 166 right: 0; 167 top: 0; 168 overflow-y: auto; 169 bottom: 0; 170 overflow-x: hidden; 171 z-index: 1; 172 display: flex; 173 flex-direction: column; 174 } 175 176 #preview { 177 max-width: 800px; 178 margin: 0 auto; 179 position: absolute; 180 right: 232px; 181 left: 0; 182 box-sizing: border-box; 183 } 184 185 #preview.exporting { 186 position: inherit; 187 max-width: none; 188 } 189 190 .b3-switch { 191 margin-left: 14px; 192 } 193 194 .exporting::-webkit-scrollbar { 195 width: 0; 196 height: 0; 197 } 198 199 .protyle-wysiwyg { 200 height: 100%; 201 overflow: auto; 202 box-sizing: border-box; 203 } 204 205 .b3-label { 206 border-bottom: 1px solid var(--b3-theme-surface-lighter); 207 display: block; 208 color: var(--b3-theme-on-surface); 209 padding-bottom: 16px; 210 margin: 0 16px 16px 16px; 211 } 212 213 .b3-label:last-child { 214 border-bottom: none; 215 } 216 ${await setInlineStyle(false, servePath)} 217 ${await getPluginStyle()} 218 </style> 219 ${getSnippetCSS()} 220</head> 221<body style="-webkit-print-color-adjust: exact;"> 222<div id="action"> 223 <div style="flex: 1;overflow-y:auto;overflow-x:hidden"> 224 <div class="b3-label"> 225 <div> 226 ${window.siyuan.languages.exportPDF0} 227 </div> 228 <span class="fn__hr"></span> 229 <select class="b3-select" id="pageSize"> 230 <option ${localData.pageSize === "A3" ? "selected" : ""} value="A3">A3</option> 231 <option ${localData.pageSize === "A4" ? "selected" : ""} value="A4">A4</option> 232 <option ${localData.pageSize === "A5" ? "selected" : ""} value="A5">A5</option> 233 <option ${localData.pageSize === "Legal" ? "selected" : ""} value="Legal">Legal</option> 234 <option ${localData.pageSize === "Letter" ? "selected" : ""} value="Letter">Letter</option> 235 <option ${localData.pageSize === "Tabloid" ? "selected" : ""} value="Tabloid">Tabloid</option> 236 </select> 237 </div> 238 <div class="b3-label"> 239 <div> 240 ${window.siyuan.languages.exportPDF2} 241 </div> 242 <span class="fn__hr"></span> 243 <select class="b3-select" id="marginsType"> 244 <option ${localData.marginType === "default" ? "selected" : ""} value="default">${window.siyuan.languages.defaultMargin}</option> 245 <option ${localData.marginType === "none" ? "selected" : ""} value="none">${window.siyuan.languages.noneMargin}</option> 246 <option ${localData.marginType === "printableArea" ? "selected" : ""} value="printableArea">${window.siyuan.languages.minimalMargin}</option> 247 <option ${localData.marginType === "custom" ? "selected" : ""} value="custom">${window.siyuan.languages.customMargin}</option> 248 </select> 249 <div class="${localData.marginType === "custom" ? "" : "fn__none"}"> 250 <span class="fn__hr"></span> 251 <small>${window.siyuan.languages.marginTop}</small> 252 <div class="fn__hr--small"></div> 253 <div class="fn__flex"> 254 <input id="marginsTop" class="b3-text-field fn__block" value="${localData.marginTop || 0}" type="number" min="0" step="0.01"> 255 <span class="fn__space"></span> 256 <small class="fn__flex-center" style="white-space: nowrap;">${window.siyuan.languages.unitInches}</small> 257 </div> 258 <div class="fn__hr"></div> 259 <small>${window.siyuan.languages.marginRight}</small> 260 <div class="fn__hr--small"></div> 261 <div class="fn__flex"> 262 <input id="marginsRight" class="b3-text-field fn__block" value="${localData.marginRight || 0}" type="number" min="0" step="0.01"> 263 <span class="fn__space"></span> 264 <small class="fn__flex-center" style="white-space: nowrap;">${window.siyuan.languages.unitInches}</small> 265 </div> 266 <div class="fn__hr"></div> 267 <small>${window.siyuan.languages.marginBottom}</small> 268 <div class="fn__hr--small"></div> 269 <div class="fn__flex"> 270 <input id="marginsBottom" class="b3-text-field fn__block" value="${localData.marginBottom || 0}" type="number" min="0" step="0.01"> 271 <span class="fn__space"></span> 272 <small class="fn__flex-center" style="white-space: nowrap;">${window.siyuan.languages.unitInches}</small> 273 </div> 274 <div class="fn__hr"></div> 275 <small>${window.siyuan.languages.marginLeft}</small> 276 <div class="fn__hr--small"></div> 277 <div class="fn__flex"> 278 <input id="marginsLeft" class="b3-text-field fn__block" value="${localData.marginLeft || 0}" type="number" min="0" step="0.01"> 279 <span class="fn__space"></span> 280 <small class="fn__flex-center" style="white-space: nowrap;">${window.siyuan.languages.unitInches}</small> 281 </div> 282 </div> 283 </div> 284 <div class="b3-label"> 285 <div> 286 ${window.siyuan.languages.exportPDF3} 287 <span id="scaleTip" style="float: right;color: var(--b3-theme-on-background);">${localData.scale || 1}</span> 288 </div> 289 <span class="fn__hr"></span> 290 <input style="width: 189px" value="${localData.scale || 1}" id="scale" step="0.1" class="b3-slider" type="range" min="0.1" max="2"> 291 </div> 292 <label class="b3-label"> 293 <div> 294 ${window.siyuan.languages.exportPDF1} 295 </div> 296 <span class="fn__hr"></span> 297 <input id="landscape" class="b3-switch" type="checkbox" ${localData.landscape ? "checked" : ""}> 298 </label> 299 <label class="b3-label"> 300 <div> 301 ${window.siyuan.languages.exportPDF4} 302 </div> 303 <span class="fn__hr"></span> 304 <input id="removeAssets" class="b3-switch" type="checkbox" ${localData.removeAssets ? "checked" : ""}> 305 </label> 306 <label class="b3-label"> 307 <div> 308 ${window.siyuan.languages.exportPDF5} 309 </div> 310 <span class="fn__hr"></span> 311 <input id="keepFold" class="b3-switch" type="checkbox" ${localData.keepFold ? "checked" : ""}> 312 </label> 313 <label class="b3-label"> 314 <div> 315 ${window.siyuan.languages.mergeSubdocs} 316 </div> 317 <span class="fn__hr"></span> 318 <input id="mergeSubdocs" class="b3-switch" type="checkbox" ${localData.mergeSubdocs ? "checked" : ""}> 319 </label> 320 <label class="b3-label"> 321 <div> 322 ${window.siyuan.languages.export27} 323 </div> 324 <span class="fn__hr"></span> 325 <input id="watermark" class="b3-switch" type="checkbox" ${localData.watermark ? "checked" : ""}> 326 </label> 327 </div> 328 <div class="fn__flex" style="padding: 0 16px"> 329 <div class="fn__flex-1"></div> 330 <button class="b3-button b3-button--cancel">${window.siyuan.languages.cancel}</button> 331 <div class="fn__space"></div> 332 <button class="b3-button b3-button--text">${window.siyuan.languages.confirm}</button> 333 </div> 334</div> 335<div style="zoom:${localData.scale || 1}" id="preview"> 336 <div class="fn__loading" style="left:0;height:100vh"><img width="48px" src="${servePath}stage/loading-pure.svg"></div> 337</div> 338${getIconScript(servePath)} 339<script src="${servePath}stage/build/export/protyle-method.js?${Constants.SIYUAN_VERSION}"></script> 340<script src="${servePath}stage/protyle/js/lute/lute.min.js?${Constants.SIYUAN_VERSION}"></script> 341<script> 342 const previewElement = document.getElementById('preview'); 343 const fixBlockWidth = () => { 344 const isLandscape = document.querySelector("#landscape").checked; 345 let width = 800 346 switch (document.querySelector("#action #pageSize").value) { 347 case "A3": 348 width = isLandscape ? 1587.84 : 1122.24 349 break; 350 case "A4": 351 width = isLandscape ? 1122.24 : 793.92 352 break; 353 case "A5": 354 width = isLandscape ? 793.92 : 559.68 355 break; 356 case "Legal": 357 width = isLandscape ? 1344: 816 358 break; 359 case "Letter": 360 width = isLandscape ? 1056 : 816 361 break; 362 case "Tabloid": 363 width = isLandscape ? 1632 : 1056 364 break; 365 } 366 width = width / parseFloat(document.querySelector("#scale").value); 367 previewElement.style.width = width + "px"; 368 width = width - parseFloat(previewElement.style.paddingLeft) * 96 * 2; 369 // 为保持代码块宽度一致,全部都进行宽度设定 https://github.com/siyuan-note/siyuan/issues/7692 370 previewElement.querySelectorAll('.hljs').forEach((item) => { 371 // 强制换行 https://ld246.com/article/1679228783553 372 item.parentElement.setAttribute("linewrap", "true"); 373 item.parentElement.style.width = ""; 374 item.parentElement.style.boxSizing = "border-box"; 375 item.parentElement.style.width = Math.min(item.parentElement.clientWidth, width) + "px"; 376 item.removeAttribute('data-render'); 377 }) 378 Protyle.highlightRender(previewElement, "${servePath}stage/protyle", document.querySelector("#scale").value); 379 previewElement.querySelectorAll('[data-type="NodeMathBlock"]').forEach((item) => { 380 // 超级块内不能移除 width https://github.com/siyuan-note/siyuan/issues/14318 381 item.removeAttribute('data-render'); 382 }) 383 previewElement.querySelectorAll('[data-type="NodeCodeBlock"][data-subtype="mermaid"] svg').forEach((item) => { 384 item.style.maxHeight = width * 1.414 + "px"; 385 }) 386 Protyle.mathRender(previewElement, "${servePath}stage/protyle", true); 387 previewElement.querySelectorAll("table").forEach(item => { 388 if (item.clientWidth > item.parentElement.clientWidth) { 389 item.style.zoom = (item.parentElement.clientWidth / item.clientWidth).toFixed(2) - 0.01; 390 item.parentElement.style.overflow = "hidden"; 391 } 392 }) 393 } 394 const setPadding = () => { 395 const isLandscape = document.querySelector("#landscape").checked; 396 const topElement = document.querySelector("#marginsTop") 397 const rightElement = document.querySelector("#marginsRight") 398 const bottomElement = document.querySelector("#marginsBottom") 399 const leftElement = document.querySelector("#marginsLeft") 400 switch (document.querySelector("#marginsType").value) { 401 case "default": 402 if (isLandscape) { 403 topElement.value = "0.42"; 404 rightElement.value = "0.42"; 405 bottomElement.value = "0.42"; 406 leftElement.value = "0.42"; 407 } else { 408 topElement.value = "1"; 409 rightElement.value = "0.54"; 410 bottomElement.value = "1"; 411 leftElement.value = "0.54"; 412 } 413 break; 414 case "none": // none 415 topElement.value = "0"; 416 rightElement.value = "0"; 417 bottomElement.value = "0"; 418 leftElement.value = "0"; 419 break; 420 case "printableArea": // minimal 421 if (isLandscape) { 422 topElement.value = ".07"; 423 rightElement.value = ".07"; 424 bottomElement.value = ".07"; 425 leftElement.value = ".07"; 426 } else { 427 topElement.value = "0.58"; 428 rightElement.value = "0.1"; 429 bottomElement.value = "0.58"; 430 leftElement.value = "0.1"; 431 } 432 break; 433 } 434 document.getElementById('preview').style.padding = topElement.value + "in " 435 + rightElement.value + "in " 436 + bottomElement.value + "in " 437 + leftElement.value + "in"; 438 setTimeout(() => { 439 fixBlockWidth(); 440 }, 300); 441 } 442 const fetchPost = (url, data, cb) => { 443 fetch("${servePathWithoutTrailingSlash}" + url, { 444 method: "POST", 445 body: JSON.stringify(data) 446 }).then((response) => { 447 return response.json(); 448 }).then((response) => { 449 cb(response); 450 }) 451 } 452 const renderPreview = (data) => { 453 previewElement.innerHTML = '<div style="padding:8px 0 0 0" class="protyle-wysiwyg${window.siyuan.config.editor.displayBookmarkIcon ? " protyle-wysiwyg--attr" : ""}">' + data.content + '</div>'; 454 const wysElement = previewElement.querySelector(".protyle-wysiwyg"); 455 wysElement.setAttribute("data-doc-type", data.type || "NodeDocument"); 456 Object.keys(data.attrs).forEach(key => { 457 wysElement.setAttribute(key, data.attrs[key]); 458 }) 459 // https://github.com/siyuan-note/siyuan/issues/13669 460 wysElement.querySelectorAll('[data-node-id]').forEach((item) => { 461 if (item.querySelector(".img")) { 462 item.insertAdjacentHTML("beforeend", "<hr style='margin:0;border:0'>"); 463 } 464 }) 465 Protyle.mermaidRender(wysElement, "${servePath}stage/protyle"); 466 Protyle.flowchartRender(wysElement, "${servePath}stage/protyle"); 467 Protyle.graphvizRender(wysElement, "${servePath}stage/protyle"); 468 Protyle.chartRender(wysElement, "${servePath}stage/protyle"); 469 Protyle.mindmapRender(wysElement, "${servePath}stage/protyle"); 470 Protyle.abcRender(wysElement, "${servePath}stage/protyle"); 471 Protyle.htmlRender(wysElement); 472 Protyle.plantumlRender(wysElement, "${servePath}stage/protyle"); 473 } 474 fetchPost("/api/export/exportPreviewHTML", { 475 id: "${id}", 476 keepFold: ${localData.keepFold}, 477 merge: ${localData.mergeSubdocs}, 478 }, response => { 479 if (response.code === 1) { 480 alert(response.msg) 481 return; 482 } 483 document.title = response.data.name 484 window.siyuan = { 485 config: { 486 appearance: { mode: 0, codeBlockThemeDark: "${window.siyuan.config.appearance.codeBlockThemeDark}", codeBlockThemeLight: "${window.siyuan.config.appearance.codeBlockThemeLight}" }, 487 editor: { 488 allowHTMLBLockScript: ${window.siyuan.config.editor.allowHTMLBLockScript}, 489 fontSize: ${window.siyuan.config.editor.fontSize}, 490 codeLineWrap: true, 491 codeLigatures: ${window.siyuan.config.editor.codeLigatures}, 492 plantUMLServePath: "${window.siyuan.config.editor.plantUMLServePath}", 493 codeSyntaxHighlightLineNum: ${window.siyuan.config.editor.codeSyntaxHighlightLineNum}, 494 katexMacros: decodeURI(\`${encodeURI(window.siyuan.config.editor.katexMacros)}\`), 495 } 496 }, 497 languages: {copy:"${window.siyuan.languages.copy}"} 498 }; 499 previewElement.addEventListener("click", (event) => { 500 let target = event.target; 501 while (target && !target.isEqualNode(previewElement)) { 502 if (target.tagName === "A") { 503 const linkAddress = target.getAttribute("href"); 504 if (linkAddress.startsWith("#")) { 505 // 导出预览模式点击块引转换后的脚注跳转不正确 https://github.com/siyuan-note/siyuan/issues/5700 506 const hash = linkAddress.substring(1); 507 previewElement.querySelector('[data-node-id="' + hash + '"], [id="' + hash + '"]').scrollIntoView(); 508 event.stopPropagation(); 509 event.preventDefault(); 510 return; 511 } 512 } else if (target.classList.contains("protyle-action__copy")) { 513 let text = target.parentElement.nextElementSibling.textContent.trimEnd(); 514 text = text.replace(/\u00A0/g, " "); // Replace non-breaking spaces with normal spaces when copying https://github.com/siyuan-note/siyuan/issues/9382 515 navigator.clipboard.writeText(text); 516 event.preventDefault(); 517 event.stopPropagation(); 518 break; 519 } 520 target = target.parentElement; 521 } 522 }); 523 const actionElement = document.getElementById('action'); 524 const keepFoldElement = actionElement.querySelector('#keepFold'); 525 keepFoldElement.addEventListener('change', () => { 526 refreshPreview(); 527 }); 528 const mergeSubdocsElement = actionElement.querySelector('#mergeSubdocs'); 529 mergeSubdocsElement.addEventListener('change', () => { 530 refreshPreview(); 531 }); 532 const watermarkElement = actionElement.querySelector('#watermark'); 533 const refreshPreview = () => { 534 previewElement.innerHTML = '<div class="fn__loading" style="left:0;height: 100vh"><img width="48px" src="${servePath}stage/loading-pure.svg"></div>' 535 fetchPost("/api/export/exportPreviewHTML", { 536 id: "${id}", 537 keepFold: keepFoldElement.checked, 538 merge: mergeSubdocsElement.checked, 539 }, response2 => { 540 if (response2.code === 1) { 541 alert(response2.msg) 542 return; 543 } 544 setPadding(); 545 renderPreview(response2.data); 546 }) 547 }; 548 549 actionElement.querySelector("#scale").addEventListener("input", () => { 550 const scale = actionElement.querySelector("#scale").value; 551 actionElement.querySelector("#scaleTip").innerText = scale; 552 previewElement.style.zoom = scale; 553 fixBlockWidth(); 554 }) 555 actionElement.querySelector("#pageSize").addEventListener('change', () => { 556 fixBlockWidth(); 557 }); 558 actionElement.querySelector("#marginsType").addEventListener('change', (event) => { 559 setPadding(); 560 if (event.target.value === "custom") { 561 event.target.nextElementSibling.classList.remove("fn__none"); 562 } else { 563 event.target.nextElementSibling.classList.add("fn__none"); 564 } 565 }); 566 actionElement.querySelector("#marginsTop").addEventListener('change', () => { 567 setPadding(); 568 }); 569 actionElement.querySelector("#marginsRight").addEventListener('change', () => { 570 setPadding(); 571 }); 572 actionElement.querySelector("#marginsBottom").addEventListener('change', () => { 573 setPadding(); 574 }); 575 actionElement.querySelector("#marginsLeft").addEventListener('change', () => { 576 setPadding(); 577 }); 578 actionElement.querySelector("#landscape").addEventListener('change', () => { 579 setPadding(); 580 }); 581 actionElement.querySelector('.b3-button--cancel').addEventListener('click', () => { 582 const {ipcRenderer} = require("electron"); 583 ipcRenderer.send("${Constants.SIYUAN_CMD}", "destroy") 584 }); 585 actionElement.querySelector('.b3-button--text').addEventListener('click', () => { 586 const {ipcRenderer} = require("electron"); 587 ipcRenderer.send("${Constants.SIYUAN_EXPORT_PDF}", { 588 title: "${window.siyuan.languages.export} PDF", 589 pdfOptions:{ 590 printBackground: true, 591 landscape: actionElement.querySelector("#landscape").checked, 592 marginType: actionElement.querySelector("#marginsType").value, 593 margins: { 594 top: parseFloat(document.querySelector("#marginsTop").value), 595 bottom: parseFloat(document.querySelector("#marginsBottom").value), 596 left: parseFloat(document.querySelector("#marginsLeft").value), 597 right: parseFloat(document.querySelector("#marginsRight").value), 598 }, 599 scale: parseFloat(actionElement.querySelector("#scale").value), 600 pageSize: actionElement.querySelector("#pageSize").value, 601 }, 602 keepFold: keepFoldElement.checked, 603 mergeSubdocs: mergeSubdocsElement.checked, 604 watermark: watermarkElement.checked, 605 removeAssets: actionElement.querySelector("#removeAssets").checked, 606 rootId: "${id}", 607 rootTitle: response.data.name, 608 parentWindowId: ${currentWindowId}, 609 }) 610 previewElement.classList.add("exporting"); 611 previewElement.style.zoom = ""; 612 previewElement.style.paddingTop = "6px"; 613 previewElement.style.paddingBottom = "0"; 614 fixBlockWidth(); 615 actionElement.remove(); 616 }); 617 setPadding(); 618 renderPreview(response.data); 619 window.addEventListener("keydown", (event) => { 620 if (event.key === "Escape") { 621 const {ipcRenderer} = require("electron"); 622 ipcRenderer.send("${Constants.SIYUAN_CMD}", "destroy") 623 event.preventDefault(); 624 } 625 }) 626 }); 627</script></body></html>`; 628 fetchPost("/api/export/exportTempContent", {content: html}, (response) => { 629 ipcRenderer.send(Constants.SIYUAN_EXPORT_NEWWINDOW, response.data.url); 630 }); 631}; 632 633const getExportPath = (option: IExportOptions, removeAssets?: boolean, mergeSubdocs?: boolean) => { 634 fetchPost("/api/block/getBlockInfo", { 635 id: option.id 636 }, async (response) => { 637 if (response.code === 3) { 638 showMessage(response.msg); 639 return; 640 } 641 let exportType = "HTML (SiYuan)"; 642 switch (option.type) { 643 case "htmlmd": 644 exportType = "HTML (Markdown)"; 645 break; 646 case "word": 647 exportType = "Word .docx"; 648 break; 649 case "pdf": 650 exportType = "PDF"; 651 break; 652 } 653 654 const result = await ipcRenderer.invoke(Constants.SIYUAN_GET, { 655 cmd: "showOpenDialog", 656 title: window.siyuan.languages.export + " " + exportType, 657 properties: ["createDirectory", "openDirectory"], 658 }); 659 if (!result.canceled) { 660 const msgId = showMessage(window.siyuan.languages.exporting, -1); 661 let url = "/api/export/exportHTML"; 662 if (option.type === "htmlmd") { 663 url = "/api/export/exportMdHTML"; 664 } else if (option.type === "word") { 665 url = "/api/export/exportDocx"; 666 } 667 let savePath = result.filePaths[0]; 668 if (option.type !== "word" && !savePath.endsWith(response.data.rootTitle)) { 669 savePath = path.join(savePath, replaceLocalPath(response.data.rootTitle)); 670 } 671 savePath = savePath.trim(); 672 fetchPost(url, { 673 id: option.id, 674 pdf: option.type === "pdf", 675 removeAssets: removeAssets, 676 merge: mergeSubdocs, 677 savePath 678 }, exportResponse => { 679 if (option.type === "word") { 680 if (exportResponse.code === 1) { 681 showMessage(exportResponse.msg, undefined, "error"); 682 hideMessage(msgId); 683 return; 684 } 685 afterExport(exportResponse.data.path, msgId); 686 } else { 687 onExport(exportResponse, savePath, "", option, msgId); 688 } 689 }); 690 } 691 }); 692}; 693/// #endif 694 695export const onExport = async (data: IWebSocketData, filePath: string, servePath: string, exportOption: IExportOptions, msgId?: string) => { 696 let themeName = window.siyuan.config.appearance.themeLight; 697 let mode = 0; 698 if (["html", "htmlmd"].includes(exportOption.type) && window.siyuan.config.appearance.mode === 1) { 699 themeName = window.siyuan.config.appearance.themeDark; 700 mode = 1; 701 } 702 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"); 703 let themeStyle = ""; 704 if (!isDefault) { 705 themeStyle = `<link rel="stylesheet" type="text/css" id="themeStyle" href="${servePath}appearance/themes/${themeName}/theme.css?${Constants.SIYUAN_VERSION}"/>`; 706 } 707 const screenWidth = getScreenWidth(); 708 const minWidthHtml = isInAndroid() || isInHarmony() ? `document.body.style.minWidth = "${screenWidth}px"` : ""; 709 const html = `<!DOCTYPE html> 710<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}"> 711<head> 712 <base href="${servePath}"> 713 <meta charset="utf-8"> 714 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 715 <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> 716 <meta name="mobile-web-app-capable" content="yes"/> 717 <meta name="apple-mobile-web-app-status-bar-style" content="black"> 718 <link rel="stylesheet" type="text/css" id="baseStyle" href="${servePath}stage/build/export/base.css?v=${Constants.SIYUAN_VERSION}"/> 719 <link rel="stylesheet" type="text/css" id="themeDefaultStyle" href="${servePath}appearance/themes/${themeName}/theme.css?v=${Constants.SIYUAN_VERSION}"/> 720 <script src="${servePath}stage/protyle/js/protyle-html.js?v=${Constants.SIYUAN_VERSION}"></script> 721 ${themeStyle} 722 <title>${data.data.name}</title> 723 <!-- Exported by SiYuan v${Constants.SIYUAN_VERSION} --> 724 <style> 725 body {font-family: var(--b3-font-family);background-color: var(--b3-theme-background);color: var(--b3-theme-on-background)} 726 ${await setInlineStyle(false, servePath)} 727 ${await getPluginStyle()} 728 </style> 729 ${getSnippetCSS()} 730</head> 731<body> 732<div class="${["htmlmd", "word"].includes(exportOption.type) ? "b3-typography" : "protyle-wysiwyg" + (window.siyuan.config.editor.displayBookmarkIcon ? " protyle-wysiwyg--attr" : "")}" 733style="${isInAndroid() || isInHarmony() ? "margin: 0 16px;" : "max-width: 800px;margin: 0 auto;"}" id="preview">${data.data.content}</div> 734${getIconScript(servePath)} 735<script src="${servePath}stage/build/export/protyle-method.js?v=${Constants.SIYUAN_VERSION}"></script> 736<script src="${servePath}stage/protyle/js/lute/lute.min.js?v=${Constants.SIYUAN_VERSION}"></script> 737<script> 738 ${minWidthHtml}; 739 window.siyuan = { 740 config: { 741 appearance: { mode: ${mode}, codeBlockThemeDark: "${window.siyuan.config.appearance.codeBlockThemeDark}", codeBlockThemeLight: "${window.siyuan.config.appearance.codeBlockThemeLight}" }, 742 editor: { 743 codeLineWrap: true, 744 fontSize: ${window.siyuan.config.editor.fontSize}, 745 codeLigatures: ${window.siyuan.config.editor.codeLigatures}, 746 plantUMLServePath: "${window.siyuan.config.editor.plantUMLServePath}", 747 codeSyntaxHighlightLineNum: ${window.siyuan.config.editor.codeSyntaxHighlightLineNum}, 748 katexMacros: decodeURI(\`${encodeURI(window.siyuan.config.editor.katexMacros)}\`), 749 } 750 }, 751 languages: {copy:"${window.siyuan.languages.copy}"} 752 }; 753 const previewElement = document.getElementById('preview'); 754 Protyle.highlightRender(previewElement, "stage/protyle"); 755 Protyle.mathRender(previewElement, "stage/protyle", ${exportOption.type === "pdf"}); 756 Protyle.mermaidRender(previewElement, "stage/protyle"); 757 Protyle.flowchartRender(previewElement, "stage/protyle"); 758 Protyle.graphvizRender(previewElement, "stage/protyle"); 759 Protyle.chartRender(previewElement, "stage/protyle"); 760 Protyle.mindmapRender(previewElement, "stage/protyle"); 761 Protyle.abcRender(previewElement, "stage/protyle"); 762 Protyle.htmlRender(previewElement); 763 Protyle.plantumlRender(previewElement, "stage/protyle"); 764 document.querySelectorAll(".protyle-action__copy").forEach((item) => { 765 item.addEventListener("click", (event) => { 766 let text = item.parentElement.nextElementSibling.textContent.trimEnd(); 767 text = text.replace(/\u00A0/g, " "); // Replace non-breaking spaces with normal spaces when copying 768 navigator.clipboard.writeText(text); 769 event.preventDefault(); 770 event.stopPropagation(); 771 }) 772 }); 773</script></body></html>`; 774 // 移动端导出 pdf、浏览器导出 HTML 775 if (typeof filePath === "undefined") { 776 return html; 777 } 778 /// #if !BROWSER 779 const htmlPath = path.join(filePath, "index.html"); 780 fs.writeFileSync(htmlPath, html); 781 afterExport(htmlPath, msgId); 782 /// #endif 783};