taylorchen_obsidian-press/main.js
2026-04-29 07:36:23 +08:00

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 = `![${src}](${absPath})${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 `![${alt}](${url}){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,
`![Mermaid Diagram ${index}](${svgPath})`
);
} 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;
}
}
};