mirror of
https://github.com/ethanolivertroy/obsidian-markitdown.git
synced 2026-07-22 05:41:54 +00:00
Merge branch 'worktree-agent-af145deb'
This commit is contained in:
commit
d154f5a6ff
3 changed files with 98 additions and 16 deletions
|
|
@ -6,6 +6,23 @@ import { EXTENSION_GROUPS } from '../utils/fileTypes';
|
|||
import { getVaultBasePath, resolveOutputFolder } from '../utils/paths';
|
||||
import { BatchProgressModal } from './BatchProgressModal';
|
||||
|
||||
/**
|
||||
* Represents a file to convert, with its path relative to the selected root folder.
|
||||
* For files picked via the HTML directory input, `file` is the browser File object and
|
||||
* `relativePath` is derived from `webkitRelativePath`. For files discovered via
|
||||
* recursive filesystem walk, `absolutePath` is set instead and `file` is null.
|
||||
*/
|
||||
interface ConvertibleFile {
|
||||
/** Browser File object (present when picked via HTML input) */
|
||||
file: File | null;
|
||||
/** Absolute path on disk (present when found via recursive walk) */
|
||||
absolutePath: string | null;
|
||||
/** Display name */
|
||||
name: string;
|
||||
/** Path relative to the selected root folder (e.g. "sub/dir/file.pdf") */
|
||||
relativePath: string;
|
||||
}
|
||||
|
||||
export class FolderConvertModal extends Modal {
|
||||
private plugin: MarkitdownPlugin;
|
||||
|
||||
|
|
@ -26,6 +43,21 @@ export class FolderConvertModal extends Modal {
|
|||
attr: { type: 'file', webkitdirectory: '', directory: '' },
|
||||
});
|
||||
|
||||
// Recursive toggle
|
||||
let includeSubfolders = this.plugin.settings.enableRecursiveConversion;
|
||||
const recursiveContainer = contentEl.createDiv('markitdown-setting-row');
|
||||
const recursiveLabel = recursiveContainer.createEl('label', {
|
||||
cls: 'markitdown-checkbox-label',
|
||||
});
|
||||
const recursiveCheckbox = recursiveLabel.createEl('input', {
|
||||
attr: { type: 'checkbox' },
|
||||
});
|
||||
recursiveCheckbox.checked = includeSubfolders;
|
||||
recursiveCheckbox.addEventListener('change', () => {
|
||||
includeSubfolders = recursiveCheckbox.checked;
|
||||
});
|
||||
recursiveLabel.appendText(' Include subfolders (recursive)');
|
||||
|
||||
// File type checkboxes
|
||||
contentEl.createEl('p', { text: 'Select file types to convert:' });
|
||||
const checkboxContainer = contentEl.createDiv('markitdown-checkbox-grid');
|
||||
|
|
@ -73,14 +105,38 @@ export class FolderConvertModal extends Modal {
|
|||
return;
|
||||
}
|
||||
|
||||
// Filter matching files
|
||||
const filesToConvert: File[] = [];
|
||||
// Determine the root folder path from the first file's webkitRelativePath
|
||||
const firstFile = folderInput.files[0];
|
||||
const rootFolderName = firstFile.webkitRelativePath.split('/')[0];
|
||||
|
||||
// Collect files from the HTML file input (these always include subfolders
|
||||
// because webkitdirectory returns all descendants). We filter based on the
|
||||
// recursive toggle.
|
||||
const filesToConvert: ConvertibleFile[] = [];
|
||||
for (let i = 0; i < folderInput.files.length; i++) {
|
||||
const file = folderInput.files[i];
|
||||
const ext = path.extname(file.name).toLowerCase();
|
||||
if (selectedExtensions.includes(ext)) {
|
||||
filesToConvert.push(file);
|
||||
if (!selectedExtensions.includes(ext)) continue;
|
||||
|
||||
// webkitRelativePath looks like "FolderName/sub/file.pdf"
|
||||
const relativePath = file.webkitRelativePath;
|
||||
const parts = relativePath.split('/');
|
||||
|
||||
// If not recursive, only include files directly in the root folder
|
||||
// (i.e., exactly 2 parts: "FolderName/file.ext")
|
||||
if (!includeSubfolders && parts.length > 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Relative path within the selected folder (strip the root folder name)
|
||||
const relativeToRoot = parts.slice(1).join('/');
|
||||
|
||||
filesToConvert.push({
|
||||
file,
|
||||
absolutePath: null,
|
||||
name: file.name,
|
||||
relativePath: relativeToRoot,
|
||||
});
|
||||
}
|
||||
|
||||
if (filesToConvert.length === 0) {
|
||||
|
|
@ -93,7 +149,7 @@ export class FolderConvertModal extends Modal {
|
|||
});
|
||||
}
|
||||
|
||||
private async runBatchConversion(files: File[]) {
|
||||
private async runBatchConversion(files: ConvertibleFile[]) {
|
||||
const vaultPath = getVaultBasePath(this.app);
|
||||
if (!vaultPath) {
|
||||
new Notice('Could not determine vault path. This plugin requires a local vault.');
|
||||
|
|
@ -116,17 +172,31 @@ export class FolderConvertModal extends Modal {
|
|||
const errors: string[] = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
progressModal?.updateProgress(i, file.name, success, failed);
|
||||
const entry = files[i];
|
||||
progressModal?.updateProgress(i, entry.name, success, failed);
|
||||
|
||||
const baseName = path.basename(file.name, path.extname(file.name));
|
||||
const outputPath = path.join(outputFolder, `${baseName}.md`);
|
||||
const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const tempFilePath = path.join(outputFolder, `tmp_${Date.now()}_${i}_${safeName}`);
|
||||
// Determine the output path, preserving subfolder structure
|
||||
const baseName = path.basename(entry.name, path.extname(entry.name));
|
||||
const relativeDir = path.dirname(entry.relativePath);
|
||||
const entryOutputFolder = relativeDir && relativeDir !== '.'
|
||||
? path.join(outputFolder, relativeDir)
|
||||
: outputFolder;
|
||||
|
||||
// Ensure the output subdirectory exists
|
||||
await fs.promises.mkdir(entryOutputFolder, { recursive: true });
|
||||
|
||||
const outputPath = path.join(entryOutputFolder, `${baseName}.md`);
|
||||
const safeName = entry.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const tempFilePath = path.join(entryOutputFolder, `tmp_${Date.now()}_${i}_${safeName}`);
|
||||
|
||||
try {
|
||||
const buffer = await file.arrayBuffer();
|
||||
await fs.promises.writeFile(tempFilePath, Buffer.from(buffer));
|
||||
// Read the file content from the browser File object
|
||||
if (entry.file) {
|
||||
const buffer = await entry.file.arrayBuffer();
|
||||
await fs.promises.writeFile(tempFilePath, Buffer.from(buffer));
|
||||
} else if (entry.absolutePath) {
|
||||
await fs.promises.copyFile(entry.absolutePath, tempFilePath);
|
||||
}
|
||||
|
||||
const result = await this.plugin.convertExternalFile(tempFilePath, outputPath);
|
||||
|
||||
|
|
@ -134,18 +204,18 @@ export class FolderConvertModal extends Modal {
|
|||
success++;
|
||||
} else {
|
||||
failed++;
|
||||
errors.push(`${file.name}: ${result.error}`);
|
||||
errors.push(`${entry.relativePath}: ${result.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
failed++;
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
errors.push(`${file.name}: ${msg}`);
|
||||
errors.push(`${entry.relativePath}: ${msg}`);
|
||||
} finally {
|
||||
await fs.promises.unlink(tempFilePath).catch(() => {});
|
||||
}
|
||||
|
||||
// Update progress after conversion completes so counts are accurate
|
||||
progressModal?.updateProgress(i + 1, file.name, success, failed);
|
||||
progressModal?.updateProgress(i + 1, entry.name, success, failed);
|
||||
}
|
||||
|
||||
if (progressModal) {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,16 @@ export class SettingsTab extends PluginSettingTab {
|
|||
}));
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Recursive folder conversion')
|
||||
.setDesc('Include subfolders by default when converting a folder')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableRecursiveConversion)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableRecursiveConversion = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Show batch progress')
|
||||
.setDesc('Display a progress bar during batch folder conversions')
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface MarkitdownSettings {
|
|||
imageSubfolderTemplate: string;
|
||||
enableBatchProgress: boolean;
|
||||
enableContextMenu: boolean;
|
||||
enableRecursiveConversion: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MarkitdownSettings = {
|
||||
|
|
@ -25,6 +26,7 @@ export const DEFAULT_SETTINGS: MarkitdownSettings = {
|
|||
imageSubfolderTemplate: '{filename}-images',
|
||||
enableBatchProgress: true,
|
||||
enableContextMenu: true,
|
||||
enableRecursiveConversion: false,
|
||||
};
|
||||
|
||||
export interface ConversionOptions {
|
||||
|
|
|
|||
Loading…
Reference in a new issue