diff --git a/main.ts b/main.ts index 436f7f1..059aba3 100644 --- a/main.ts +++ b/main.ts @@ -14,6 +14,7 @@ import { checkDependencies, installPackage } from './src/utils/python'; import { getVaultBasePath, resolveOutputFolder, + resolveFilenameTemplate, resolveImageDir, toVaultRelative, } from './src/utils/paths'; @@ -147,8 +148,11 @@ export default class MarkitdownPlugin extends Plugin { const inputPath = path.join(vaultPath, file.path); const outputFolder = resolveOutputFolder(vaultPath, this.settings.outputPath); - const baseName = file.basename; - const outputPath = path.join(outputFolder, `${baseName}.md`); + const resolvedName = resolveFilenameTemplate( + this.settings.outputFilenameTemplate || '{filename}', + inputPath + ); + const outputPath = path.join(outputFolder, `${resolvedName}.md`); const options = this.buildConversionOptions(outputPath); new Notice('Converting file...'); diff --git a/src/modals/FileConvertModal.ts b/src/modals/FileConvertModal.ts index 312c511..7d09772 100644 --- a/src/modals/FileConvertModal.ts +++ b/src/modals/FileConvertModal.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as fs from 'fs'; import type MarkitdownPlugin from '../../main'; import { FILE_INPUT_ACCEPT } from '../utils/fileTypes'; -import { getVaultBasePath, resolveOutputFolder, toVaultRelative } from '../utils/paths'; +import { getVaultBasePath, resolveOutputFolder, resolveFilenameTemplate, toVaultRelative } from '../utils/paths'; export class FileConvertModal extends Modal { private plugin: MarkitdownPlugin; @@ -51,8 +51,11 @@ export class FileConvertModal extends Modal { } const outputFolder = resolveOutputFolder(vaultPath, this.plugin.settings.outputPath); - const baseName = path.basename(file.name, path.extname(file.name)); - const outputPath = path.join(outputFolder, `${baseName}.md`); + const resolvedName = resolveFilenameTemplate( + this.plugin.settings.outputFilenameTemplate || '{filename}', + file.name + ); + const outputPath = path.join(outputFolder, `${resolvedName}.md`); // Write the DOM File to a temp file on disk const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_'); diff --git a/src/modals/FolderConvertModal.ts b/src/modals/FolderConvertModal.ts index 0db6831..87ba14e 100644 --- a/src/modals/FolderConvertModal.ts +++ b/src/modals/FolderConvertModal.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as fs from 'fs'; import type MarkitdownPlugin from '../../main'; import { EXTENSION_GROUPS } from '../utils/fileTypes'; -import { getVaultBasePath, resolveOutputFolder } from '../utils/paths'; +import { getVaultBasePath, resolveOutputFolder, resolveFilenameTemplate } from '../utils/paths'; import { BatchProgressModal } from './BatchProgressModal'; /** @@ -176,7 +176,10 @@ export class FolderConvertModal extends Modal { progressModal?.updateProgress(i, entry.name, success, failed); // Determine the output path, preserving subfolder structure - const baseName = path.basename(entry.name, path.extname(entry.name)); + const resolvedName = resolveFilenameTemplate( + this.plugin.settings.outputFilenameTemplate || '{filename}', + entry.name + ); const relativeDir = path.dirname(entry.relativePath); const entryOutputFolder = relativeDir && relativeDir !== '.' ? path.join(outputFolder, relativeDir) @@ -185,7 +188,7 @@ export class FolderConvertModal extends Modal { // Ensure the output subdirectory exists await fs.promises.mkdir(entryOutputFolder, { recursive: true }); - const outputPath = path.join(entryOutputFolder, `${baseName}.md`); + const outputPath = path.join(entryOutputFolder, `${resolvedName}.md`); const safeName = entry.name.replace(/[^a-zA-Z0-9._-]/g, '_'); const tempFilePath = path.join(entryOutputFolder, `tmp_${Date.now()}_${i}_${safeName}`); diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 87e85ca..639a0a1 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -84,6 +84,21 @@ export class SettingsTab extends PluginSettingTab { await this.plugin.saveSettings(); })); + new Setting(containerEl) + .setName('Output filename template') + .setDesc( + 'Template for converted filenames. Variables: {filename} (original name), ' + + '{ext} (original extension), {date} (YYYY-MM-DD), {datetime} (YYYY-MM-DD_HHmmss). ' + + 'The .md extension is always appended automatically.' + ) + .addText(text => text + .setPlaceholder('{filename}') + .setValue(this.plugin.settings.outputFilenameTemplate) + .onChange(async (value) => { + this.plugin.settings.outputFilenameTemplate = value || '{filename}'; + await this.plugin.saveSettings(); + })); + new Setting(containerEl) .setName('Extract images') .setDesc('Extract embedded images from PDFs and save as separate files') diff --git a/src/types/settings.ts b/src/types/settings.ts index 14ef291..123740c 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -14,6 +14,7 @@ export interface MarkitdownSettings { enableBatchProgress: boolean; enableContextMenu: boolean; enableRecursiveConversion: boolean; + outputFilenameTemplate: string; } export const DEFAULT_SETTINGS: MarkitdownSettings = { @@ -27,6 +28,7 @@ export const DEFAULT_SETTINGS: MarkitdownSettings = { enableBatchProgress: true, enableContextMenu: true, enableRecursiveConversion: false, + outputFilenameTemplate: '{filename}', }; export interface ConversionOptions { diff --git a/src/utils/paths.ts b/src/utils/paths.ts index 9736c9a..5435c1f 100644 --- a/src/utils/paths.ts +++ b/src/utils/paths.ts @@ -37,6 +37,43 @@ export function resolveOutputFolder(vaultPath: string, outputSetting: string): s return outputFolder; } +/** + * Resolve an output filename template using variables from the input path. + * Supported variables: + * {filename} — original file name without extension + * {ext} — original file extension (without dot) + * {date} — conversion date as YYYY-MM-DD + * {datetime} — conversion datetime as YYYY-MM-DD_HHmmss + * + * Returns the resolved name without .md extension (caller appends it). + * Characters invalid in filenames are stripped from the result. + */ +export function resolveFilenameTemplate(template: string, inputPath: string): string { + const baseName = path.basename(inputPath, path.extname(inputPath)); + const ext = path.extname(inputPath).replace(/^\./, ''); + + const now = new Date(); + const pad = (n: number) => String(n).padStart(2, '0'); + const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; + const datetime = `${date}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; + + let resolved = template + .replace(/\{filename\}/g, baseName) + .replace(/\{ext\}/g, ext) + .replace(/\{date\}/g, date) + .replace(/\{datetime\}/g, datetime); + + // Remove characters invalid in filenames + resolved = resolved.replace(/[/\\:*?"<>|]/g, ''); + + // Fall back to the original filename if the result is empty after sanitisation + if (!resolved.trim()) { + resolved = baseName; + } + + return resolved; +} + /** * Compute the image extraction directory for a given output markdown file. * Applies the template (e.g., "{filename}-images") to generate the folder name.