🎨 Supports exporting a code block as a file https://github.com/siyuan-note/siyuan/pull/16774

Signed-off-by: Daniel <845765@qq.com>
This commit is contained in:
Daniel 2026-01-04 21:42:38 +08:00
parent 9199188e96
commit e6d79c1760
No known key found for this signature in database
GPG key ID: 86211BA83DF03017
16 changed files with 89 additions and 88 deletions

View file

@ -36,7 +36,7 @@ import {highlightRender} from "../render/highlightRender";
import {blockRender} from "../render/blockRender";
import {getContenteditableElement, getParentBlock, getTopAloneElement, isNotEditBlock} from "../wysiwyg/getBlock";
import * as dayjs from "dayjs";
import {fetchPost} from "../../util/fetch";
import {fetchPost, fetchSyncPost} from "../../util/fetch";
import {cancelSB, genEmptyElement, getLangByType, insertEmptyBlock, jumpToParent,} from "../../block/util";
import {countBlockWord} from "../../layout/status";
import {Constants} from "../../constants";
@ -61,7 +61,6 @@ import {processClonePHElement} from "../render/util";
/// #if !MOBILE
import {openFileById} from "../../editor/util";
import * as path from "path";
import {fetchSyncPost} from "../../util/fetch";
import {replaceLocalPath} from "../../editor/rename";
import {showMessage} from "../../dialog/message";
import {ipcRenderer} from "electron";
@ -69,6 +68,7 @@ import * as fs from "node:fs";
/// #endif
import {checkFold} from "../../util/noRelyPCFunction";
import {clearSelect} from "../util/clear";
import {code160to32} from "../util/code160to32";
export class Gutter {
public element: HTMLElement;
@ -1577,79 +1577,80 @@ export class Gutter {
window.siyuan.menus.menu.remove();
});
}
}, {
/// #if !MOBILE
id: "saveCodeBlockAsFile",
iconHTML: "",
label: window.siyuan.languages.saveCodeBlockAsFile,
async bind(element) {
element.addEventListener("click", async () => {
const hljsElement = nodeElement.querySelector(".hljs") as HTMLElement;
let code = hljsElement?.textContent || "";
code = code160to32(code);
// https://github.com/siyuan-note/siyuan/issues/14800
code = code.replace(/\u200D```/g, "```");
let docName = window.siyuan.languages._kernel[16];
if (protyle.block?.rootID) {
try {
const docInfo = await fetchSyncPost("/api/block/getDocInfo", {
id: protyle.block.rootID
});
if (docInfo?.data?.name) {
docName = replaceLocalPath(docInfo.data.name);
let truncatedDocName = "";
let byteCount = 0;
const encoder = new TextEncoder();
for (const char of docName) {
const charBytes = encoder.encode(char).length;
if (byteCount + charBytes > 170) { // 189 - 19(-YYYYMMDDHHmmss.txt)
break;
}
truncatedDocName += char;
byteCount += charBytes;
}
docName = truncatedDocName;
}
} catch {
console.warn("Failed to fetch document info for code block export.");
}
}
const fileName = `${docName}-${dayjs().format("YYYYMMDDHHmmss")}.txt`;
/// #if BROWSER
const blob = new Blob([code], {type: "text/plain;charset=utf-8"});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
showMessage(window.siyuan.languages.exported);
/// #else
const result = await ipcRenderer.invoke(Constants.SIYUAN_GET, {
cmd: "showSaveDialog",
defaultPath: fileName,
properties: ["showOverwriteConfirmation"],
});
if (!result.canceled && result.filePath) {
try {
fs.writeFileSync(result.filePath, code, "utf-8");
showMessage(window.siyuan.languages.exported);
} catch (error) {
showMessage(window.siyuan.languages._kernel[14].replace("%s", (error instanceof Error ? error.message : String(error))));
}
}
/// #endif
window.siyuan.menus.menu.remove();
});
}
/// #endif
}]
}).element);
/// #if !MOBILE
const hljsElement = nodeElement.querySelector(".hljs") as HTMLElement;
let code = hljsElement?.textContent || "";
if (code.length > 0) {
window.siyuan.menus.menu.append(new MenuItem({
id: "exportCodeBlock",
label: window.siyuan.languages.exportCodeBlock,
icon: "iconUpload",
async click() {
code = code.replace(/\u00A0/g, " ");
// https://github.com/siyuan-note/siyuan/issues/14800
code = code.replace(/\u200D```/g, "```");
let docName = window.siyuan.languages._kernel[16];
if (protyle.block?.rootID) {
try {
const docInfo = await fetchSyncPost("/api/block/getDocInfo", {
id: protyle.block.rootID
});
if (docInfo?.data?.name) {
docName = replaceLocalPath(docInfo.data.name);
let truncatedDocName = "";
let byteCount = 0;
const encoder = new TextEncoder();
for (const char of docName) {
const charBytes = encoder.encode(char).length;
if (byteCount + charBytes > 170) { // 189 - 19(-YYYYMMDDHHmmss.txt)
break;
}
truncatedDocName += char;
byteCount += charBytes;
}
docName = truncatedDocName;
}
} catch {
// API 获取失败时使用默认值
}
}
const fileName = `${docName}-${dayjs().format("YYYYMMDDHHmmss")}.txt`;
/// #if BROWSER
const blob = new Blob([code], {type: "text/plain;charset=utf-8"});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
showMessage(window.siyuan.languages.exported);
/// #else
const result = await ipcRenderer.invoke(Constants.SIYUAN_GET, {
cmd: "showSaveDialog",
defaultPath: fileName,
properties: ["showOverwriteConfirmation"],
});
if (!result.canceled && result.filePath) {
try {
fs.writeFileSync(result.filePath, code, "utf-8");
showMessage(window.siyuan.languages.exported);
} catch (error) {
showMessage(window.siyuan.languages._kernel[14].replace("%s", (error instanceof Error ? error.message : String(error))));
}
}
/// #endif
window.siyuan.menus.menu.remove();
}
}).element);
}
/// #endif
} else if (type === "NodeCodeBlock" && !protyle.disabled && ["echarts", "mindmap"].includes(nodeElement.getAttribute("data-subtype"))) {
window.siyuan.menus.menu.append(new MenuItem({id: "separator_chart", type: "separator"}).element);
const height = (nodeElement as HTMLElement).style.height;