Address marketplace review feedback

This commit is contained in:
mk 2026-04-29 15:01:50 +08:00
parent 50e6f698dc
commit 80a14cf3cd
15 changed files with 4927 additions and 199 deletions

View file

@ -9,7 +9,7 @@ Press PDF Export converts Obsidian Markdown into Pandoc-compatible Markdown, the
- Version: `1.0.0`
- License: MIT
- Obsidian: desktop app only
- Minimum Obsidian version: `0.15.0`
- Minimum Obsidian version: `1.8.7`
## Features

33
eslint.config.mjs Normal file
View file

@ -0,0 +1,33 @@
import tsparser from "@typescript-eslint/parser";
import { defineConfig } from "eslint/config";
import obsidianmd from "eslint-plugin-obsidianmd";
export default defineConfig([
...obsidianmd.configs.recommended,
{
files: ["src/**/*.ts"],
languageOptions: {
parser: tsparser,
parserOptions: { project: "./tsconfig.json" },
},
rules: {
"obsidianmd/no-nodejs-modules": "off",
"obsidianmd/ui/sentence-case": [
"warn",
{
acronyms: ["PDF", "DOCX", "HTML", "CSS", "CLI", "CJK"],
brands: [
"Pandoc",
"XeLaTeX",
"pdfLaTeX",
"LuaLaTeX",
"WeasyPrint",
"Typst",
"Mermaid",
"Homebrew",
],
},
],
},
},
]);

202
main.js
View file

@ -37,7 +37,8 @@ __export(main_exports, {
default: () => ObsidianPressPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian4 = require("obsidian");
var import_obsidian5 = require("obsidian");
var import_electron = require("electron");
var path5 = __toESM(require("path"));
// src/types.ts
@ -74,36 +75,35 @@ var ObsidianPressSettingTab = class extends import_obsidian.PluginSettingTab {
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) => {
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) => {
(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) => {
(dropdown) => dropdown.addOption("pdf", "PDF").addOption("docx", "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"
"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) => {
(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) => {
(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();
})
@ -137,20 +137,20 @@ var ObsidianPressSettingTab = class extends import_obsidian.PluginSettingTab {
})
);
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) => {
(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."
"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(
new import_obsidian.Setting(containerEl).setName("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();
@ -172,7 +172,7 @@ var ObsidianPressSettingTab = class extends import_obsidian.PluginSettingTab {
})
);
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) => {
(text) => text.setPlaceholder("Mmdc").setValue(this.plugin.settings.mermaidPath).onChange(async (value) => {
this.plugin.settings.mermaidPath = value || "mmdc";
await this.plugin.saveSettings();
})
@ -189,7 +189,7 @@ var ObsidianPressSettingTab = class extends import_obsidian.PluginSettingTab {
await this.plugin.saveSettings();
})
);
new import_obsidian.Setting(containerEl).setName("Batch Export").setHeading();
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);
@ -209,15 +209,16 @@ var ObsidianPressSettingTab = class extends import_obsidian.PluginSettingTab {
};
// src/exporter.ts
var import_obsidian3 = require("obsidian");
var import_obsidian4 = 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");
var import_obsidian3 = require("obsidian");
// src/utils.ts
var import_obsidian2 = require("obsidian");
var import_child_process = require("child_process");
var path = __toESM(require("path"));
var fs = __toESM(require("fs"));
@ -247,10 +248,13 @@ function runCommand(cmd, options) {
});
}
function getVaultPath(app) {
return app.vault.adapter.basePath;
if (app.vault.adapter instanceof import_obsidian2.FileSystemAdapter) {
return app.vault.adapter.getBasePath();
}
throw new Error("Press PDF Export requires the desktop file system adapter.");
}
function resolveAttachmentPath(src, currentFile, app) {
var _a, _b;
var _a;
const vaultPath = getVaultPath(app);
if (path.isAbsolute(src)) {
return src;
@ -271,8 +275,9 @@ function resolveAttachmentPath(src, currentFile, app) {
if (fs.existsSync(absRoot)) {
return absRoot;
}
const attachmentFolder = (_b = (_a = app.vault).getConfig) == null ? void 0 : _b.call(_a, "attachmentFolderPath");
if (attachmentFolder) {
const vaultConfig = app.vault;
const attachmentFolder = (_a = vaultConfig.getConfig) == null ? void 0 : _a.call(vaultConfig, "attachmentFolderPath");
if (typeof attachmentFolder === "string" && attachmentFolder) {
const absAttachment = path.join(vaultPath, attachmentFolder, src);
if (fs.existsSync(absAttachment)) {
return absAttachment;
@ -286,10 +291,11 @@ async function checkCommandExists(cmd) {
const { code } = await runCommand(checkCmd);
return code === 0;
}
function getTmpDir(vaultPath) {
function getTmpDir(app) {
const vaultPath = getVaultPath(app);
const tmpDir = path.join(
vaultPath,
".obsidian",
app.vault.configDir,
"plugins",
"press-pdf-export",
"tmp"
@ -326,10 +332,11 @@ function getOutputPath(file, vaultPath, outputDir, naming, format) {
const baseName = path.basename(file.path, ".md");
let fileName;
switch (naming) {
case "timestamp":
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;
@ -421,8 +428,7 @@ var CALLOUT_ICONS = {
bug: "\u{1F41B}"
};
async function renderToPandoc(content, file, app, mermaidPath, mermaidTheme) {
const vaultPath = getVaultPath(app);
const tmpDir = getTmpDir(vaultPath);
const tmpDir = getTmpDir(app);
const tempFiles = [];
let rendered = stripFrontmatter(content);
rendered = formatFlattenedCodeBlocks(rendered);
@ -437,7 +443,7 @@ async function renderToPandoc(content, file, app, mermaidPath, mermaidTheme) {
rendered = protectedCode.content;
rendered = convertCallouts(rendered);
rendered = convertWikilinks(rendered, file, app);
rendered = await convertEmbeds(rendered, file, app);
rendered = convertEmbeds(rendered, file, app);
rendered = await inlineNoteEmbeds(rendered, file, app, 0, 5);
rendered = convertHighlights(rendered);
rendered = convertSupSub(rendered);
@ -538,16 +544,25 @@ function indentFormattedCode(code) {
const output = [];
let level = 0;
for (const line of lines) {
if (/^[}\])]/.test(line)) {
if (line.startsWith("}") || line.startsWith("]") || line.startsWith(")")) {
level = Math.max(0, level - 1);
}
output.push(`${" ".repeat(level)}${line}`);
const opens = (line.match(/[{\[(]/g) || []).length;
const closes = (line.match(/[}\])]/g) || []).length;
const opens = countCharacters(line, "{[(");
const closes = countCharacters(line, "}])");
level = Math.max(0, level + opens - closes);
}
return output.join("\n");
}
function countCharacters(value, characters) {
let count = 0;
for (const char of value) {
if (characters.includes(char)) {
count++;
}
}
return count;
}
function protectCodeSegments(content) {
const segments = /* @__PURE__ */ new Map();
let index = 0;
@ -596,7 +611,7 @@ function convertCallouts(content) {
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) => {
const bodyLines = lines.map((line) => line.replace(/^>\s?/, "")).filter((_line, i) => {
if (i === 0) return false;
return true;
});
@ -634,7 +649,7 @@ function convertWikilinks(content, file, app) {
}
);
}
async function convertEmbeds(content, file, app) {
function convertEmbeds(content, file, app) {
const embedRegex = /!\[\[([^\]|]+?)(?:\|(\d+))?\]\]/g;
let result = content;
let match;
@ -664,7 +679,7 @@ async function inlineNoteEmbeds(content, currentFile, app, depth, maxDepth) {
target,
currentFile.path
);
if (resolvedFile instanceof import_obsidian2.TFile && resolvedFile.extension === "md") {
if (resolvedFile instanceof import_obsidian3.TFile && resolvedFile.extension === "md") {
const embedContent = await app.vault.read(resolvedFile);
const processed = await inlineNoteEmbeds(
embedContent,
@ -693,7 +708,7 @@ function convertHighlights(content) {
return content.replace(/==([^=]+)==/g, "<mark>$1</mark>");
}
function convertSupSub(content) {
let result = content.replace(/\^([^\^]+)\^/g, "<sup>$1</sup>");
let result = content.replace(/\^([^^]+)\^/g, "<sup>$1</sup>");
result = result.replace(/~~([^~]+)~~/g, "<sub>$1</sub>");
return result;
}
@ -708,7 +723,7 @@ function convertImageSizes(content) {
}
);
}
async function convertMermaidBlocks(content, mermaidPath, mermaidTheme, tmpDir, tempFiles) {
async function convertMermaidBlocks(content, _mermaidPath, mermaidTheme, tmpDir, tempFiles) {
const mermaidRegex = /```mermaid\n([\s\S]*?)```/g;
let result = content;
let match;
@ -919,7 +934,7 @@ async function exportWithPandoc(options) {
}
const args = buildPandocArgs(options);
return new Promise((resolve) => {
var _a, _b;
var _a;
const child = (0, import_child_process2.spawn)(pandocPath, args, {
shell: false,
cwd: options.tempDir,
@ -937,12 +952,8 @@ async function exportWithPandoc(options) {
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) => {
(_a = child.stderr) == null ? void 0 : _a.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
@ -1038,7 +1049,7 @@ async function checkPandocAvailable(pandocPath) {
});
child.on("close", (code) => {
if (code === 0) {
const versionMatch = stdout.match(/pandoc\s+(\d+\.\d+[\.\d]*)/);
const versionMatch = stdout.match(/pandoc\s+(\d+\.\d+[.\d]*)/);
resolve({
available: true,
version: versionMatch ? versionMatch[1] : "unknown"
@ -1056,7 +1067,7 @@ async function checkPandocAvailable(pandocPath) {
// src/exporter.ts
async function exportFile(file, app, settings) {
const vaultPath = getVaultPath(app);
const tmpDir = getTmpDir(vaultPath);
const tmpDir = getTmpDir(app);
try {
const content = await app.vault.read(file);
const effectivePdfEngine = settings.defaultFormat === "pdf" && settings.pdfEngine === "pdflatex" && containsCjk(content) ? "xelatex" : settings.pdfEngine;
@ -1234,9 +1245,9 @@ async function exportBatch(files, app, settings, onProgress) {
}
function collectMarkdownFiles(folder, files) {
for (const child of folder.children) {
if (child instanceof import_obsidian3.TFile && child.extension === "md") {
if (child instanceof import_obsidian4.TFile && child.extension === "md") {
files.push(child);
} else if (child instanceof import_obsidian3.TFolder) {
} else if (child instanceof import_obsidian4.TFolder) {
collectMarkdownFiles(child, files);
}
}
@ -1465,7 +1476,7 @@ async function copyLocalImageForTypst(rawPath, file, vaultPath, tmpDir, index) {
return outputPath;
}
async function downloadRemoteImage(url) {
const response = await (0, import_obsidian3.requestUrl)({
const response = await (0, import_obsidian4.requestUrl)({
url,
method: "GET",
headers: {
@ -1645,18 +1656,18 @@ async function preflightChecks(settings) {
// 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({
this.notice = new import_obsidian5.Notice("", 0);
this.notice.messageEl.empty();
this.notice.messageEl.addClass("obsidian-press-progress-notice");
this.titleEl = this.notice.messageEl.createDiv({
cls: "obsidian-press-progress-title",
text: title
});
this.detailEl = this.notice.noticeEl.createDiv({
this.detailEl = this.notice.messageEl.createDiv({
cls: "obsidian-press-progress-detail",
text: detail
});
const barEl = this.notice.noticeEl.createDiv({
const barEl = this.notice.messageEl.createDiv({
cls: "obsidian-press-progress-bar"
});
this.fillEl = barEl.createDiv({
@ -1677,41 +1688,41 @@ var ExportProgressNotice = class {
this.fillEl.style.width = `${normalized}%`;
}
};
var ObsidianPressPlugin = class extends import_obsidian4.Plugin {
var ObsidianPressPlugin = class extends import_obsidian5.Plugin {
async onload() {
await this.loadSettings();
this.addRibbonIcon("file-output", "Press PDF Export: Export current note", () => {
this.exportCurrentNote();
this.addRibbonIcon("file-output", "Export current note", () => {
void this.exportCurrentNote();
});
this.addCommand({
id: "export-current-note-pdf",
name: "Export current note to PDF",
callback: () => this.exportCurrentNote()
callback: () => void this.exportCurrentNote()
});
this.addCommand({
id: "export-current-note-pdf-choose-folder",
name: "Export current note to PDF...",
callback: () => this.exportCurrentNoteWithDirectory("pdf")
callback: () => void this.exportCurrentNoteWithDirectory("pdf")
});
this.addCommand({
id: "export-current-note-docx",
name: "Export current note to Word (DOCX)",
callback: () => this.exportCurrentNote("docx")
name: "Export current note to DOCX",
callback: () => void this.exportCurrentNote("docx")
});
this.addCommand({
id: "export-current-note-docx-choose-folder",
name: "Export current note to Word (DOCX)...",
callback: () => this.exportCurrentNoteWithDirectory("docx")
name: "Export current note to DOCX...",
callback: () => void this.exportCurrentNoteWithDirectory("docx")
});
this.addCommand({
id: "export-current-note-html",
name: "Export current note to HTML",
callback: () => this.exportCurrentNote("html")
callback: () => void this.exportCurrentNote("html")
});
this.addCommand({
id: "export-current-note-html-choose-folder",
name: "Export current note to HTML...",
callback: () => this.exportCurrentNoteWithDirectory("html")
callback: () => void this.exportCurrentNoteWithDirectory("html")
});
this.addCommand({
id: "export-folder",
@ -1720,9 +1731,9 @@ var ObsidianPressPlugin = class extends import_obsidian4.Plugin {
const file = this.app.workspace.getActiveFile();
if (file) {
const folder = file.parent;
if (folder && folder instanceof import_obsidian4.TFolder) {
if (folder && folder instanceof import_obsidian5.TFolder) {
if (!checking) {
this.exportFolder(folder);
void this.exportFolder(folder);
}
return true;
}
@ -1737,9 +1748,9 @@ var ObsidianPressPlugin = class extends import_obsidian4.Plugin {
const file = this.app.workspace.getActiveFile();
if (file) {
const folder = file.parent;
if (folder && folder instanceof import_obsidian4.TFolder) {
if (folder && folder instanceof import_obsidian5.TFolder) {
if (!checking) {
this.exportFolderWithDirectory(folder);
void this.exportFolderWithDirectory(folder);
}
return true;
}
@ -1750,29 +1761,29 @@ var ObsidianPressPlugin = class extends import_obsidian4.Plugin {
this.addCommand({
id: "export-vault",
name: "Export entire vault",
callback: () => this.exportEntireVault()
callback: () => void this.exportEntireVault()
});
this.addCommand({
id: "export-vault-choose-folder",
name: "Export entire vault...",
callback: () => this.exportEntireVaultWithDirectory()
callback: () => void this.exportEntireVaultWithDirectory()
});
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file) => {
if (file instanceof import_obsidian4.TFile && file.extension === "md") {
if (file instanceof import_obsidian5.TFile && file.extension === "md") {
menu.addItem((item) => {
item.setTitle("\u5BFC\u51FA\u4E3A PDF\uFF08Press PDF Export\uFF09").setIcon("file-output").onClick(() => this.exportSpecificFile(file));
item.setTitle("\u5BFC\u51FA\u4E3A PDF").setIcon("file-output").onClick(() => void this.exportSpecificFile(file));
});
menu.addItem((item) => {
item.setTitle("\u9009\u62E9\u76EE\u5F55\u5BFC\u51FA\u4E3A PDF\uFF08Press PDF Export\uFF09").setIcon("folder-open").onClick(() => this.exportSpecificFileWithDirectory(file, "pdf"));
item.setTitle("\u9009\u62E9\u76EE\u5F55\u5BFC\u51FA\u4E3A PDF").setIcon("folder-open").onClick(() => void this.exportSpecificFileWithDirectory(file, "pdf"));
});
}
if (file instanceof import_obsidian4.TFolder) {
if (file instanceof import_obsidian5.TFolder) {
menu.addItem((item) => {
item.setTitle("\u5168\u90E8\u5BFC\u51FA\u4E3A PDF\uFF08Press PDF Export\uFF09").setIcon("file-output").onClick(() => this.exportFolder(file));
item.setTitle("\u5168\u90E8\u5BFC\u51FA\u4E3A PDF").setIcon("file-output").onClick(() => void this.exportFolder(file));
});
menu.addItem((item) => {
item.setTitle("\u9009\u62E9\u76EE\u5F55\u5168\u90E8\u5BFC\u51FA\u4E3A PDF\uFF08Press PDF Export\uFF09").setIcon("folder-open").onClick(() => this.exportFolderWithDirectory(file));
item.setTitle("\u9009\u62E9\u76EE\u5F55\u5168\u90E8\u5BFC\u51FA\u4E3A PDF").setIcon("folder-open").onClick(() => void this.exportFolderWithDirectory(file));
});
}
})
@ -1782,7 +1793,9 @@ var ObsidianPressPlugin = class extends import_obsidian4.Plugin {
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
const loadedData = await this.loadData();
const savedSettings = loadedData && typeof loadedData === "object" ? loadedData : {};
this.settings = { ...DEFAULT_SETTINGS, ...savedSettings };
if (this.settings.pdfEngine === "chromium") {
this.settings.pdfEngine = "xelatex";
await this.saveSettings();
@ -1799,7 +1812,7 @@ var ObsidianPressPlugin = class extends import_obsidian4.Plugin {
async exportCurrentNote(forceFormat) {
const file = this.app.workspace.getActiveFile();
if (!file || file.extension !== "md") {
new import_obsidian4.Notice("No active Markdown file");
new import_obsidian5.Notice("No active markdown file");
return;
}
const format = forceFormat || this.settings.defaultFormat;
@ -1809,7 +1822,7 @@ var ObsidianPressPlugin = class extends import_obsidian4.Plugin {
async exportCurrentNoteWithDirectory(forceFormat) {
const file = this.app.workspace.getActiveFile();
if (!file || file.extension !== "md") {
new import_obsidian4.Notice("No active Markdown file");
new import_obsidian5.Notice("No active markdown file");
return;
}
await this.exportSpecificFileWithDirectory(
@ -1832,7 +1845,7 @@ var ObsidianPressPlugin = class extends import_obsidian4.Plugin {
const settings = overrideSettings || this.settings;
const check = await preflightChecks(settings);
if (!check.ok) {
new import_obsidian4.Notice(`Export failed:
new import_obsidian5.Notice(`Export failed:
${check.errors.join("\n")}`, 8e3);
return;
}
@ -1847,7 +1860,7 @@ ${check.errors.join("\n")}`, 8e3);
progress.update(`\u6B63\u5728\u5BFC\u51FA ${formatLabel}`, "\u5199\u5165\u5BFC\u51FA\u6587\u4EF6...", 90);
progress.hide();
if (result.success) {
new import_obsidian4.Notice(
new import_obsidian5.Notice(
`Exported: ${result.outputPath}
(${(result.duration / 1e3).toFixed(1)}s)`
);
@ -1855,11 +1868,11 @@ ${check.errors.join("\n")}`, 8e3);
window.open(`file://${result.outputPath}`);
}
} else {
new import_obsidian4.Notice(`Export failed: ${result.error}`, 8e3);
new import_obsidian5.Notice(`Export failed: ${result.error}`, 8e3);
}
} catch (err) {
progress.hide();
new import_obsidian4.Notice(
new import_obsidian5.Notice(
`Export error: ${err instanceof Error ? err.message : String(err)}`,
8e3
);
@ -1869,7 +1882,7 @@ ${check.errors.join("\n")}`, 8e3);
const settings = overrideSettings || this.settings;
const check = await preflightChecks(settings);
if (!check.ok) {
new import_obsidian4.Notice(`Export failed:
new import_obsidian5.Notice(`Export failed:
${check.errors.join("\n")}`, 8e3);
return;
}
@ -1899,13 +1912,13 @@ ${check.errors.join("\n")}`, 8e3);
`Output: ${result.outputDir}`,
`Time: ${(result.duration / 1e3).toFixed(1)}s`
].join("\n");
new import_obsidian4.Notice(summary, 1e4);
new import_obsidian5.Notice(summary, 1e4);
if (result.failed > 0 && result.errors.length > 0) {
console.error("Press PDF Export export errors:", result.errors);
}
} catch (err) {
progress.hide();
new import_obsidian4.Notice(
new import_obsidian5.Notice(
`Export error: ${err instanceof Error ? err.message : String(err)}`,
8e3
);
@ -1925,7 +1938,7 @@ ${check.errors.join("\n")}`, 8e3);
const settings = overrideSettings || this.settings;
const check = await preflightChecks(settings);
if (!check.ok) {
new import_obsidian4.Notice(`Export failed:
new import_obsidian5.Notice(`Export failed:
${check.errors.join("\n")}`, 8e3);
return;
}
@ -1954,13 +1967,13 @@ ${check.errors.join("\n")}`, 8e3);
`Output: ${result.outputDir}`,
`Time: ${(result.duration / 1e3).toFixed(1)}s`
].join("\n");
new import_obsidian4.Notice(summary, 1e4);
new import_obsidian5.Notice(summary, 1e4);
if (result.failed > 0 && result.errors.length > 0) {
console.error("Press PDF Export export errors:", result.errors);
}
} catch (err) {
progress.hide();
new import_obsidian4.Notice(
new import_obsidian5.Notice(
`Export error: ${err instanceof Error ? err.message : String(err)}`,
8e3
);
@ -1983,15 +1996,14 @@ ${check.errors.join("\n")}`, 8e3);
}
async chooseOutputDirectory() {
var _a, _b;
if (!import_obsidian4.Platform.isDesktopApp) {
new import_obsidian4.Notice("Folder selection is only available in the desktop app", 8e3);
if (!import_obsidian5.Platform.isDesktopApp) {
new import_obsidian5.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);
const dialog = ((_a = import_electron.remote) == null ? void 0 : _a.dialog) || import_electron.dialog;
if (!dialog.showOpenDialog) {
new import_obsidian5.Notice("Folder selection is not available in this obsidian window", 8e3);
return null;
}
const result = await dialog.showOpenDialog({
@ -2000,12 +2012,12 @@ ${check.errors.join("\n")}`, 8e3);
properties: ["openDirectory", "createDirectory"]
});
if (result.canceled || !((_b = result.filePaths) == null ? void 0 : _b[0])) {
new import_obsidian4.Notice("Export canceled");
new import_obsidian5.Notice("Export canceled");
return null;
}
return result.filePaths[0];
} catch (err) {
new import_obsidian4.Notice(
new import_obsidian5.Notice(
`Could not open folder picker: ${err instanceof Error ? err.message : String(err)}`,
8e3
);

View file

@ -3,7 +3,7 @@
"name": "Press PDF Export",
"version": "1.0.0",
"description": "High-fidelity PDF export powered by Pandoc. Supports multiple engines (XeLaTeX, wkhtmltopdf, WeasyPrint), Mermaid diagrams, custom CSS/templates, and batch export.",
"minAppVersion": "0.15.0",
"minAppVersion": "1.8.7",
"author": "Taylor Chen",
"isDesktopOnly": true
}

4643
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,13 +5,24 @@
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"lint": "eslint src --max-warnings=0",
"typecheck": "tsc --noEmit",
"build": "npm run typecheck && node esbuild.config.mjs production",
"verify:code-export": "node scripts/verify-code-export.mjs",
"release:package": "npm run build && node scripts/package-release.mjs",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": ["obsidian", "pandoc", "pdf", "docx", "html", "export", "latex", "typst", "mermaid"],
"keywords": [
"obsidian",
"pandoc",
"pdf",
"docx",
"html",
"export",
"latex",
"typst",
"mermaid"
],
"license": "MIT",
"files": [
"main.js",
@ -21,11 +32,15 @@
"LICENSE"
],
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^20.0.0",
"builtin-modules": "^4.0.0",
"esbuild": "^0.21.0",
"eslint": "^10.2.1",
"eslint-plugin-obsidianmd": "^0.2.8",
"obsidian": "latest",
"tslib": "^2.6.0",
"typescript": "^5.4.0"
"typescript": "^5.4.0",
"typescript-eslint": "^8.59.1"
}
}

21
src/electron.d.ts vendored Normal file
View file

@ -0,0 +1,21 @@
declare module "electron" {
export interface OpenDialogOptions {
title?: string;
defaultPath?: string;
properties?: Array<"openDirectory" | "createDirectory">;
}
export interface OpenDialogReturnValue {
canceled: boolean;
filePaths: string[];
}
export interface Dialog {
showOpenDialog(
options: OpenDialogOptions
): Promise<OpenDialogReturnValue>;
}
export const dialog: Dialog;
export const remote: { dialog?: Dialog } | undefined;
}

View file

@ -1,4 +1,4 @@
import { App, TFile, TFolder, Notice, Vault, requestUrl } from "obsidian";
import { App, TFile, TFolder, requestUrl } from "obsidian";
import * as path from "path";
import * as fs from "fs";
import { spawn } from "child_process";
@ -9,7 +9,6 @@ import {
getVaultPath,
getOutputPath,
getTmpDir,
cleanTmpDir,
createSemaphore,
checkCommandExists,
} from "./utils";
@ -22,7 +21,7 @@ export async function exportFile(
settings: PluginSettings
): Promise<ExportResult> {
const vaultPath = getVaultPath(app);
const tmpDir = getTmpDir(vaultPath);
const tmpDir = getTmpDir(app);
try {
// Read file content
@ -88,7 +87,7 @@ export async function exportFile(
const pandocOptions: PandocOptions = {
inputPath: tempMdPath,
outputPath,
format: format as any,
format,
engine: effectivePdfEngine,
pandocPath: settings.pandocPath,
tempDir: tmpDir,

View file

@ -1,4 +1,5 @@
import { Plugin, TFile, TFolder, Notice, Menu, Platform } from "obsidian";
import { dialog as electronDialog, remote } from "electron";
import * as path from "path";
import { PluginSettings, DEFAULT_SETTINGS, OutputFormat } from "./types";
import { ObsidianPressSettingTab } from "./settings";
@ -13,19 +14,19 @@ class ExportProgressNotice {
constructor(title: string, detail = "") {
this.notice = new Notice("", 0);
this.notice.noticeEl.empty();
this.notice.noticeEl.addClass("obsidian-press-progress-notice");
this.notice.messageEl.empty();
this.notice.messageEl.addClass("obsidian-press-progress-notice");
this.titleEl = this.notice.noticeEl.createDiv({
this.titleEl = this.notice.messageEl.createDiv({
cls: "obsidian-press-progress-title",
text: title,
});
this.detailEl = this.notice.noticeEl.createDiv({
this.detailEl = this.notice.messageEl.createDiv({
cls: "obsidian-press-progress-detail",
text: detail,
});
const barEl = this.notice.noticeEl.createDiv({
const barEl = this.notice.messageEl.createDiv({
cls: "obsidian-press-progress-bar",
});
this.fillEl = barEl.createDiv({
@ -57,45 +58,45 @@ export default class ObsidianPressPlugin extends Plugin {
await this.loadSettings();
// Ribbon icon — export current note
this.addRibbonIcon("file-output", "Press PDF Export: Export current note", () => {
this.exportCurrentNote();
this.addRibbonIcon("file-output", "Export current note", () => {
void this.exportCurrentNote();
});
// Commands
this.addCommand({
id: "export-current-note-pdf",
name: "Export current note to PDF",
callback: () => this.exportCurrentNote(),
callback: () => void this.exportCurrentNote(),
});
this.addCommand({
id: "export-current-note-pdf-choose-folder",
name: "Export current note to PDF...",
callback: () => this.exportCurrentNoteWithDirectory("pdf"),
callback: () => void this.exportCurrentNoteWithDirectory("pdf"),
});
this.addCommand({
id: "export-current-note-docx",
name: "Export current note to Word (DOCX)",
callback: () => this.exportCurrentNote("docx"),
name: "Export current note to DOCX",
callback: () => void this.exportCurrentNote("docx"),
});
this.addCommand({
id: "export-current-note-docx-choose-folder",
name: "Export current note to Word (DOCX)...",
callback: () => this.exportCurrentNoteWithDirectory("docx"),
name: "Export current note to DOCX...",
callback: () => void this.exportCurrentNoteWithDirectory("docx"),
});
this.addCommand({
id: "export-current-note-html",
name: "Export current note to HTML",
callback: () => this.exportCurrentNote("html"),
callback: () => void this.exportCurrentNote("html"),
});
this.addCommand({
id: "export-current-note-html-choose-folder",
name: "Export current note to HTML...",
callback: () => this.exportCurrentNoteWithDirectory("html"),
callback: () => void this.exportCurrentNoteWithDirectory("html"),
});
this.addCommand({
@ -107,7 +108,7 @@ export default class ObsidianPressPlugin extends Plugin {
const folder = file.parent;
if (folder && folder instanceof TFolder) {
if (!checking) {
this.exportFolder(folder);
void this.exportFolder(folder);
}
return true;
}
@ -125,7 +126,7 @@ export default class ObsidianPressPlugin extends Plugin {
const folder = file.parent;
if (folder && folder instanceof TFolder) {
if (!checking) {
this.exportFolderWithDirectory(folder);
void this.exportFolderWithDirectory(folder);
}
return true;
}
@ -137,13 +138,13 @@ export default class ObsidianPressPlugin extends Plugin {
this.addCommand({
id: "export-vault",
name: "Export entire vault",
callback: () => this.exportEntireVault(),
callback: () => void this.exportEntireVault(),
});
this.addCommand({
id: "export-vault-choose-folder",
name: "Export entire vault...",
callback: () => this.exportEntireVaultWithDirectory(),
callback: () => void this.exportEntireVaultWithDirectory(),
});
// File menu — right-click on file
@ -152,30 +153,30 @@ export default class ObsidianPressPlugin extends Plugin {
if (file instanceof TFile && file.extension === "md") {
menu.addItem((item) => {
item
.setTitle("导出为 PDFPress PDF Export")
.setTitle("导出为 PDF")
.setIcon("file-output")
.onClick(() => this.exportSpecificFile(file));
.onClick(() => void this.exportSpecificFile(file));
});
menu.addItem((item) => {
item
.setTitle("选择目录导出为 PDFPress PDF Export")
.setTitle("选择目录导出为 PDF")
.setIcon("folder-open")
.onClick(() => this.exportSpecificFileWithDirectory(file, "pdf"));
.onClick(() => void this.exportSpecificFileWithDirectory(file, "pdf"));
});
}
if (file instanceof TFolder) {
menu.addItem((item) => {
item
.setTitle("全部导出为 PDFPress PDF Export")
.setTitle("全部导出为 PDF")
.setIcon("file-output")
.onClick(() => this.exportFolder(file));
.onClick(() => void this.exportFolder(file));
});
menu.addItem((item) => {
item
.setTitle("选择目录全部导出为 PDFPress PDF Export")
.setTitle("选择目录全部导出为 PDF")
.setIcon("folder-open")
.onClick(() => this.exportFolderWithDirectory(file));
.onClick(() => void this.exportFolderWithDirectory(file));
});
}
})
@ -190,7 +191,12 @@ export default class ObsidianPressPlugin extends Plugin {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
const loadedData = (await this.loadData()) as unknown;
const savedSettings =
loadedData && typeof loadedData === "object"
? (loadedData as Partial<PluginSettings>)
: {};
this.settings = { ...DEFAULT_SETTINGS, ...savedSettings };
if ((this.settings.pdfEngine as string) === "chromium") {
this.settings.pdfEngine = "xelatex";
await this.saveSettings();
@ -210,7 +216,7 @@ export default class ObsidianPressPlugin extends Plugin {
private async exportCurrentNote(forceFormat?: OutputFormat) {
const file = this.app.workspace.getActiveFile();
if (!file || file.extension !== "md") {
new Notice("No active Markdown file");
new Notice("No active markdown file");
return;
}
@ -222,7 +228,7 @@ export default class ObsidianPressPlugin extends Plugin {
private async exportCurrentNoteWithDirectory(forceFormat?: OutputFormat) {
const file = this.app.workspace.getActiveFile();
if (!file || file.extension !== "md") {
new Notice("No active Markdown file");
new Notice("No active markdown file");
return;
}
@ -441,11 +447,10 @@ export default class ObsidianPressPlugin extends Plugin {
}
try {
const electron = require("electron") as any;
const dialog = electron.remote?.dialog || electron.dialog;
const dialog = remote?.dialog || electronDialog;
if (!dialog?.showOpenDialog) {
new Notice("Folder selection is not available in this Obsidian window", 8000);
if (!dialog.showOpenDialog) {
new Notice("Folder selection is not available in this obsidian window", 8000);
return null;
}

View file

@ -215,12 +215,8 @@ export async function exportWithPandoc(
},
});
let stdout = "";
let stderr = "";
child.stdout?.on("data", (data: Buffer) => {
stdout += data.toString();
});
child.stderr?.on("data", (data: Buffer) => {
stderr += data.toString();
});
@ -340,7 +336,7 @@ export async function checkPandocAvailable(
child.on("close", (code) => {
if (code === 0) {
const versionMatch = stdout.match(/pandoc\s+(\d+\.\d+[\.\d]*)/);
const versionMatch = stdout.match(/pandoc\s+(\d+\.\d+[.\d]*)/);
resolve({
available: true,
version: versionMatch ? versionMatch[1] : "unknown",

View file

@ -1,6 +1,6 @@
import { App, TFile, Vault } from "obsidian";
import { App, TFile } from "obsidian";
import { RenderResult, CalloutType } from "./types";
import { resolveAttachmentPath, getVaultPath, getTmpDir } from "./utils";
import { resolveAttachmentPath, getTmpDir } from "./utils";
import { renderMermaidBlock } from "./mermaid";
const CALLOUT_TYPES: CalloutType[] = [
@ -49,8 +49,7 @@ export async function renderToPandoc(
mermaidPath: string,
mermaidTheme: string
): Promise<RenderResult> {
const vaultPath = getVaultPath(app);
const tmpDir = getTmpDir(vaultPath);
const tmpDir = getTmpDir(app);
const tempFiles: string[] = [];
// Step 1: Strip YAML frontmatter (preserve title as heading)
@ -78,7 +77,7 @@ export async function renderToPandoc(
rendered = convertWikilinks(rendered, file, app);
// Step 6: Convert embedded images ![[img.png]] → ![img](abs/path)
rendered = await convertEmbeds(rendered, file, app);
rendered = convertEmbeds(rendered, file, app);
// Step 7: Inline embedded notes ![[other-note]] (limited depth)
rendered = await inlineNoteEmbeds(rendered, file, app, 0, 5);
@ -224,20 +223,30 @@ function indentFormattedCode(code: string): string {
let level = 0;
for (const line of lines) {
if (/^[}\])]/.test(line)) {
if (line.startsWith("}") || line.startsWith("]") || line.startsWith(")")) {
level = Math.max(0, level - 1);
}
output.push(`${" ".repeat(level)}${line}`);
const opens = (line.match(/[{\[(]/g) || []).length;
const closes = (line.match(/[}\])]/g) || []).length;
const opens = countCharacters(line, "{[(");
const closes = countCharacters(line, "}])");
level = Math.max(0, level + opens - closes);
}
return output.join("\n");
}
function countCharacters(value: string, characters: string): number {
let count = 0;
for (const char of value) {
if (characters.includes(char)) {
count++;
}
}
return count;
}
// === Code protection ===
interface ProtectedCodeSegments {
@ -323,7 +332,7 @@ function convertCallouts(content: string): string {
const lines = match.split("\n");
const bodyLines = lines
.map((line: string) => line.replace(/^>\s?/, ""))
.filter((line: string, i: number) => {
.filter((_line: string, i: number) => {
// Remove the first line (the [!type] line)
if (i === 0) return false;
return true;
@ -368,11 +377,11 @@ function convertWikilinks(content: string, file: TFile, app: App): string {
// === Step 4: Embed Images ===
async function convertEmbeds(
function convertEmbeds(
content: string,
file: TFile,
app: App
): Promise<string> {
): string {
// ![[image.png]] or ![[image.png|size]]
const embedRegex = /!\[\[([^\]|]+?)(?:\|(\d+))?\]\]/g;
@ -457,7 +466,7 @@ function convertHighlights(content: string): string {
function convertSupSub(content: string): string {
// ^text^ → <sup>text</sup>
let result = content.replace(/\^([^\^]+)\^/g, "<sup>$1</sup>");
let result = content.replace(/\^([^^]+)\^/g, "<sup>$1</sup>");
// ~~text~~ → <sub>text</sub>
result = result.replace(/~~([^~]+)~~/g, "<sub>$1</sub>");
return result;
@ -488,7 +497,7 @@ function convertImageSizes(content: string): string {
async function convertMermaidBlocks(
content: string,
mermaidPath: string,
_mermaidPath: string,
mermaidTheme: string,
tmpDir: string,
tempFiles: string[]

View file

@ -1,6 +1,13 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import ObsidianPressPlugin from "./main";
import { PdfEngine, PageSize, CodeTheme, MermaidTheme } from "./types";
import {
PdfEngine,
PageSize,
CodeTheme,
MermaidTheme,
OutputFormat,
OutputNaming,
} from "./types";
export class ObsidianPressSettingTab extends PluginSettingTab {
plugin: ObsidianPressPlugin;
@ -14,15 +21,12 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
const { containerEl } = this;
containerEl.empty();
// === General ===
new Setting(containerEl).setName("General").setHeading();
new Setting(containerEl)
.setName("Pandoc path")
.setDesc("Path to the pandoc binary")
.setDesc("Path to the Pandoc binary")
.addText((text) =>
text
.setPlaceholder("/opt/homebrew/bin/pandoc")
.setPlaceholder("/opt/Homebrew/bin/Pandoc")
.setValue(this.plugin.settings.pandocPath)
.onChange(async (value) => {
this.plugin.settings.pandocPath = value || "/opt/homebrew/bin/pandoc";
@ -38,7 +42,7 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
.addOption("xelatex", "XeLaTeX (best quality)")
.addOption("pdflatex", "pdfLaTeX (auto XeLaTeX for CJK)")
.addOption("lualatex", "LuaLaTeX")
.addOption("wkhtmltopdf", "wkhtmltopdf (lightweight)")
.addOption("wkhtmltopdf", "Wkhtmltopdf (lightweight)")
.addOption("weasyprint", "WeasyPrint (CSS-based)")
.addOption("typst", "Typst (experimental)")
.setValue(this.plugin.settings.pdfEngine)
@ -54,11 +58,11 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
.addDropdown((dropdown) =>
dropdown
.addOption("pdf", "PDF")
.addOption("docx", "Word (DOCX)")
.addOption("docx", "DOCX")
.addOption("html", "HTML")
.setValue(this.plugin.settings.defaultFormat)
.onChange(async (value: string) => {
this.plugin.settings.defaultFormat = value as any;
this.plugin.settings.defaultFormat = value as OutputFormat;
await this.plugin.saveSettings();
})
);
@ -69,11 +73,11 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Output directory")
.setDesc(
"Relative to vault root, or absolute path. Leave empty for 'pdf' folder"
"Relative to vault root, or absolute path. Leave empty for 'PDF' folder"
)
.addText((text) =>
text
.setPlaceholder("pdf")
.setPlaceholder("PDF")
.setValue(this.plugin.settings.outputDir)
.onChange(async (value) => {
this.plugin.settings.outputDir = value || "pdf";
@ -87,11 +91,11 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
.addDropdown((dropdown) =>
dropdown
.addOption("same", "Same as source (note.pdf)")
.addOption("timestamp", "With timestamp (note_2024-01-01T00-00-00.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: string) => {
this.plugin.settings.outputNaming = value as any;
this.plugin.settings.outputNaming = value as OutputNaming;
await this.plugin.saveSettings();
})
);
@ -164,7 +168,7 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
.addOption("tango", "Tango (default)")
.addOption("pygments", "Pygments (minimal background)")
.addOption("zenburn", "Zenburn")
.addOption("breezedark", "Breeze Dark")
.addOption("breezedark", "Breeze dark")
.addOption("kate", "Kate")
.addOption("monochrome", "Monochrome")
.setValue(this.plugin.settings.codeTheme)
@ -177,7 +181,7 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName("CJK font")
.setDesc(
"Chinese/Japanese/Korean font name. On macOS, STHeitiSC-Medium is a reliable XeLaTeX choice."
"Chinese/japanese/korean font name. On macOS, stheitisc-medium is a reliable XeLaTeX choice."
)
.addText((text) =>
text
@ -190,7 +194,7 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("Enable CJK support")
.setName("CJK support")
.setDesc("Add CJK font configuration for LaTeX engines")
.addToggle((toggle) =>
toggle
@ -237,7 +241,7 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
.setDesc("Path to mmdc binary for Mermaid diagram rendering")
.addText((text) =>
text
.setPlaceholder("mmdc")
.setPlaceholder("Mmdc")
.setValue(this.plugin.settings.mermaidPath)
.onChange(async (value) => {
this.plugin.settings.mermaidPath = value || "mmdc";
@ -275,7 +279,7 @@ export class ObsidianPressSettingTab extends PluginSettingTab {
);
// === Batch ===
new Setting(containerEl).setName("Batch Export").setHeading();
new Setting(containerEl).setName("Batch export").setHeading();
new Setting(containerEl)
.setName("Concurrency")

View file

@ -1,5 +1,3 @@
import { TFile, TFolder } from "obsidian";
// === Settings ===
export type PdfEngine =

View file

@ -1,5 +1,5 @@
import { App, TFile, TFolder, Vault } from "obsidian";
import { exec, spawn } from "child_process";
import { App, FileSystemAdapter, TFile } from "obsidian";
import { exec } from "child_process";
import * as path from "path";
import * as fs from "fs";
import * as os from "os";
@ -39,7 +39,10 @@ export function runCommand(
// === Vault Path ===
export function getVaultPath(app: App): string {
return (app.vault.adapter as any).basePath;
if (app.vault.adapter instanceof FileSystemAdapter) {
return app.vault.adapter.getBasePath();
}
throw new Error("Press PDF Export requires the desktop file system adapter.");
}
// === Attachment Path Resolution ===
@ -81,8 +84,11 @@ export function resolveAttachmentPath(
}
// Try Obsidian attachmentFolderPath setting
const attachmentFolder = (app.vault as any).getConfig?.("attachmentFolderPath");
if (attachmentFolder) {
const vaultConfig = app.vault as unknown as {
getConfig?: (key: string) => unknown;
};
const attachmentFolder = vaultConfig.getConfig?.("attachmentFolderPath");
if (typeof attachmentFolder === "string" && attachmentFolder) {
const absAttachment = path.join(vaultPath, attachmentFolder, src);
if (fs.existsSync(absAttachment)) {
return absAttachment;
@ -128,10 +134,11 @@ export async function checkCommandExists(cmd: string): Promise<boolean> {
// === Temp Directory ===
export function getTmpDir(vaultPath: string): string {
export function getTmpDir(app: App): string {
const vaultPath = getVaultPath(app);
const tmpDir = path.join(
vaultPath,
".obsidian",
app.vault.configDir,
"plugins",
"press-pdf-export",
"tmp"
@ -142,19 +149,6 @@ export function getTmpDir(vaultPath: string): string {
return tmpDir;
}
export async function cleanTmpDir(tmpDir: string): Promise<void> {
try {
if (fs.existsSync(tmpDir)) {
const files = fs.readdirSync(tmpDir);
for (const file of files) {
fs.unlinkSync(path.join(tmpDir, file));
}
}
} catch {
// Ignore cleanup errors
}
}
// === Semaphore ===
export interface Semaphore {
@ -200,13 +194,14 @@ export function getOutputPath(
let fileName: string;
switch (naming) {
case "timestamp":
case "timestamp": {
const ts = new Date()
.toISOString()
.replace(/[:.]/g, "-")
.slice(0, 19);
fileName = `${baseName}_${ts}.${format}`;
break;
}
case "suffix":
fileName = `${baseName}_export.${format}`;
break;
@ -239,7 +234,7 @@ export async function getPandocVersion(
): Promise<string | null> {
const { stdout, code } = await runCommand(`${pandocPath} --version`);
if (code === 0) {
const match = stdout.match(/pandoc\s+(\d+\.\d+[\.\d]*)/);
const match = stdout.match(/pandoc\s+(\d+\.\d+[.\d]*)/);
return match ? match[1] : "unknown";
}
return null;

View file

@ -1,3 +1,3 @@
{
"1.0.0": "0.15.0"
"1.0.0": "1.8.7"
}