A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
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};