Merge branch 'worktree-agent-af145deb'

This commit is contained in:
Ethan Troy 2026-03-17 20:40:00 -04:00
commit d154f5a6ff
3 changed files with 98 additions and 16 deletions

View file

@ -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) {

View file

@ -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')

View file

@ -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 {