diff --git a/main.ts b/main.ts index a700c40..26a6e97 100644 --- a/main.ts +++ b/main.ts @@ -13,6 +13,7 @@ import { checkDependencies, installPackage } from './src/utils/python'; import { getVaultBasePath, resolveOutputFolder, + resolveFilenameTemplate, resolveImageDir, toVaultRelative, } from './src/utils/paths'; @@ -123,8 +124,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 28ebbc4..2380e5e 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'; export class FolderConvertModal extends Modal { @@ -119,8 +119,11 @@ export class FolderConvertModal extends Modal { const file = files[i]; progressModal?.updateProgress(i, file.name, success, failed); - 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`); const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_'); const tempFilePath = path.join(outputFolder, `tmp_${Date.now()}_${i}_${safeName}`); diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 7cc2a42..12d9a85 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -76,6 +76,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 a2e1e8d..36b98d4 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -13,6 +13,7 @@ export interface MarkitdownSettings { imageSubfolderTemplate: string; enableBatchProgress: boolean; enableContextMenu: boolean; + outputFilenameTemplate: string; } export const DEFAULT_SETTINGS: MarkitdownSettings = { @@ -25,6 +26,7 @@ export const DEFAULT_SETTINGS: MarkitdownSettings = { imageSubfolderTemplate: '{filename}-images', enableBatchProgress: true, enableContextMenu: true, + 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.