mirror of
https://github.com/taylorchen/obsidian-press.git
synced 2026-07-22 06:52:12 +00:00
2015 lines
64 KiB
JavaScript
2015 lines
64 KiB
JavaScript
/*
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
if you want to view the source, please visit the github repository
|
|
*/
|
|
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// src/main.ts
|
|
var main_exports = {};
|
|
__export(main_exports, {
|
|
default: () => ObsidianPressPlugin
|
|
});
|
|
module.exports = __toCommonJS(main_exports);
|
|
var import_obsidian4 = require("obsidian");
|
|
var path5 = __toESM(require("path"));
|
|
|
|
// src/types.ts
|
|
var DEFAULT_SETTINGS = {
|
|
pandocPath: "/opt/homebrew/bin/pandoc",
|
|
pdfEngine: "xelatex",
|
|
enginePath: "",
|
|
outputDir: "pdf",
|
|
outputNaming: "same",
|
|
openAfterExport: true,
|
|
defaultFormat: "pdf",
|
|
customCssPath: "",
|
|
customTemplatePath: "",
|
|
fontSize: 11,
|
|
pageSize: "A4",
|
|
pageMargin: "25",
|
|
codeTheme: "tango",
|
|
cjkFont: "",
|
|
enableCjk: true,
|
|
mermaidPath: "mmdc",
|
|
mermaidTheme: "default",
|
|
extraArgs: "",
|
|
concurrency: 3,
|
|
skipErrors: true
|
|
};
|
|
|
|
// src/settings.ts
|
|
var import_obsidian = require("obsidian");
|
|
var ObsidianPressSettingTab = class extends import_obsidian.PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
new import_obsidian.Setting(containerEl).setName("General").setHeading();
|
|
new import_obsidian.Setting(containerEl).setName("Pandoc path").setDesc("Path to the pandoc binary").addText(
|
|
(text) => text.setPlaceholder("/opt/homebrew/bin/pandoc").setValue(this.plugin.settings.pandocPath).onChange(async (value) => {
|
|
this.plugin.settings.pandocPath = value || "/opt/homebrew/bin/pandoc";
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("PDF engine").setDesc("The PDF rendering engine to use").addDropdown(
|
|
(dropdown) => dropdown.addOption("xelatex", "XeLaTeX (best quality)").addOption("pdflatex", "pdfLaTeX (auto XeLaTeX for CJK)").addOption("lualatex", "LuaLaTeX").addOption("wkhtmltopdf", "wkhtmltopdf (lightweight)").addOption("weasyprint", "WeasyPrint (CSS-based)").addOption("typst", "Typst (experimental)").setValue(this.plugin.settings.pdfEngine).onChange(async (value) => {
|
|
this.plugin.settings.pdfEngine = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Default format").setDesc("Default export format").addDropdown(
|
|
(dropdown) => dropdown.addOption("pdf", "PDF").addOption("docx", "Word (DOCX)").addOption("html", "HTML").setValue(this.plugin.settings.defaultFormat).onChange(async (value) => {
|
|
this.plugin.settings.defaultFormat = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Output").setHeading();
|
|
new import_obsidian.Setting(containerEl).setName("Output directory").setDesc(
|
|
"Relative to vault root, or absolute path. Leave empty for 'pdf' folder"
|
|
).addText(
|
|
(text) => text.setPlaceholder("pdf").setValue(this.plugin.settings.outputDir).onChange(async (value) => {
|
|
this.plugin.settings.outputDir = value || "pdf";
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("File naming").setDesc("How output files are named").addDropdown(
|
|
(dropdown) => dropdown.addOption("same", "Same as source (note.pdf)").addOption("timestamp", "With timestamp (note_2024-01-01T00-00-00.pdf)").addOption("suffix", "With suffix (note_export.pdf)").setValue(this.plugin.settings.outputNaming).onChange(async (value) => {
|
|
this.plugin.settings.outputNaming = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Open after export").setDesc("Automatically open the exported file after completion").addToggle(
|
|
(toggle) => toggle.setValue(this.plugin.settings.openAfterExport).onChange(async (value) => {
|
|
this.plugin.settings.openAfterExport = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Typography").setHeading();
|
|
new import_obsidian.Setting(containerEl).setName("Font size").setDesc("Base font size in points").addText(
|
|
(text) => text.setPlaceholder("11").setValue(String(this.plugin.settings.fontSize)).onChange(async (value) => {
|
|
const num = parseInt(value, 10);
|
|
if (!isNaN(num) && num > 0 && num <= 72) {
|
|
this.plugin.settings.fontSize = num;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Page size").setDesc("PDF page dimensions").addDropdown(
|
|
(dropdown) => dropdown.addOption("A4", "A4").addOption("Letter", "Letter").addOption("Legal", "Legal").addOption("A3", "A3").setValue(this.plugin.settings.pageSize).onChange(async (value) => {
|
|
this.plugin.settings.pageSize = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Page margin").setDesc("Page margin in millimeters").addText(
|
|
(text) => text.setPlaceholder("25").setValue(this.plugin.settings.pageMargin).onChange(async (value) => {
|
|
this.plugin.settings.pageMargin = value || "25";
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Code highlight theme").setDesc("Syntax highlighting theme for code blocks").addDropdown(
|
|
(dropdown) => dropdown.addOption("tango", "Tango (default)").addOption("pygments", "Pygments (minimal background)").addOption("zenburn", "Zenburn").addOption("breezedark", "Breeze Dark").addOption("kate", "Kate").addOption("monochrome", "Monochrome").setValue(this.plugin.settings.codeTheme).onChange(async (value) => {
|
|
this.plugin.settings.codeTheme = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("CJK font").setDesc(
|
|
"Chinese/Japanese/Korean font name. On macOS, STHeitiSC-Medium is a reliable XeLaTeX choice."
|
|
).addText(
|
|
(text) => text.setPlaceholder("STHeitiSC-Medium").setValue(this.plugin.settings.cjkFont).onChange(async (value) => {
|
|
this.plugin.settings.cjkFont = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Enable CJK support").setDesc("Add CJK font configuration for LaTeX engines").addToggle(
|
|
(toggle) => toggle.setValue(this.plugin.settings.enableCjk).onChange(async (value) => {
|
|
this.plugin.settings.enableCjk = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Advanced").setHeading();
|
|
new import_obsidian.Setting(containerEl).setName("Custom CSS file").setDesc(
|
|
"Path to custom CSS file (relative to vault root, for HTML-based engines)"
|
|
).addText(
|
|
(text) => text.setPlaceholder("styles/custom-pdf.css").setValue(this.plugin.settings.customCssPath).onChange(async (value) => {
|
|
this.plugin.settings.customCssPath = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Custom Pandoc template").setDesc("Path to custom Pandoc template file").addText(
|
|
(text) => text.setPlaceholder("templates/custom.html").setValue(this.plugin.settings.customTemplatePath).onChange(async (value) => {
|
|
this.plugin.settings.customTemplatePath = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Mermaid CLI path").setDesc("Path to mmdc binary for Mermaid diagram rendering").addText(
|
|
(text) => text.setPlaceholder("mmdc").setValue(this.plugin.settings.mermaidPath).onChange(async (value) => {
|
|
this.plugin.settings.mermaidPath = value || "mmdc";
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Mermaid theme").setDesc("Theme for Mermaid diagrams").addDropdown(
|
|
(dropdown) => dropdown.addOption("default", "Default").addOption("dark", "Dark").addOption("forest", "Forest").addOption("neutral", "Neutral").setValue(this.plugin.settings.mermaidTheme).onChange(async (value) => {
|
|
this.plugin.settings.mermaidTheme = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Extra Pandoc arguments").setDesc("Additional command-line arguments passed to Pandoc").addText(
|
|
(text) => text.setPlaceholder("--pdf-engine-opt=--enable-local-file-access").setValue(this.plugin.settings.extraArgs).onChange(async (value) => {
|
|
this.plugin.settings.extraArgs = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Batch Export").setHeading();
|
|
new import_obsidian.Setting(containerEl).setName("Concurrency").setDesc("Number of files to export in parallel").addText(
|
|
(text) => text.setPlaceholder("3").setValue(String(this.plugin.settings.concurrency)).onChange(async (value) => {
|
|
const num = parseInt(value, 10);
|
|
if (!isNaN(num) && num > 0 && num <= 20) {
|
|
this.plugin.settings.concurrency = num;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
})
|
|
);
|
|
new import_obsidian.Setting(containerEl).setName("Skip errors").setDesc("Continue exporting remaining files if one fails").addToggle(
|
|
(toggle) => toggle.setValue(this.plugin.settings.skipErrors).onChange(async (value) => {
|
|
this.plugin.settings.skipErrors = value;
|
|
await this.plugin.saveSettings();
|
|
})
|
|
);
|
|
}
|
|
};
|
|
|
|
// src/exporter.ts
|
|
var import_obsidian3 = require("obsidian");
|
|
var path4 = __toESM(require("path"));
|
|
var fs4 = __toESM(require("fs"));
|
|
var import_child_process3 = require("child_process");
|
|
|
|
// src/renderer.ts
|
|
var import_obsidian2 = require("obsidian");
|
|
|
|
// src/utils.ts
|
|
var import_child_process = require("child_process");
|
|
var path = __toESM(require("path"));
|
|
var fs = __toESM(require("fs"));
|
|
var os = __toESM(require("os"));
|
|
function runCommand(cmd, options) {
|
|
return new Promise((resolve) => {
|
|
(0, import_child_process.exec)(
|
|
cmd,
|
|
{
|
|
timeout: (options == null ? void 0 : options.timeout) || 3e4,
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
// Use login shell so PATH includes Homebrew
|
|
shell: "/bin/zsh",
|
|
env: {
|
|
...process.env,
|
|
PATH: `/Library/TeX/texbin:/opt/homebrew/bin:/usr/local/bin:${process.env.PATH || ""}`
|
|
}
|
|
},
|
|
(error, stdout, stderr) => {
|
|
resolve({
|
|
stdout: stdout || "",
|
|
stderr: stderr || "",
|
|
code: error ? error.code || 1 : 0
|
|
});
|
|
}
|
|
);
|
|
});
|
|
}
|
|
function getVaultPath(app) {
|
|
return app.vault.adapter.basePath;
|
|
}
|
|
function resolveAttachmentPath(src, currentFile, app) {
|
|
var _a, _b;
|
|
const vaultPath = getVaultPath(app);
|
|
if (path.isAbsolute(src)) {
|
|
return src;
|
|
}
|
|
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
return src;
|
|
}
|
|
if (src.startsWith("data:")) {
|
|
return src;
|
|
}
|
|
const currentDir = path.dirname(currentFile.path);
|
|
const relativePath = path.join(currentDir, src);
|
|
const absRelative = path.join(vaultPath, relativePath);
|
|
if (fs.existsSync(absRelative)) {
|
|
return absRelative;
|
|
}
|
|
const absRoot = path.join(vaultPath, src);
|
|
if (fs.existsSync(absRoot)) {
|
|
return absRoot;
|
|
}
|
|
const attachmentFolder = (_b = (_a = app.vault).getConfig) == null ? void 0 : _b.call(_a, "attachmentFolderPath");
|
|
if (attachmentFolder) {
|
|
const absAttachment = path.join(vaultPath, attachmentFolder, src);
|
|
if (fs.existsSync(absAttachment)) {
|
|
return absAttachment;
|
|
}
|
|
}
|
|
return src;
|
|
}
|
|
async function checkCommandExists(cmd) {
|
|
const platform2 = os.platform();
|
|
const checkCmd = platform2 === "win32" ? `where ${cmd}` : `which ${cmd}`;
|
|
const { code } = await runCommand(checkCmd);
|
|
return code === 0;
|
|
}
|
|
function getTmpDir(vaultPath) {
|
|
const tmpDir = path.join(
|
|
vaultPath,
|
|
".obsidian",
|
|
"plugins",
|
|
"obsidian-press",
|
|
"tmp"
|
|
);
|
|
if (!fs.existsSync(tmpDir)) {
|
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
}
|
|
return tmpDir;
|
|
}
|
|
function createSemaphore(limit) {
|
|
let current = 0;
|
|
const queue = [];
|
|
return {
|
|
async acquire() {
|
|
if (current < limit) {
|
|
current++;
|
|
return;
|
|
}
|
|
return new Promise((resolve) => {
|
|
queue.push(resolve);
|
|
});
|
|
},
|
|
release() {
|
|
current--;
|
|
if (queue.length > 0) {
|
|
current++;
|
|
const next = queue.shift();
|
|
next();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
function getOutputPath(file, vaultPath, outputDir, naming, format) {
|
|
const baseName = path.basename(file.path, ".md");
|
|
let fileName;
|
|
switch (naming) {
|
|
case "timestamp":
|
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
fileName = `${baseName}_${ts}.${format}`;
|
|
break;
|
|
case "suffix":
|
|
fileName = `${baseName}_export.${format}`;
|
|
break;
|
|
default:
|
|
fileName = `${baseName}.${format}`;
|
|
}
|
|
let outDir;
|
|
if (path.isAbsolute(outputDir)) {
|
|
outDir = outputDir;
|
|
} else if (outputDir) {
|
|
outDir = path.join(vaultPath, outputDir);
|
|
} else {
|
|
outDir = path.join(vaultPath, "pdf");
|
|
}
|
|
if (!fs.existsSync(outDir)) {
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
}
|
|
return path.join(outDir, fileName);
|
|
}
|
|
|
|
// src/mermaid.ts
|
|
var fs2 = __toESM(require("fs"));
|
|
var path2 = __toESM(require("path"));
|
|
async function renderMermaidBlock(code, theme, tmpDir, index) {
|
|
const mmdcAvailable = await checkCommandExists("mmdc");
|
|
if (!mmdcAvailable) {
|
|
console.warn("Obsidian Press: mmdc not found, skipping Mermaid rendering");
|
|
return null;
|
|
}
|
|
const inputFile = path2.join(tmpDir, `mermaid-input-${index}.mmd`);
|
|
const outputFile = path2.join(tmpDir, `mermaid-output-${index}.svg`);
|
|
try {
|
|
fs2.writeFileSync(inputFile, code, "utf8");
|
|
const cmd = `mmdc -i '${inputFile}' -o '${outputFile}' -t ${theme} -b transparent --quiet`;
|
|
const { code: exitCode, stderr } = await runCommand(cmd, {
|
|
timeout: 3e4
|
|
});
|
|
if (exitCode === 0 && fs2.existsSync(outputFile)) {
|
|
return outputFile;
|
|
}
|
|
console.warn("Obsidian Press: Mermaid rendering failed:", stderr);
|
|
return null;
|
|
} catch (err) {
|
|
console.error("Obsidian Press: Mermaid rendering error:", err);
|
|
return null;
|
|
} finally {
|
|
try {
|
|
if (fs2.existsSync(inputFile)) {
|
|
fs2.unlinkSync(inputFile);
|
|
}
|
|
} catch (e) {
|
|
}
|
|
}
|
|
}
|
|
|
|
// src/renderer.ts
|
|
var CALLOUT_TYPES = [
|
|
"note",
|
|
"tip",
|
|
"important",
|
|
"warning",
|
|
"caution",
|
|
"abstract",
|
|
"info",
|
|
"todo",
|
|
"example",
|
|
"quote",
|
|
"success",
|
|
"question",
|
|
"failure",
|
|
"danger",
|
|
"bug"
|
|
];
|
|
var CALLOUT_ICONS = {
|
|
note: "\u{1F4DD}",
|
|
tip: "\u{1F4A1}",
|
|
important: "\u2757",
|
|
warning: "\u26A0\uFE0F",
|
|
caution: "\u26A0\uFE0F",
|
|
abstract: "\u{1F4CB}",
|
|
info: "\u2139\uFE0F",
|
|
todo: "\u2611",
|
|
example: "\u{1F4DA}",
|
|
quote: "\u275D",
|
|
success: "\u2705",
|
|
question: "\u2753",
|
|
failure: "\u274C",
|
|
danger: "\u{1F6A8}",
|
|
bug: "\u{1F41B}"
|
|
};
|
|
async function renderToPandoc(content, file, app, mermaidPath, mermaidTheme) {
|
|
const vaultPath = getVaultPath(app);
|
|
const tmpDir = getTmpDir(vaultPath);
|
|
const tempFiles = [];
|
|
let rendered = stripFrontmatter(content);
|
|
rendered = formatFlattenedCodeBlocks(rendered);
|
|
rendered = await convertMermaidBlocks(
|
|
rendered,
|
|
mermaidPath,
|
|
mermaidTheme,
|
|
tmpDir,
|
|
tempFiles
|
|
);
|
|
const protectedCode = protectCodeSegments(rendered);
|
|
rendered = protectedCode.content;
|
|
rendered = convertCallouts(rendered);
|
|
rendered = convertWikilinks(rendered, file, app);
|
|
rendered = await convertEmbeds(rendered, file, app);
|
|
rendered = await inlineNoteEmbeds(rendered, file, app, 0, 5);
|
|
rendered = convertHighlights(rendered);
|
|
rendered = convertSupSub(rendered);
|
|
rendered = stripComments(rendered);
|
|
rendered = convertImageSizes(rendered);
|
|
rendered = restoreCodeSegments(rendered, protectedCode.segments);
|
|
return { content: rendered, tempFiles };
|
|
}
|
|
function formatFlattenedCodeBlocks(content) {
|
|
return content.replace(
|
|
/^(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)^\1[ \t]*$/gm,
|
|
(match, fence, info, code) => {
|
|
var _a;
|
|
const language = ((_a = info.trim().split(/\s+/)[0]) == null ? void 0 : _a.toLowerCase()) || "";
|
|
if (!shouldFormatFlattenedCode(language, code)) {
|
|
return match;
|
|
}
|
|
const formatted = formatJavaScriptLikeCode(code);
|
|
return `${fence}${info}
|
|
${formatted}
|
|
${fence}`;
|
|
}
|
|
);
|
|
}
|
|
function shouldFormatFlattenedCode(language, code) {
|
|
const supportedLanguages = /* @__PURE__ */ new Set([
|
|
"",
|
|
"js",
|
|
"javascript",
|
|
"jsx",
|
|
"ts",
|
|
"typescript",
|
|
"tsx",
|
|
"php"
|
|
]);
|
|
if (!supportedLanguages.has(language)) {
|
|
return false;
|
|
}
|
|
const trimmed = code.trim();
|
|
if (trimmed.length < 160) {
|
|
return false;
|
|
}
|
|
const nonEmptyLines = trimmed.split(/\r?\n/).filter((line) => line.trim());
|
|
if (nonEmptyLines.length > 2) {
|
|
return false;
|
|
}
|
|
return /[{};]/.test(trimmed) && /\b(const|let|var|function|class|async|while|if|return|await|new)\b/.test(trimmed);
|
|
}
|
|
function formatJavaScriptLikeCode(code) {
|
|
const { text, literals } = protectStringLiterals(code.trim());
|
|
let formatted = text.replace(/\r?\n/g, " ").replace(/[ \t]{2,}/g, " ").replace(/\}\s*else\s*\{/g, "}\nelse {").replace(/\}\s*catch\s*\(/g, "}\ncatch (").replace(/\}\s*finally\s*\{/g, "}\nfinally {").replace(/\s*\{\s*/g, " {\n").replace(/;\s*/g, ";\n").replace(/,\s*/g, ",\n").replace(/\s*\}\s*/g, "\n}\n").replace(/\)\s*(?=(?:async\s+)?(?:function|class|const|let|var|if|for|while|return|await|new|this\.))/g, ")\n").replace(/\n{2,}/g, "\n");
|
|
formatted = restoreStringLiterals(formatted, literals);
|
|
return indentFormattedCode(formatted);
|
|
}
|
|
function protectStringLiterals(code) {
|
|
const literals = [];
|
|
let text = "";
|
|
let i = 0;
|
|
while (i < code.length) {
|
|
const char = code[i];
|
|
if (char !== "'" && char !== '"' && char !== "`") {
|
|
text += char;
|
|
i++;
|
|
continue;
|
|
}
|
|
const quote = char;
|
|
let literal = char;
|
|
i++;
|
|
while (i < code.length) {
|
|
const next = code[i];
|
|
literal += next;
|
|
i++;
|
|
if (next === "\\") {
|
|
if (i < code.length) {
|
|
literal += code[i];
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
if (next === quote) {
|
|
break;
|
|
}
|
|
}
|
|
const token = `__OBSIDIAN_PRESS_LITERAL_${literals.length}__`;
|
|
literals.push(literal);
|
|
text += token;
|
|
}
|
|
return { text, literals };
|
|
}
|
|
function restoreStringLiterals(text, literals) {
|
|
return literals.reduce(
|
|
(result, literal, index) => result.split(`__OBSIDIAN_PRESS_LITERAL_${index}__`).join(literal),
|
|
text
|
|
);
|
|
}
|
|
function indentFormattedCode(code) {
|
|
const lines = code.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
const output = [];
|
|
let level = 0;
|
|
for (const line of lines) {
|
|
if (/^[}\])]/.test(line)) {
|
|
level = Math.max(0, level - 1);
|
|
}
|
|
output.push(`${" ".repeat(level)}${line}`);
|
|
const opens = (line.match(/[{\[(]/g) || []).length;
|
|
const closes = (line.match(/[}\])]/g) || []).length;
|
|
level = Math.max(0, level + opens - closes);
|
|
}
|
|
return output.join("\n");
|
|
}
|
|
function protectCodeSegments(content) {
|
|
const segments = /* @__PURE__ */ new Map();
|
|
let index = 0;
|
|
const store = (value) => {
|
|
const token = `OBSIDIAN_PRESS_CODE_${index++}`;
|
|
segments.set(token, value);
|
|
return token;
|
|
};
|
|
let result = content.replace(
|
|
/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1[ \t]*$/gm,
|
|
(match) => store(match)
|
|
);
|
|
result = result.replace(/(`+)([\s\S]*?[^`])\1/g, (match) => store(match));
|
|
return { content: result, segments };
|
|
}
|
|
function restoreCodeSegments(content, segments) {
|
|
let result = content;
|
|
for (const [token, value] of segments) {
|
|
result = result.split(token).join(value);
|
|
}
|
|
return result;
|
|
}
|
|
function stripFrontmatter(content) {
|
|
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n?/;
|
|
const match = content.match(frontmatterRegex);
|
|
if (!match) return content;
|
|
const yamlContent = match[1];
|
|
const rest = content.slice(match[0].length);
|
|
const titleMatch = yamlContent.match(/^title:\s*(.+)$/m);
|
|
if (titleMatch) {
|
|
const title = titleMatch[1].replace(/^["']|["']$/g, "");
|
|
return `# ${title}
|
|
|
|
${rest}`;
|
|
}
|
|
return rest;
|
|
}
|
|
function convertCallouts(content) {
|
|
const calloutRegex = /^(>\s*\[!([a-zA-Z]+)\](\+|-)?\s*(.*)?\n(?:>\s*.*\n?)*)/gm;
|
|
return content.replace(
|
|
calloutRegex,
|
|
(match, _full, type, _collapse, title) => {
|
|
const calloutType = type.toLowerCase();
|
|
const isValidType = CALLOUT_TYPES.includes(calloutType);
|
|
const cssType = isValidType ? calloutType : "note";
|
|
const icon = CALLOUT_ICONS[cssType] || "\u{1F4DD}";
|
|
const displayTitle = (title || cssType).trim();
|
|
const lines = match.split("\n");
|
|
const bodyLines = lines.map((line) => line.replace(/^>\s?/, "")).filter((line, i) => {
|
|
if (i === 0) return false;
|
|
return true;
|
|
});
|
|
const body = bodyLines.join("\n").trim();
|
|
return `<div class="callout callout-${cssType}">
|
|
<div class="callout-title">
|
|
${icon} ${displayTitle}
|
|
</div>
|
|
<div class="callout-body">
|
|
|
|
${body}
|
|
|
|
</div>
|
|
</div>`;
|
|
}
|
|
);
|
|
}
|
|
function convertWikilinks(content, file, app) {
|
|
return content.replace(
|
|
/\[\[([^\]|]+?)(?:\|([^\]]*?))?\]\]/g,
|
|
(_match, target, alias) => {
|
|
const displayText = alias || target;
|
|
if (/\.(png|jpg|jpeg|gif|svg|webp|bmp|ico)$/i.test(target)) {
|
|
return displayText;
|
|
}
|
|
const resolvedFile = app.metadataCache.getFirstLinkpathDest(
|
|
target,
|
|
file.path
|
|
);
|
|
if (resolvedFile) {
|
|
const relativePath = getRelativePath(file.path, resolvedFile.path);
|
|
return `[${displayText}](${relativePath})`;
|
|
}
|
|
return `[${displayText}](${target}.md)`;
|
|
}
|
|
);
|
|
}
|
|
async function convertEmbeds(content, file, app) {
|
|
const embedRegex = /!\[\[([^\]|]+?)(?:\|(\d+))?\]\]/g;
|
|
let result = content;
|
|
let match;
|
|
while ((match = embedRegex.exec(content)) !== null) {
|
|
const [fullMatch, src, size] = match;
|
|
if (/\.(png|jpg|jpeg|gif|svg|webp|bmp|ico)$/i.test(src)) {
|
|
const absPath = resolveAttachmentPath(src, file, app);
|
|
const sizeAttr = size ? ` width="${size}"` : "";
|
|
const replacement = `${sizeAttr ? "{width=" + size + "}" : ""}`;
|
|
result = result.replace(fullMatch, replacement);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
async function inlineNoteEmbeds(content, currentFile, app, depth, maxDepth) {
|
|
if (depth >= maxDepth) return content;
|
|
const embedRegex = /!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\]/g;
|
|
let result = content;
|
|
let match;
|
|
const contentSnapshot = result;
|
|
while ((match = embedRegex.exec(contentSnapshot)) !== null) {
|
|
const [fullMatch, target] = match;
|
|
if (/\.(png|jpg|jpeg|gif|svg|webp|bmp|ico)$/i.test(target)) {
|
|
continue;
|
|
}
|
|
const resolvedFile = app.metadataCache.getFirstLinkpathDest(
|
|
target,
|
|
currentFile.path
|
|
);
|
|
if (resolvedFile instanceof import_obsidian2.TFile && resolvedFile.extension === "md") {
|
|
const embedContent = await app.vault.read(resolvedFile);
|
|
const processed = await inlineNoteEmbeds(
|
|
embedContent,
|
|
resolvedFile,
|
|
app,
|
|
depth + 1,
|
|
maxDepth
|
|
);
|
|
result = result.replace(
|
|
fullMatch,
|
|
`
|
|
|
|
<!-- Embedded: ${target} -->
|
|
|
|
${processed}
|
|
|
|
<!-- End embed: ${target} -->
|
|
|
|
`
|
|
);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function convertHighlights(content) {
|
|
return content.replace(/==([^=]+)==/g, "<mark>$1</mark>");
|
|
}
|
|
function convertSupSub(content) {
|
|
let result = content.replace(/\^([^\^]+)\^/g, "<sup>$1</sup>");
|
|
result = result.replace(/~~([^~]+)~~/g, "<sub>$1</sub>");
|
|
return result;
|
|
}
|
|
function stripComments(content) {
|
|
return content.replace(/%%[\s\S]*?%%/g, "");
|
|
}
|
|
function convertImageSizes(content) {
|
|
return content.replace(
|
|
/!\[([^\]]*?)\|(\d+)\]\(([^)]+)\)/g,
|
|
(_match, alt, size, url) => {
|
|
return `{width=${size}}`;
|
|
}
|
|
);
|
|
}
|
|
async function convertMermaidBlocks(content, mermaidPath, mermaidTheme, tmpDir, tempFiles) {
|
|
const mermaidRegex = /```mermaid\n([\s\S]*?)```/g;
|
|
let result = content;
|
|
let match;
|
|
let index = 0;
|
|
const contentSnapshot = content;
|
|
while ((match = mermaidRegex.exec(contentSnapshot)) !== null) {
|
|
const [fullMatch, code] = match;
|
|
index++;
|
|
try {
|
|
const svgPath = await renderMermaidBlock(
|
|
code.trim(),
|
|
mermaidTheme,
|
|
tmpDir,
|
|
index
|
|
);
|
|
if (svgPath) {
|
|
tempFiles.push(svgPath);
|
|
result = result.replace(
|
|
fullMatch,
|
|
``
|
|
);
|
|
} else {
|
|
result = result.replace(
|
|
fullMatch,
|
|
"```mermaid\n" + code + "```"
|
|
);
|
|
}
|
|
} catch (e) {
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function getRelativePath(from, to) {
|
|
const fromDir = from.substring(0, from.lastIndexOf("/"));
|
|
let relative = "";
|
|
const fromParts = fromDir.split("/");
|
|
const toParts = to.split("/");
|
|
let commonLength = 0;
|
|
while (commonLength < fromParts.length && commonLength < toParts.length && fromParts[commonLength] === toParts[commonLength]) {
|
|
commonLength++;
|
|
}
|
|
for (let i = commonLength; i < fromParts.length; i++) {
|
|
relative += "../";
|
|
}
|
|
for (let i = commonLength; i < toParts.length; i++) {
|
|
relative += toParts[i];
|
|
if (i < toParts.length - 1) relative += "/";
|
|
}
|
|
return relative || to;
|
|
}
|
|
|
|
// src/pandoc.ts
|
|
var path3 = __toESM(require("path"));
|
|
var fs3 = __toESM(require("fs"));
|
|
var import_child_process2 = require("child_process");
|
|
function buildPandocArgs(options) {
|
|
const {
|
|
inputPath,
|
|
outputPath,
|
|
format,
|
|
engine,
|
|
fontSize,
|
|
pageSize,
|
|
pageMargin,
|
|
codeTheme,
|
|
cjkFont,
|
|
enableCjk,
|
|
customCssPath,
|
|
customTemplatePath,
|
|
extraArgs
|
|
} = options;
|
|
const listingsHeaderPath = path3.join(options.tempDir, "obsidian-press-listings.tex");
|
|
const args = [
|
|
inputPath,
|
|
"-o",
|
|
outputPath,
|
|
"--from",
|
|
"markdown+fenced_code_blocks+fenced_code_attributes+backtick_code_blocks+pipe_tables+grid_tables+raw_html+tex_math_dollars",
|
|
"--to",
|
|
format === "pdf" ? "pdf" : format === "docx" ? "docx" : "html5",
|
|
"--standalone",
|
|
"--toc",
|
|
"--toc-depth=3",
|
|
`--highlight-style=${codeTheme}`,
|
|
"--resource-path",
|
|
path3.dirname(inputPath)
|
|
];
|
|
const latexBase = [
|
|
"-V",
|
|
`geometry:margin=${pageMargin}mm`,
|
|
"-V",
|
|
`fontsize=${fontSize}pt`,
|
|
"-V",
|
|
`papersize=${pageSize.toLowerCase()}`
|
|
];
|
|
switch (engine) {
|
|
case "xelatex":
|
|
args.push(
|
|
"--pdf-engine=xelatex",
|
|
"--listings",
|
|
"-H",
|
|
listingsHeaderPath,
|
|
...latexBase,
|
|
...getCjkArgs(engine, enableCjk, cjkFont),
|
|
"-V",
|
|
"colorlinks=true"
|
|
);
|
|
break;
|
|
case "pdflatex":
|
|
args.push(
|
|
"--pdf-engine=pdflatex",
|
|
"--listings",
|
|
"-H",
|
|
listingsHeaderPath,
|
|
...latexBase,
|
|
"-V",
|
|
"colorlinks=true"
|
|
);
|
|
break;
|
|
case "lualatex":
|
|
args.push(
|
|
"--pdf-engine=lualatex",
|
|
"--listings",
|
|
"-H",
|
|
listingsHeaderPath,
|
|
...latexBase,
|
|
...getCjkArgs(engine, enableCjk, cjkFont),
|
|
"-V",
|
|
"colorlinks=true"
|
|
);
|
|
break;
|
|
case "wkhtmltopdf":
|
|
args.push(
|
|
"--pdf-engine=wkhtmltopdf",
|
|
"-V",
|
|
`margin-top=${pageMargin}mm`,
|
|
"-V",
|
|
`margin-bottom=${pageMargin}mm`,
|
|
"-V",
|
|
`margin-left=${pageMargin}mm`,
|
|
"-V",
|
|
`margin-right=${pageMargin}mm`
|
|
);
|
|
break;
|
|
case "weasyprint":
|
|
args.push("--pdf-engine=weasyprint", "-V", `margin=${pageMargin}mm`);
|
|
break;
|
|
case "typst":
|
|
args.push(
|
|
"--pdf-engine=typst",
|
|
"-V",
|
|
`font-size=${fontSize}pt`,
|
|
"-V",
|
|
`page-size=${pageSize.toLowerCase()}`
|
|
);
|
|
break;
|
|
}
|
|
if ((engine === "wkhtmltopdf" || engine === "weasyprint") && customCssPath) {
|
|
args.push("--css", customCssPath);
|
|
}
|
|
if (customTemplatePath) {
|
|
args.push("--template", customTemplatePath);
|
|
}
|
|
if (extraArgs.length > 0) {
|
|
args.push(...extraArgs);
|
|
}
|
|
return args;
|
|
}
|
|
function normalizeCjkFontName(fontName) {
|
|
const trimmed = fontName.trim();
|
|
if (process.platform === "darwin" && trimmed === "PingFang SC") {
|
|
return "STHeitiSC-Medium";
|
|
}
|
|
return trimmed;
|
|
}
|
|
function getCjkArgs(engine, enableCjk, cjkFont) {
|
|
if (!enableCjk) return [];
|
|
const resolved = normalizeCjkFontName(cjkFont);
|
|
if (resolved) {
|
|
return ["-V", `CJKmainfont=${resolved}`];
|
|
}
|
|
if (process.platform === "darwin" && engine === "xelatex") {
|
|
return ["-V", "CJKmainfont=STHeitiSC-Medium"];
|
|
}
|
|
return [];
|
|
}
|
|
async function exportWithPandoc(options) {
|
|
const startTime = Date.now();
|
|
const pandocPath = options.pandocPath || "pandoc";
|
|
if (!fs3.existsSync(options.inputPath)) {
|
|
return {
|
|
success: false,
|
|
error: `Input file not found: ${options.inputPath}`,
|
|
duration: Date.now() - startTime
|
|
};
|
|
}
|
|
const outputDir = path3.dirname(options.outputPath);
|
|
if (!fs3.existsSync(outputDir)) {
|
|
fs3.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
if (!fs3.existsSync(options.tempDir)) {
|
|
fs3.mkdirSync(options.tempDir, { recursive: true });
|
|
}
|
|
writeListingsHeader(options.tempDir);
|
|
const texCacheDir = path3.join(options.tempDir, "tex-cache");
|
|
if (!fs3.existsSync(texCacheDir)) {
|
|
fs3.mkdirSync(texCacheDir, { recursive: true });
|
|
}
|
|
const args = buildPandocArgs(options);
|
|
return new Promise((resolve) => {
|
|
var _a, _b;
|
|
const child = (0, import_child_process2.spawn)(pandocPath, args, {
|
|
shell: false,
|
|
cwd: options.tempDir,
|
|
stdio: "pipe",
|
|
timeout: 12e4,
|
|
env: {
|
|
...process.env,
|
|
PATH: `/Library/TeX/texbin:/opt/homebrew/bin:/usr/local/bin:${process.env.PATH || ""}`,
|
|
TMPDIR: options.tempDir,
|
|
TMP: options.tempDir,
|
|
TEMP: options.tempDir,
|
|
TEXMFVAR: texCacheDir,
|
|
TEXMFCACHE: texCacheDir,
|
|
LANG: process.env.LANG || "en_US.UTF-8",
|
|
LC_ALL: process.env.LC_ALL || "en_US.UTF-8"
|
|
}
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
(_a = child.stdout) == null ? void 0 : _a.on("data", (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
(_b = child.stderr) == null ? void 0 : _b.on("data", (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
child.on("close", (code) => {
|
|
const duration = Date.now() - startTime;
|
|
if (code === 0) {
|
|
if (fs3.existsSync(options.outputPath)) {
|
|
const stats = fs3.statSync(options.outputPath);
|
|
if (stats.size > 0) {
|
|
resolve({
|
|
success: true,
|
|
outputPath: options.outputPath,
|
|
duration
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
resolve({
|
|
success: false,
|
|
error: "Output file was not created or is empty",
|
|
duration
|
|
});
|
|
return;
|
|
}
|
|
const errorMsg = parsePandocError(stderr, code);
|
|
resolve({ success: false, error: errorMsg, duration });
|
|
});
|
|
child.on("error", (err) => {
|
|
resolve({
|
|
success: false,
|
|
error: `Failed to run pandoc: ${err.message}`,
|
|
duration: Date.now() - startTime
|
|
});
|
|
});
|
|
});
|
|
}
|
|
function writeListingsHeader(tempDir) {
|
|
const headerPath = path3.join(tempDir, "obsidian-press-listings.tex");
|
|
const content = String.raw`\lstset{
|
|
breaklines=true,
|
|
breakatwhitespace=false,
|
|
columns=fullflexible,
|
|
keepspaces=true,
|
|
basicstyle=\ttfamily\footnotesize,
|
|
frame=single,
|
|
framerule=0.3pt,
|
|
rulecolor=\color[HTML]{D0D7DE},
|
|
backgroundcolor=\color[HTML]{F6F8FA},
|
|
xleftmargin=0.5em,
|
|
xrightmargin=0.5em,
|
|
aboveskip=0.8em,
|
|
belowskip=0.8em
|
|
}
|
|
`;
|
|
fs3.writeFileSync(headerPath, content, "utf8");
|
|
}
|
|
function parsePandocError(stderr, code) {
|
|
if (!stderr) return `Pandoc exited with code ${code}`;
|
|
const lines = stderr.split("\n").filter((line) => line.trim());
|
|
const latexErrorIndex = lines.findIndex((line) => /^! /.test(line.trim()));
|
|
if (latexErrorIndex >= 0) {
|
|
return lines.slice(latexErrorIndex, latexErrorIndex + 5).join("\n").substring(0, 800);
|
|
}
|
|
const errorPatterns = [
|
|
/Package .* Error/,
|
|
/LaTeX Error/,
|
|
/error:/i,
|
|
/not found/i,
|
|
/failed/i,
|
|
/cannot/i,
|
|
/unable/i,
|
|
/does not exist/i
|
|
];
|
|
for (const pattern of errorPatterns) {
|
|
const errorLine = lines.find((line) => pattern.test(line));
|
|
if (errorLine) return errorLine.trim();
|
|
}
|
|
return lines.slice(0, 3).join("; ").substring(0, 500);
|
|
}
|
|
async function checkPandocAvailable(pandocPath) {
|
|
return new Promise((resolve) => {
|
|
var _a;
|
|
const child = (0, import_child_process2.spawn)(pandocPath, ["--version"], {
|
|
shell: false,
|
|
stdio: "pipe",
|
|
env: {
|
|
...process.env,
|
|
PATH: `/Library/TeX/texbin:/opt/homebrew/bin:/usr/local/bin:${process.env.PATH || ""}`
|
|
}
|
|
});
|
|
let stdout = "";
|
|
(_a = child.stdout) == null ? void 0 : _a.on("data", (data) => {
|
|
stdout += data.toString();
|
|
});
|
|
child.on("close", (code) => {
|
|
if (code === 0) {
|
|
const versionMatch = stdout.match(/pandoc\s+(\d+\.\d+[\.\d]*)/);
|
|
resolve({
|
|
available: true,
|
|
version: versionMatch ? versionMatch[1] : "unknown"
|
|
});
|
|
} else {
|
|
resolve({ available: false });
|
|
}
|
|
});
|
|
child.on("error", () => {
|
|
resolve({ available: false });
|
|
});
|
|
});
|
|
}
|
|
|
|
// src/exporter.ts
|
|
async function exportFile(file, app, settings) {
|
|
const vaultPath = getVaultPath(app);
|
|
const tmpDir = getTmpDir(vaultPath);
|
|
try {
|
|
const content = await app.vault.read(file);
|
|
const effectivePdfEngine = settings.defaultFormat === "pdf" && settings.pdfEngine === "pdflatex" && containsCjk(content) ? "xelatex" : settings.pdfEngine;
|
|
const format = settings.defaultFormat;
|
|
const outputPath = getOutputPath(
|
|
file,
|
|
vaultPath,
|
|
settings.outputDir,
|
|
settings.outputNaming,
|
|
format
|
|
);
|
|
const rendered = await renderToPandoc(
|
|
content,
|
|
file,
|
|
app,
|
|
settings.mermaidPath,
|
|
settings.mermaidTheme
|
|
);
|
|
if (settings.defaultFormat === "pdf") {
|
|
rendered.content = normalizeRemoteImageUrls(rendered.content);
|
|
}
|
|
if (settings.defaultFormat === "pdf" && effectivePdfEngine === "typst") {
|
|
rendered.content = await localizeImagesForTypst(
|
|
rendered.content,
|
|
file,
|
|
vaultPath,
|
|
tmpDir,
|
|
rendered.tempFiles
|
|
);
|
|
}
|
|
if (settings.defaultFormat === "pdf" && isLatexEngine(effectivePdfEngine)) {
|
|
rendered.content = await convertWebpImagesForLatex(
|
|
rendered.content,
|
|
file,
|
|
vaultPath,
|
|
tmpDir,
|
|
rendered.tempFiles
|
|
);
|
|
}
|
|
const tempMdPath = path4.join(
|
|
tmpDir,
|
|
`press-${Date.now()}-${path4.basename(file.path)}`
|
|
);
|
|
fs4.writeFileSync(tempMdPath, rendered.content, "utf8");
|
|
const pandocOptions = {
|
|
inputPath: tempMdPath,
|
|
outputPath,
|
|
format,
|
|
engine: effectivePdfEngine,
|
|
pandocPath: settings.pandocPath,
|
|
tempDir: tmpDir,
|
|
fontSize: settings.fontSize,
|
|
pageSize: settings.pageSize,
|
|
pageMargin: settings.pageMargin,
|
|
codeTheme: settings.codeTheme,
|
|
cjkFont: settings.cjkFont,
|
|
enableCjk: settings.enableCjk,
|
|
customCssPath: settings.customCssPath || void 0,
|
|
customTemplatePath: settings.customTemplatePath || void 0,
|
|
extraArgs: settings.extraArgs ? settings.extraArgs.split(/\s+/).filter(Boolean) : []
|
|
};
|
|
const result = await exportWithPandoc(pandocOptions);
|
|
try {
|
|
fs4.unlinkSync(tempMdPath);
|
|
} catch (e) {
|
|
}
|
|
for (const tempFile of rendered.tempFiles) {
|
|
try {
|
|
if (fs4.existsSync(tempFile)) {
|
|
fs4.unlinkSync(tempFile);
|
|
}
|
|
} catch (e) {
|
|
}
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
duration: 0
|
|
};
|
|
}
|
|
}
|
|
async function exportFolder(folder, app, settings, onProgress) {
|
|
const startTime = Date.now();
|
|
const mdFiles = [];
|
|
collectMarkdownFiles(folder, mdFiles);
|
|
if (mdFiles.length === 0) {
|
|
return {
|
|
total: 0,
|
|
success: 0,
|
|
failed: 0,
|
|
errors: [],
|
|
outputDir: "",
|
|
duration: Date.now() - startTime
|
|
};
|
|
}
|
|
const result = await exportBatch(mdFiles, app, settings, onProgress);
|
|
return {
|
|
...result,
|
|
duration: Date.now() - startTime
|
|
};
|
|
}
|
|
async function exportVault(app, settings, onProgress) {
|
|
const startTime = Date.now();
|
|
const mdFiles = app.vault.getMarkdownFiles();
|
|
if (mdFiles.length === 0) {
|
|
return {
|
|
total: 0,
|
|
success: 0,
|
|
failed: 0,
|
|
errors: [],
|
|
outputDir: "",
|
|
duration: Date.now() - startTime
|
|
};
|
|
}
|
|
const result = await exportBatch(mdFiles, app, settings, onProgress);
|
|
return {
|
|
...result,
|
|
duration: Date.now() - startTime
|
|
};
|
|
}
|
|
async function exportBatch(files, app, settings, onProgress) {
|
|
const total = files.length;
|
|
let success = 0;
|
|
let failed = 0;
|
|
const errors = [];
|
|
const semaphore = createSemaphore(settings.concurrency);
|
|
let done = 0;
|
|
const promises = files.map(async (file) => {
|
|
await semaphore.acquire();
|
|
try {
|
|
const result = await exportFile(file, app, settings);
|
|
done++;
|
|
onProgress == null ? void 0 : onProgress(done, total, file.name);
|
|
if (result.success) {
|
|
success++;
|
|
} else {
|
|
failed++;
|
|
errors.push({
|
|
file: file.path,
|
|
error: result.error || "Unknown error"
|
|
});
|
|
if (!settings.skipErrors) {
|
|
throw new Error(`Export failed for ${file.path}: ${result.error}`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
failed++;
|
|
errors.push({
|
|
file: file.path,
|
|
error: err instanceof Error ? err.message : String(err)
|
|
});
|
|
if (!settings.skipErrors) {
|
|
throw err;
|
|
}
|
|
} finally {
|
|
semaphore.release();
|
|
}
|
|
});
|
|
if (settings.skipErrors) {
|
|
await Promise.allSettled(promises);
|
|
} else {
|
|
await Promise.all(promises);
|
|
}
|
|
const vaultPath = getVaultPath(app);
|
|
const outputDir = path4.isAbsolute(settings.outputDir) ? settings.outputDir : path4.join(vaultPath, settings.outputDir || "pdf");
|
|
return {
|
|
total,
|
|
success,
|
|
failed,
|
|
errors,
|
|
outputDir
|
|
};
|
|
}
|
|
function collectMarkdownFiles(folder, files) {
|
|
for (const child of folder.children) {
|
|
if (child instanceof import_obsidian3.TFile && child.extension === "md") {
|
|
files.push(child);
|
|
} else if (child instanceof import_obsidian3.TFolder) {
|
|
collectMarkdownFiles(child, files);
|
|
}
|
|
}
|
|
}
|
|
function isLatexEngine(engine) {
|
|
return engine === "xelatex" || engine === "pdflatex" || engine === "lualatex";
|
|
}
|
|
function containsCjk(content) {
|
|
return /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3040-\u30ff\uac00-\ud7af]/u.test(
|
|
content
|
|
);
|
|
}
|
|
function normalizeRemoteImageUrls(content) {
|
|
return content.replace(
|
|
/(https?:\/\/[^\s)"'>]+[?&])tp=webp(&?)/gi,
|
|
(_match, prefix, suffix) => {
|
|
if (suffix) return prefix;
|
|
return prefix.endsWith("?") ? prefix.slice(0, -1) : prefix.slice(0, -1);
|
|
}
|
|
);
|
|
}
|
|
async function localizeImagesForTypst(content, file, vaultPath, tmpDir, tempFiles) {
|
|
const references = collectImageReferences(content);
|
|
const replacements = [];
|
|
const localizedBySource = /* @__PURE__ */ new Map();
|
|
let index = 0;
|
|
for (const rawPath of references) {
|
|
const normalizedPath = stripMarkdownUrlDelimiters(rawPath);
|
|
if (/^data:/i.test(normalizedPath)) {
|
|
continue;
|
|
}
|
|
const sourceKey = normalizedPath;
|
|
let localPath = localizedBySource.get(sourceKey);
|
|
if (!localPath) {
|
|
localPath = /^https?:/i.test(normalizedPath) ? await downloadRemoteImageForTypst(normalizedPath, tmpDir, index++) : await copyLocalImageForTypst(
|
|
normalizedPath,
|
|
file,
|
|
vaultPath,
|
|
tmpDir,
|
|
index++
|
|
);
|
|
if (!localPath) {
|
|
continue;
|
|
}
|
|
localizedBySource.set(sourceKey, localPath);
|
|
tempFiles.push(localPath);
|
|
}
|
|
replacements.push({
|
|
original: rawPath,
|
|
converted: `<./${path4.basename(localPath)}>`
|
|
});
|
|
}
|
|
let result = content;
|
|
for (const replacement of replacements) {
|
|
result = result.split(replacement.original).join(replacement.converted);
|
|
}
|
|
return result;
|
|
}
|
|
async function convertWebpImagesForLatex(content, file, vaultPath, tmpDir, tempFiles) {
|
|
const references = collectImageReferences(content);
|
|
const replacements = [];
|
|
const convertedBySource = /* @__PURE__ */ new Map();
|
|
let index = 0;
|
|
for (const rawPath of references) {
|
|
const normalizedPath = stripMarkdownUrlDelimiters(rawPath);
|
|
if (/^data:/i.test(normalizedPath)) {
|
|
continue;
|
|
}
|
|
const resolvedPath = /^https?:/i.test(normalizedPath) ? await downloadRemoteWebpIfNeeded(normalizedPath, tmpDir, index) : isWebpReference(normalizedPath) ? resolveWebpReference(normalizedPath, file, vaultPath) : null;
|
|
if (!resolvedPath) {
|
|
continue;
|
|
}
|
|
if (resolvedPath.startsWith(path4.join(tmpDir, "remote-webp-"))) {
|
|
tempFiles.push(resolvedPath);
|
|
}
|
|
let outputPath = convertedBySource.get(resolvedPath);
|
|
if (!outputPath) {
|
|
outputPath = path4.join(
|
|
tmpDir,
|
|
`webp-${Date.now()}-${index++}.png`
|
|
);
|
|
await convertWebpToPng(resolvedPath, outputPath);
|
|
convertedBySource.set(resolvedPath, outputPath);
|
|
tempFiles.push(outputPath);
|
|
}
|
|
replacements.push({
|
|
original: rawPath,
|
|
converted: `<${outputPath}>`
|
|
});
|
|
}
|
|
let result = content;
|
|
for (const replacement of replacements) {
|
|
result = result.split(replacement.original).join(replacement.converted);
|
|
}
|
|
return result;
|
|
}
|
|
function collectImageReferences(content) {
|
|
const references = /* @__PURE__ */ new Set();
|
|
const markdownImageRegex = /!\[[^\]]*\]\(\s*(<[^>]+>|[^)]+?)(?:\s+["'][^"']*["'])?\s*\)(?:\{[^}]*\})?/gi;
|
|
const htmlImageRegex = /<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi;
|
|
const remoteImageUrlRegex = /https?:\/\/[^\s<>"']+/gi;
|
|
let match;
|
|
while ((match = markdownImageRegex.exec(content)) !== null) {
|
|
const reference = match[1].trim();
|
|
references.add(reference);
|
|
}
|
|
while ((match = htmlImageRegex.exec(content)) !== null) {
|
|
const reference = match[1].trim();
|
|
references.add(reference);
|
|
}
|
|
while ((match = remoteImageUrlRegex.exec(content)) !== null) {
|
|
const reference = trimUrlDelimiters(match[0]);
|
|
if (isLikelyImageUrl(reference)) {
|
|
references.add(reference);
|
|
}
|
|
}
|
|
return Array.from(references);
|
|
}
|
|
function trimUrlDelimiters(value) {
|
|
return value.replace(/[)\]}|.,;:]+$/g, "");
|
|
}
|
|
function isLikelyImageUrl(value) {
|
|
const url = stripMarkdownUrlDelimiters(value);
|
|
if (/\.(png|jpe?g|gif|svg|webp|bmp|ico)(?:[?#].*)?$/i.test(url)) {
|
|
return true;
|
|
}
|
|
try {
|
|
const parsed = new URL(url);
|
|
const pathname = parsed.pathname.toLowerCase();
|
|
if (/\.(png|jpe?g|gif|svg|webp|bmp|ico)$/.test(pathname)) {
|
|
return true;
|
|
}
|
|
return Boolean(
|
|
parsed.searchParams.get("wx_fmt") || parsed.searchParams.get("tp") === "webp"
|
|
);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
function isWebpReference(reference) {
|
|
return /\.webp(?:[?#].*)?$/i.test(stripMarkdownUrlDelimiters(reference));
|
|
}
|
|
function resolveWebpReference(rawPath, file, vaultPath) {
|
|
return resolveImageReference(rawPath, file, vaultPath);
|
|
}
|
|
function resolveImageReference(rawPath, file, vaultPath) {
|
|
const sourcePath = stripMarkdownUrlDelimiters(rawPath);
|
|
const decodedPath = stripUrlQueryAndHash(safeDecodeUri(sourcePath));
|
|
const candidates = path4.isAbsolute(decodedPath) ? [decodedPath] : [
|
|
path4.join(vaultPath, path4.dirname(file.path), decodedPath),
|
|
path4.join(vaultPath, decodedPath)
|
|
];
|
|
for (const candidate of candidates) {
|
|
if (fs4.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
async function downloadRemoteWebpIfNeeded(rawUrl, tmpDir, index) {
|
|
const url = stripMarkdownUrlDelimiters(rawUrl);
|
|
const isUrlWebp = isWebpReference(url);
|
|
let image;
|
|
try {
|
|
image = await downloadRemoteImage(url);
|
|
} catch (err) {
|
|
console.warn(
|
|
"Obsidian Press: could not inspect remote image before export:",
|
|
url,
|
|
err
|
|
);
|
|
return null;
|
|
}
|
|
if (!isUrlWebp && !image.contentType.includes("image/webp") && !isWebpBuffer(image.data)) {
|
|
return null;
|
|
}
|
|
const outputPath = path4.join(
|
|
tmpDir,
|
|
`remote-webp-${Date.now()}-${index}.webp`
|
|
);
|
|
fs4.writeFileSync(outputPath, image.data);
|
|
return outputPath;
|
|
}
|
|
async function downloadRemoteImageForTypst(url, tmpDir, index) {
|
|
try {
|
|
const image = await downloadRemoteImage(url);
|
|
const extension = getImageExtension(url, image.contentType, image.data);
|
|
const downloadedPath = path4.join(
|
|
tmpDir,
|
|
`remote-image-${Date.now()}-${index}.${extension}`
|
|
);
|
|
fs4.writeFileSync(downloadedPath, image.data);
|
|
if (extension === "webp" || isWebpBuffer(image.data)) {
|
|
const pngPath = path4.join(
|
|
tmpDir,
|
|
`remote-image-${Date.now()}-${index}.png`
|
|
);
|
|
await convertWebpToPng(downloadedPath, pngPath);
|
|
return pngPath;
|
|
}
|
|
return downloadedPath;
|
|
} catch (err) {
|
|
console.warn("Obsidian Press: could not download remote image:", url, err);
|
|
return createMissingImagePlaceholder(tmpDir, index);
|
|
}
|
|
}
|
|
async function copyLocalImageForTypst(rawPath, file, vaultPath, tmpDir, index) {
|
|
const resolvedPath = resolveImageReference(rawPath, file, vaultPath);
|
|
if (!resolvedPath) {
|
|
return null;
|
|
}
|
|
if (isWebpReference(resolvedPath)) {
|
|
const outputPath2 = path4.join(
|
|
tmpDir,
|
|
`typst-image-${Date.now()}-${index}.png`
|
|
);
|
|
await convertWebpToPng(resolvedPath, outputPath2);
|
|
return outputPath2;
|
|
}
|
|
const extension = getImageExtensionFromPath(resolvedPath);
|
|
const outputPath = path4.join(
|
|
tmpDir,
|
|
`typst-image-${Date.now()}-${index}.${extension}`
|
|
);
|
|
fs4.copyFileSync(resolvedPath, outputPath);
|
|
return outputPath;
|
|
}
|
|
async function downloadRemoteImage(url) {
|
|
const response = await (0, import_obsidian3.requestUrl)({
|
|
url,
|
|
method: "GET",
|
|
headers: {
|
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
|
|
}
|
|
});
|
|
const contentTypeHeader = response.headers["content-type"] || response.headers["Content-Type"] || "";
|
|
return {
|
|
data: Buffer.from(response.arrayBuffer),
|
|
contentType: contentTypeHeader.toLowerCase()
|
|
};
|
|
}
|
|
function getImageExtension(url, contentType, data) {
|
|
if (isWebpBuffer(data) || contentType.includes("image/webp")) return "webp";
|
|
if (isPngBuffer(data) || contentType.includes("image/png")) return "png";
|
|
if (isJpegBuffer(data) || contentType.includes("image/jpeg")) return "jpg";
|
|
if (isGifBuffer(data) || contentType.includes("image/gif")) return "gif";
|
|
if (isSvgBuffer(data) || contentType.includes("image/svg")) return "svg";
|
|
const formatFromQuery = getImageFormatFromQuery(url);
|
|
if (formatFromQuery) return formatFromQuery;
|
|
const pathname = safeUrlPathname(url);
|
|
const ext = path4.extname(pathname).replace(".", "").toLowerCase();
|
|
if (["png", "jpg", "jpeg", "gif", "svg", "webp"].includes(ext)) {
|
|
return ext === "jpeg" ? "jpg" : ext;
|
|
}
|
|
return "png";
|
|
}
|
|
function getImageExtensionFromPath(filePath) {
|
|
const ext = path4.extname(stripUrlQueryAndHash(filePath)).replace(".", "").toLowerCase();
|
|
if (ext === "jpeg") return "jpg";
|
|
if (["png", "jpg", "gif", "svg", "webp"].includes(ext)) return ext;
|
|
return "png";
|
|
}
|
|
function getImageFormatFromQuery(url) {
|
|
try {
|
|
const parsed = new URL(url);
|
|
const value = (parsed.searchParams.get("wx_fmt") || parsed.searchParams.get("format") || "").toLowerCase();
|
|
if (value === "jpeg") return "jpg";
|
|
if (["png", "jpg", "gif", "svg", "webp"].includes(value)) return value;
|
|
} catch (e) {
|
|
}
|
|
return null;
|
|
}
|
|
function safeUrlPathname(url) {
|
|
try {
|
|
return new URL(url).pathname;
|
|
} catch (e) {
|
|
return url;
|
|
}
|
|
}
|
|
function isWebpBuffer(data) {
|
|
return data.length >= 12 && data.toString("ascii", 0, 4) === "RIFF" && data.toString("ascii", 8, 12) === "WEBP";
|
|
}
|
|
function isPngBuffer(data) {
|
|
return data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71;
|
|
}
|
|
function isJpegBuffer(data) {
|
|
return data.length >= 3 && data[0] === 255 && data[1] === 216;
|
|
}
|
|
function isGifBuffer(data) {
|
|
return data.length >= 6 && (data.toString("ascii", 0, 6) === "GIF87a" || data.toString("ascii", 0, 6) === "GIF89a");
|
|
}
|
|
function isSvgBuffer(data) {
|
|
return data.toString("utf8", 0, Math.min(data.length, 256)).includes("<svg");
|
|
}
|
|
function createMissingImagePlaceholder(tmpDir, index) {
|
|
const outputPath = path4.join(
|
|
tmpDir,
|
|
`remote-image-missing-${Date.now()}-${index}.svg`
|
|
);
|
|
fs4.writeFileSync(
|
|
outputPath,
|
|
`<svg xmlns="http://www.w3.org/2000/svg" width="640" height="160" viewBox="0 0 640 160">
|
|
<rect width="640" height="160" fill="#f6f6f6" stroke="#cccccc"/>
|
|
<text x="320" y="86" text-anchor="middle" font-family="sans-serif" font-size="20" fill="#666666">Remote image unavailable</text>
|
|
</svg>`,
|
|
"utf8"
|
|
);
|
|
return outputPath;
|
|
}
|
|
function stripMarkdownUrlDelimiters(value) {
|
|
if (value.startsWith("<") && value.endsWith(">")) {
|
|
return value.slice(1, -1);
|
|
}
|
|
return value;
|
|
}
|
|
function safeDecodeUri(value) {
|
|
try {
|
|
return decodeURI(value);
|
|
} catch (e) {
|
|
return value;
|
|
}
|
|
}
|
|
function stripUrlQueryAndHash(value) {
|
|
return value.replace(/[?#].*$/, "");
|
|
}
|
|
function convertWebpToPng(inputPath, outputPath) {
|
|
return new Promise((resolve, reject) => {
|
|
var _a;
|
|
const child = (0, import_child_process3.spawn)("dwebp", [inputPath, "-o", outputPath], {
|
|
shell: false,
|
|
stdio: "pipe",
|
|
env: {
|
|
...process.env,
|
|
PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH || ""}`
|
|
}
|
|
});
|
|
let stderr = "";
|
|
(_a = child.stderr) == null ? void 0 : _a.on("data", (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
child.on("close", (code) => {
|
|
if (code === 0 && fs4.existsSync(outputPath)) {
|
|
resolve();
|
|
} else {
|
|
reject(
|
|
new Error(
|
|
`Failed to convert WebP image for LaTeX: ${stderr.trim() || `dwebp exited with code ${code}`}`
|
|
)
|
|
);
|
|
}
|
|
});
|
|
child.on("error", (err) => {
|
|
reject(
|
|
new Error(
|
|
`Failed to run dwebp for WebP image conversion: ${err.message}`
|
|
)
|
|
);
|
|
});
|
|
});
|
|
}
|
|
async function preflightChecks(settings) {
|
|
const errors = [];
|
|
const pandocCheck = await checkPandocAvailable(settings.pandocPath);
|
|
if (!pandocCheck.available) {
|
|
errors.push(
|
|
`Pandoc not found at "${settings.pandocPath}". Install: brew install pandoc`
|
|
);
|
|
}
|
|
if (settings.pdfEngine === "wkhtmltopdf") {
|
|
const exists = await checkCommandExists("wkhtmltopdf");
|
|
if (!exists) {
|
|
errors.push(
|
|
"wkhtmltopdf not found. Install: brew install wkhtmltopdf"
|
|
);
|
|
}
|
|
} else if (settings.pdfEngine === "weasyprint") {
|
|
const exists = await checkCommandExists("weasyprint");
|
|
if (!exists) {
|
|
errors.push(
|
|
"WeasyPrint not found. Install: pip install weasyprint"
|
|
);
|
|
}
|
|
} else if (settings.pdfEngine === "xelatex" || settings.pdfEngine === "pdflatex" || settings.pdfEngine === "lualatex") {
|
|
const exists = await checkCommandExists(settings.pdfEngine);
|
|
if (!exists) {
|
|
errors.push(
|
|
`${settings.pdfEngine} not found. Install: brew install --cask mactex-no-gui`
|
|
);
|
|
}
|
|
}
|
|
const mermaidExists = await checkCommandExists(
|
|
settings.mermaidPath || "mmdc"
|
|
);
|
|
if (!mermaidExists) {
|
|
console.warn(
|
|
"Obsidian Press: mmdc not found. Mermaid diagrams will be exported as code blocks."
|
|
);
|
|
}
|
|
return {
|
|
ok: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
// src/main.ts
|
|
var ExportProgressNotice = class {
|
|
constructor(title, detail = "") {
|
|
this.notice = new import_obsidian4.Notice("", 0);
|
|
this.notice.noticeEl.empty();
|
|
this.notice.noticeEl.addClass("obsidian-press-progress-notice");
|
|
this.titleEl = this.notice.noticeEl.createDiv({
|
|
cls: "obsidian-press-progress-title",
|
|
text: title
|
|
});
|
|
this.detailEl = this.notice.noticeEl.createDiv({
|
|
cls: "obsidian-press-progress-detail",
|
|
text: detail
|
|
});
|
|
const barEl = this.notice.noticeEl.createDiv({
|
|
cls: "obsidian-press-progress-bar"
|
|
});
|
|
this.fillEl = barEl.createDiv({
|
|
cls: "obsidian-press-progress-bar-fill"
|
|
});
|
|
this.setProgress(0);
|
|
}
|
|
update(title, detail, progress) {
|
|
this.titleEl.setText(title);
|
|
this.detailEl.setText(detail);
|
|
this.setProgress(progress);
|
|
}
|
|
hide() {
|
|
this.notice.hide();
|
|
}
|
|
setProgress(progress) {
|
|
const normalized = Math.max(0, Math.min(100, progress));
|
|
this.fillEl.style.width = `${normalized}%`;
|
|
}
|
|
};
|
|
var ObsidianPressPlugin = class extends import_obsidian4.Plugin {
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.addRibbonIcon("file-output", "Obsidian Press: Export current note", () => {
|
|
this.exportCurrentNote();
|
|
});
|
|
this.addCommand({
|
|
id: "export-current-note-pdf",
|
|
name: "Export current note to PDF",
|
|
callback: () => this.exportCurrentNote()
|
|
});
|
|
this.addCommand({
|
|
id: "export-current-note-pdf-choose-folder",
|
|
name: "Export current note to PDF...",
|
|
callback: () => this.exportCurrentNoteWithDirectory("pdf")
|
|
});
|
|
this.addCommand({
|
|
id: "export-current-note-docx",
|
|
name: "Export current note to Word (DOCX)",
|
|
callback: () => this.exportCurrentNote("docx")
|
|
});
|
|
this.addCommand({
|
|
id: "export-current-note-docx-choose-folder",
|
|
name: "Export current note to Word (DOCX)...",
|
|
callback: () => this.exportCurrentNoteWithDirectory("docx")
|
|
});
|
|
this.addCommand({
|
|
id: "export-current-note-html",
|
|
name: "Export current note to HTML",
|
|
callback: () => this.exportCurrentNote("html")
|
|
});
|
|
this.addCommand({
|
|
id: "export-current-note-html-choose-folder",
|
|
name: "Export current note to HTML...",
|
|
callback: () => this.exportCurrentNoteWithDirectory("html")
|
|
});
|
|
this.addCommand({
|
|
id: "export-folder",
|
|
name: "Export all notes in current folder",
|
|
checkCallback: (checking) => {
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (file) {
|
|
const folder = file.parent;
|
|
if (folder && folder instanceof import_obsidian4.TFolder) {
|
|
if (!checking) {
|
|
this.exportFolder(folder);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "export-folder-choose-folder",
|
|
name: "Export all notes in current folder...",
|
|
checkCallback: (checking) => {
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (file) {
|
|
const folder = file.parent;
|
|
if (folder && folder instanceof import_obsidian4.TFolder) {
|
|
if (!checking) {
|
|
this.exportFolderWithDirectory(folder);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "export-vault",
|
|
name: "Export entire vault",
|
|
callback: () => this.exportEntireVault()
|
|
});
|
|
this.addCommand({
|
|
id: "export-vault-choose-folder",
|
|
name: "Export entire vault...",
|
|
callback: () => this.exportEntireVaultWithDirectory()
|
|
});
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-menu", (menu, file) => {
|
|
if (file instanceof import_obsidian4.TFile && file.extension === "md") {
|
|
menu.addItem((item) => {
|
|
item.setTitle("\u5BFC\u51FA\u4E3A PDF\uFF08Obsidian Press\uFF09").setIcon("file-output").onClick(() => this.exportSpecificFile(file));
|
|
});
|
|
menu.addItem((item) => {
|
|
item.setTitle("\u9009\u62E9\u76EE\u5F55\u5BFC\u51FA\u4E3A PDF\uFF08Obsidian Press\uFF09").setIcon("folder-open").onClick(() => this.exportSpecificFileWithDirectory(file, "pdf"));
|
|
});
|
|
}
|
|
if (file instanceof import_obsidian4.TFolder) {
|
|
menu.addItem((item) => {
|
|
item.setTitle("\u5168\u90E8\u5BFC\u51FA\u4E3A PDF\uFF08Obsidian Press\uFF09").setIcon("file-output").onClick(() => this.exportFolder(file));
|
|
});
|
|
menu.addItem((item) => {
|
|
item.setTitle("\u9009\u62E9\u76EE\u5F55\u5168\u90E8\u5BFC\u51FA\u4E3A PDF\uFF08Obsidian Press\uFF09").setIcon("folder-open").onClick(() => this.exportFolderWithDirectory(file));
|
|
});
|
|
}
|
|
})
|
|
);
|
|
this.addSettingTab(new ObsidianPressSettingTab(this.app, this));
|
|
}
|
|
onunload() {
|
|
}
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
if (this.settings.pdfEngine === "chromium") {
|
|
this.settings.pdfEngine = "xelatex";
|
|
await this.saveSettings();
|
|
}
|
|
if (this.settings.codeTheme === "pygments") {
|
|
this.settings.codeTheme = "tango";
|
|
await this.saveSettings();
|
|
}
|
|
}
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
// === Export actions ===
|
|
async exportCurrentNote(forceFormat) {
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (!file || file.extension !== "md") {
|
|
new import_obsidian4.Notice("No active Markdown file");
|
|
return;
|
|
}
|
|
const format = forceFormat || this.settings.defaultFormat;
|
|
const settings = { ...this.settings, defaultFormat: format };
|
|
await this.exportSpecificFile(file, settings);
|
|
}
|
|
async exportCurrentNoteWithDirectory(forceFormat) {
|
|
const file = this.app.workspace.getActiveFile();
|
|
if (!file || file.extension !== "md") {
|
|
new import_obsidian4.Notice("No active Markdown file");
|
|
return;
|
|
}
|
|
await this.exportSpecificFileWithDirectory(
|
|
file,
|
|
forceFormat || this.settings.defaultFormat
|
|
);
|
|
}
|
|
async exportSpecificFileWithDirectory(file, format) {
|
|
const outputDir = await this.chooseOutputDirectory();
|
|
if (!outputDir) {
|
|
return;
|
|
}
|
|
await this.exportSpecificFile(file, {
|
|
...this.settings,
|
|
defaultFormat: format,
|
|
outputDir
|
|
});
|
|
}
|
|
async exportSpecificFile(file, overrideSettings) {
|
|
const settings = overrideSettings || this.settings;
|
|
const check = await preflightChecks(settings);
|
|
if (!check.ok) {
|
|
new import_obsidian4.Notice(`Export failed:
|
|
${check.errors.join("\n")}`, 8e3);
|
|
return;
|
|
}
|
|
const formatLabel = settings.defaultFormat.toUpperCase();
|
|
const progress = new ExportProgressNotice(
|
|
`\u6B63\u5728\u5BFC\u51FA ${formatLabel}`,
|
|
file.name
|
|
);
|
|
try {
|
|
progress.update(`\u6B63\u5728\u5BFC\u51FA ${formatLabel}`, "\u51C6\u5907\u6587\u4EF6...", 20);
|
|
const result = await exportFile(file, this.app, settings);
|
|
progress.update(`\u6B63\u5728\u5BFC\u51FA ${formatLabel}`, "\u5199\u5165\u5BFC\u51FA\u6587\u4EF6...", 90);
|
|
progress.hide();
|
|
if (result.success) {
|
|
new import_obsidian4.Notice(
|
|
`Exported: ${result.outputPath}
|
|
(${(result.duration / 1e3).toFixed(1)}s)`
|
|
);
|
|
if (settings.openAfterExport && result.outputPath) {
|
|
window.open(`file://${result.outputPath}`);
|
|
}
|
|
} else {
|
|
new import_obsidian4.Notice(`Export failed: ${result.error}`, 8e3);
|
|
}
|
|
} catch (err) {
|
|
progress.hide();
|
|
new import_obsidian4.Notice(
|
|
`Export error: ${err instanceof Error ? err.message : String(err)}`,
|
|
8e3
|
|
);
|
|
}
|
|
}
|
|
async exportFolder(folder, overrideSettings) {
|
|
const settings = overrideSettings || this.settings;
|
|
const check = await preflightChecks(settings);
|
|
if (!check.ok) {
|
|
new import_obsidian4.Notice(`Export failed:
|
|
${check.errors.join("\n")}`, 8e3);
|
|
return;
|
|
}
|
|
const progress = new ExportProgressNotice(
|
|
"\u6B63\u5728\u5BFC\u51FA\u6587\u4EF6\u5939",
|
|
folder.name
|
|
);
|
|
try {
|
|
const result = await exportFolder(
|
|
folder,
|
|
this.app,
|
|
settings,
|
|
(done, total, current) => {
|
|
progress.update(
|
|
"\u6B63\u5728\u5BFC\u51FA\u6587\u4EF6\u5939",
|
|
`${done}/${total} \xB7 ${current}`,
|
|
total > 0 ? done / total * 100 : 0
|
|
);
|
|
}
|
|
);
|
|
progress.hide();
|
|
const summary = [
|
|
`Export complete`,
|
|
`Total: ${result.total}`,
|
|
`Success: ${result.success}`,
|
|
`Failed: ${result.failed}`,
|
|
`Output: ${result.outputDir}`,
|
|
`Time: ${(result.duration / 1e3).toFixed(1)}s`
|
|
].join("\n");
|
|
new import_obsidian4.Notice(summary, 1e4);
|
|
if (result.failed > 0 && result.errors.length > 0) {
|
|
console.error("Obsidian Press export errors:", result.errors);
|
|
}
|
|
} catch (err) {
|
|
progress.hide();
|
|
new import_obsidian4.Notice(
|
|
`Export error: ${err instanceof Error ? err.message : String(err)}`,
|
|
8e3
|
|
);
|
|
}
|
|
}
|
|
async exportFolderWithDirectory(folder) {
|
|
const outputDir = await this.chooseOutputDirectory();
|
|
if (!outputDir) {
|
|
return;
|
|
}
|
|
await this.exportFolder(folder, {
|
|
...this.settings,
|
|
outputDir
|
|
});
|
|
}
|
|
async exportEntireVault(overrideSettings) {
|
|
const settings = overrideSettings || this.settings;
|
|
const check = await preflightChecks(settings);
|
|
if (!check.ok) {
|
|
new import_obsidian4.Notice(`Export failed:
|
|
${check.errors.join("\n")}`, 8e3);
|
|
return;
|
|
}
|
|
const progress = new ExportProgressNotice(
|
|
"\u6B63\u5728\u5BFC\u51FA\u6574\u4E2A\u4ED3\u5E93",
|
|
"\u626B\u63CF Markdown \u6587\u4EF6..."
|
|
);
|
|
try {
|
|
const result = await exportVault(
|
|
this.app,
|
|
settings,
|
|
(done, total, current) => {
|
|
progress.update(
|
|
"\u6B63\u5728\u5BFC\u51FA\u6574\u4E2A\u4ED3\u5E93",
|
|
`${done}/${total} \xB7 ${current}`,
|
|
total > 0 ? done / total * 100 : 0
|
|
);
|
|
}
|
|
);
|
|
progress.hide();
|
|
const summary = [
|
|
`Vault export complete`,
|
|
`Total: ${result.total}`,
|
|
`Success: ${result.success}`,
|
|
`Failed: ${result.failed}`,
|
|
`Output: ${result.outputDir}`,
|
|
`Time: ${(result.duration / 1e3).toFixed(1)}s`
|
|
].join("\n");
|
|
new import_obsidian4.Notice(summary, 1e4);
|
|
if (result.failed > 0 && result.errors.length > 0) {
|
|
console.error("Obsidian Press export errors:", result.errors);
|
|
}
|
|
} catch (err) {
|
|
progress.hide();
|
|
new import_obsidian4.Notice(
|
|
`Export error: ${err instanceof Error ? err.message : String(err)}`,
|
|
8e3
|
|
);
|
|
}
|
|
}
|
|
async exportEntireVaultWithDirectory() {
|
|
const outputDir = await this.chooseOutputDirectory();
|
|
if (!outputDir) {
|
|
return;
|
|
}
|
|
await this.exportEntireVault({
|
|
...this.settings,
|
|
outputDir
|
|
});
|
|
}
|
|
getConfiguredOutputDirectory() {
|
|
const vaultPath = getVaultPath(this.app);
|
|
const outputDir = this.settings.outputDir || "pdf";
|
|
return path5.isAbsolute(outputDir) ? outputDir : path5.join(vaultPath, outputDir);
|
|
}
|
|
async chooseOutputDirectory() {
|
|
var _a, _b;
|
|
if (!import_obsidian4.Platform.isDesktopApp) {
|
|
new import_obsidian4.Notice("Folder selection is only available in the desktop app", 8e3);
|
|
return null;
|
|
}
|
|
try {
|
|
const electron = require("electron");
|
|
const dialog = ((_a = electron.remote) == null ? void 0 : _a.dialog) || electron.dialog;
|
|
if (!(dialog == null ? void 0 : dialog.showOpenDialog)) {
|
|
new import_obsidian4.Notice("Folder selection is not available in this Obsidian window", 8e3);
|
|
return null;
|
|
}
|
|
const result = await dialog.showOpenDialog({
|
|
title: "Choose export folder",
|
|
defaultPath: this.getConfiguredOutputDirectory(),
|
|
properties: ["openDirectory", "createDirectory"]
|
|
});
|
|
if (result.canceled || !((_b = result.filePaths) == null ? void 0 : _b[0])) {
|
|
new import_obsidian4.Notice("Export canceled");
|
|
return null;
|
|
}
|
|
return result.filePaths[0];
|
|
} catch (err) {
|
|
new import_obsidian4.Notice(
|
|
`Could not open folder picker: ${err instanceof Error ? err.message : String(err)}`,
|
|
8e3
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
};
|