2026-05-25 08:02:41 +00:00
|
|
|
import { TFile } from 'obsidian';
|
|
|
|
|
import { DEFAULT_SETTINGS } from './constants';
|
|
|
|
|
import { FolderTaskFileSelectionProfile, NotemdSettings } from './types';
|
|
|
|
|
|
|
|
|
|
export type FolderTaskFileFilterMode = 'none' | 'contains' | 'regex' | 'glob';
|
|
|
|
|
export type FolderTaskFileFilterTarget = 'relativePath' | 'basename';
|
|
|
|
|
export type FolderTaskIncludeSubfoldersMode = 'legacy' | 'include' | 'exclude';
|
2026-05-26 10:36:26 +00:00
|
|
|
export type FolderTaskSelectionPresetId =
|
|
|
|
|
| 'contains-index-family'
|
|
|
|
|
| 'regex-index-variants'
|
|
|
|
|
| 'glob-index-family'
|
|
|
|
|
| 'glob-index-cross-folder';
|
2026-05-25 08:02:41 +00:00
|
|
|
|
|
|
|
|
export type FolderTaskKind =
|
|
|
|
|
| 'process-folder-add-links'
|
|
|
|
|
| 'extract-concepts-folder'
|
|
|
|
|
| 'batch-extract-original-text'
|
|
|
|
|
| 'batch-generate-from-titles'
|
|
|
|
|
| 'batch-mermaid-fix'
|
|
|
|
|
| 'batch-fix-formula'
|
|
|
|
|
| 'batch-translate-folder';
|
|
|
|
|
|
|
|
|
|
export interface FolderTaskFileFilterConfig {
|
|
|
|
|
mode: FolderTaskFileFilterMode;
|
|
|
|
|
pattern: string;
|
|
|
|
|
target: FolderTaskFileFilterTarget;
|
|
|
|
|
caseSensitive: boolean;
|
|
|
|
|
invert: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface FolderTaskFileSelectionOverride {
|
|
|
|
|
profileId?: string;
|
|
|
|
|
profileName?: string;
|
|
|
|
|
includeSubfoldersMode?: FolderTaskIncludeSubfoldersMode;
|
|
|
|
|
fileFilterMode?: FolderTaskFileFilterMode;
|
|
|
|
|
fileFilterPattern?: string;
|
|
|
|
|
fileFilterTarget?: FolderTaskFileFilterTarget;
|
|
|
|
|
fileFilterCaseSensitive?: boolean;
|
|
|
|
|
fileFilterInvert?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface FolderTaskInteractiveSelection {
|
|
|
|
|
folderPath: string;
|
|
|
|
|
fileSelectionOverride?: FolderTaskFileSelectionOverride;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 10:36:26 +00:00
|
|
|
export interface FolderTaskProfileFolderSelectionUiState {
|
|
|
|
|
folderPath: string;
|
|
|
|
|
hasProfileFolderHint: boolean;
|
|
|
|
|
profileFolderHint: string | null;
|
|
|
|
|
allowTemporaryFolderOverride: boolean;
|
|
|
|
|
folderSelectionDisabled: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 08:02:41 +00:00
|
|
|
export interface FolderTaskFileSelectionOptions {
|
|
|
|
|
taskKind: FolderTaskKind;
|
|
|
|
|
folderPath: string;
|
|
|
|
|
files: TFile[];
|
|
|
|
|
allowedExtensions: string[];
|
|
|
|
|
settings: NotemdSettings;
|
|
|
|
|
exclude?: (file: TFile, relativePath: string) => boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FolderTaskFilterMatcher {
|
|
|
|
|
matcher: (value: string) => boolean;
|
|
|
|
|
noOp: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const LEGACY_INCLUDE_SUBFOLDERS_BY_TASK: Record<FolderTaskKind, boolean> = {
|
|
|
|
|
'process-folder-add-links': true,
|
|
|
|
|
'extract-concepts-folder': true,
|
|
|
|
|
'batch-extract-original-text': true,
|
|
|
|
|
'batch-generate-from-titles': true,
|
|
|
|
|
'batch-mermaid-fix': true,
|
|
|
|
|
'batch-fix-formula': true,
|
|
|
|
|
'batch-translate-folder': false
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-26 10:36:26 +00:00
|
|
|
export function isAdvancedFolderTaskFileSelectionEnabled(
|
|
|
|
|
settings: Pick<NotemdSettings, 'enableDeveloperMode' | 'enableAdvancedFileSelectionProfiles'>
|
|
|
|
|
): boolean {
|
|
|
|
|
return settings.enableDeveloperMode && settings.enableAdvancedFileSelectionProfiles;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveFolderTaskRuntimeSettings(settings: NotemdSettings): NotemdSettings {
|
|
|
|
|
if (isAdvancedFolderTaskFileSelectionEnabled(settings)) {
|
|
|
|
|
return settings;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...settings,
|
|
|
|
|
folderTaskFileSelectionProfiles: [],
|
|
|
|
|
folderTaskFileFilterMode: DEFAULT_SETTINGS.folderTaskFileFilterMode,
|
|
|
|
|
folderTaskFileFilterPattern: DEFAULT_SETTINGS.folderTaskFileFilterPattern,
|
|
|
|
|
folderTaskFileFilterTarget: DEFAULT_SETTINGS.folderTaskFileFilterTarget,
|
|
|
|
|
folderTaskFileFilterCaseSensitive: DEFAULT_SETTINGS.folderTaskFileFilterCaseSensitive,
|
|
|
|
|
folderTaskFileFilterInvert: DEFAULT_SETTINGS.folderTaskFileFilterInvert,
|
|
|
|
|
folderTaskIncludeSubfoldersMode: DEFAULT_SETTINGS.folderTaskIncludeSubfoldersMode
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 08:02:41 +00:00
|
|
|
function normalizeFolderPath(folderPath: string): string {
|
|
|
|
|
const trimmed = folderPath.trim();
|
|
|
|
|
if (!trimmed || trimmed === '/') {
|
|
|
|
|
return '/';
|
|
|
|
|
}
|
|
|
|
|
return trimmed.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveIncludeSubfolders(
|
|
|
|
|
taskKind: FolderTaskKind,
|
|
|
|
|
mode: FolderTaskIncludeSubfoldersMode
|
|
|
|
|
): boolean {
|
|
|
|
|
if (mode === 'include') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (mode === 'exclude') {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return LEGACY_INCLUDE_SUBFOLDERS_BY_TASK[taskKind];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getRelativePathWithinFolder(filePath: string, folderPath: string): string | null {
|
|
|
|
|
if (folderPath === '/') {
|
|
|
|
|
return filePath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const prefix = `${folderPath}/`;
|
|
|
|
|
if (!filePath.startsWith(prefix)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const relativePath = filePath.slice(prefix.length);
|
|
|
|
|
return relativePath.length > 0 ? relativePath : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function matchesFolderScope(relativePath: string, includeSubfolders: boolean): boolean {
|
|
|
|
|
if (includeSubfolders) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return !relativePath.includes('/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeRegex(text: string): string {
|
|
|
|
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compileGlobPattern(pattern: string, caseSensitive: boolean): RegExp {
|
|
|
|
|
let source = '^';
|
|
|
|
|
for (let index = 0; index < pattern.length; index++) {
|
|
|
|
|
const char = pattern[index];
|
|
|
|
|
if (char === '*') {
|
|
|
|
|
if (pattern[index + 1] === '*') {
|
|
|
|
|
source += '.*';
|
|
|
|
|
index += 1;
|
|
|
|
|
} else {
|
|
|
|
|
source += '[^/]*';
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (char === '?') {
|
|
|
|
|
source += '.';
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
source += escapeRegex(char);
|
|
|
|
|
}
|
|
|
|
|
source += '$';
|
|
|
|
|
return new RegExp(source, caseSensitive ? undefined : 'i');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveFilterConfig(settings: NotemdSettings): FolderTaskFileFilterConfig {
|
2026-05-26 10:36:26 +00:00
|
|
|
const runtimeSettings = resolveFolderTaskRuntimeSettings(settings);
|
2026-05-25 08:02:41 +00:00
|
|
|
return {
|
2026-05-26 10:36:26 +00:00
|
|
|
mode: runtimeSettings.folderTaskFileFilterMode,
|
|
|
|
|
pattern: runtimeSettings.folderTaskFileFilterPattern,
|
|
|
|
|
target: runtimeSettings.folderTaskFileFilterTarget,
|
|
|
|
|
caseSensitive: runtimeSettings.folderTaskFileFilterCaseSensitive,
|
|
|
|
|
invert: runtimeSettings.folderTaskFileFilterInvert
|
2026-05-25 08:02:41 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeFolderTaskFileSelectionProfile(
|
|
|
|
|
profile: Partial<FolderTaskFileSelectionProfile> | null | undefined,
|
|
|
|
|
index: number
|
|
|
|
|
): FolderTaskFileSelectionProfile {
|
|
|
|
|
const fallback = DEFAULT_SETTINGS;
|
|
|
|
|
const normalizedName = typeof profile?.name === 'string' && profile.name.trim().length > 0
|
|
|
|
|
? profile.name.trim()
|
|
|
|
|
: `Profile ${index + 1}`;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: typeof profile?.id === 'string' && profile.id.trim().length > 0
|
|
|
|
|
? profile.id.trim()
|
|
|
|
|
: `folder-task-profile-${index + 1}`,
|
|
|
|
|
name: normalizedName,
|
|
|
|
|
folderPathHint: typeof profile?.folderPathHint === 'string' ? profile.folderPathHint.trim() : '',
|
|
|
|
|
includeSubfoldersMode: profile?.includeSubfoldersMode ?? fallback.folderTaskIncludeSubfoldersMode,
|
|
|
|
|
fileFilterMode: profile?.fileFilterMode ?? fallback.folderTaskFileFilterMode,
|
|
|
|
|
fileFilterPattern: typeof profile?.fileFilterPattern === 'string'
|
|
|
|
|
? profile.fileFilterPattern
|
|
|
|
|
: fallback.folderTaskFileFilterPattern,
|
|
|
|
|
fileFilterTarget: profile?.fileFilterTarget ?? fallback.folderTaskFileFilterTarget,
|
|
|
|
|
fileFilterCaseSensitive: profile?.fileFilterCaseSensitive ?? fallback.folderTaskFileFilterCaseSensitive,
|
|
|
|
|
fileFilterInvert: profile?.fileFilterInvert ?? fallback.folderTaskFileFilterInvert
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getFolderTaskFileSelectionProfiles(settings: NotemdSettings): FolderTaskFileSelectionProfile[] {
|
2026-05-26 10:36:26 +00:00
|
|
|
const runtimeSettings = resolveFolderTaskRuntimeSettings(settings);
|
2026-05-25 08:02:41 +00:00
|
|
|
const rawProfiles = Array.isArray(settings.folderTaskFileSelectionProfiles)
|
2026-05-26 10:36:26 +00:00
|
|
|
? runtimeSettings.folderTaskFileSelectionProfiles
|
2026-05-25 08:02:41 +00:00
|
|
|
: [];
|
|
|
|
|
return rawProfiles.map((profile, index) => normalizeFolderTaskFileSelectionProfile(profile, index));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 10:36:26 +00:00
|
|
|
export function createCurrentFolderTaskFileSelectionProfile(
|
|
|
|
|
settings: NotemdSettings,
|
|
|
|
|
profileName = 'Current default filter settings'
|
|
|
|
|
): FolderTaskFileSelectionProfile {
|
|
|
|
|
const runtimeSettings = resolveFolderTaskRuntimeSettings(settings);
|
|
|
|
|
return normalizeFolderTaskFileSelectionProfile({
|
|
|
|
|
id: '__current_defaults__',
|
|
|
|
|
name: profileName,
|
|
|
|
|
folderPathHint: '',
|
|
|
|
|
includeSubfoldersMode: runtimeSettings.folderTaskIncludeSubfoldersMode,
|
|
|
|
|
fileFilterMode: runtimeSettings.folderTaskFileFilterMode,
|
|
|
|
|
fileFilterPattern: runtimeSettings.folderTaskFileFilterPattern,
|
|
|
|
|
fileFilterTarget: runtimeSettings.folderTaskFileFilterTarget,
|
|
|
|
|
fileFilterCaseSensitive: runtimeSettings.folderTaskFileFilterCaseSensitive,
|
|
|
|
|
fileFilterInvert: runtimeSettings.folderTaskFileFilterInvert
|
|
|
|
|
}, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveExistingFolderTaskFolderPath(
|
|
|
|
|
availableFolders: string[],
|
|
|
|
|
folderPath: string | null | undefined
|
|
|
|
|
): string | null {
|
|
|
|
|
const normalized = normalizeFolderPath(typeof folderPath === 'string' ? folderPath : '');
|
|
|
|
|
return availableFolders.includes(normalized) ? normalized : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function looksLikeFolderTaskPatternPath(folderPath: string | null | undefined): boolean {
|
|
|
|
|
const normalized = typeof folderPath === 'string' ? folderPath.trim() : '';
|
|
|
|
|
if (!normalized) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return /[*?[\]{}()|\\^$]/.test(normalized);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createFolderTaskSelectionPresetOverride(
|
|
|
|
|
presetId: FolderTaskSelectionPresetId
|
|
|
|
|
): FolderTaskFileSelectionOverride {
|
|
|
|
|
switch (presetId) {
|
|
|
|
|
case 'contains-index-family':
|
|
|
|
|
return {
|
|
|
|
|
includeSubfoldersMode: 'exclude',
|
|
|
|
|
fileFilterMode: 'contains',
|
|
|
|
|
fileFilterPattern: 'index',
|
|
|
|
|
fileFilterTarget: 'basename',
|
|
|
|
|
fileFilterCaseSensitive: false,
|
|
|
|
|
fileFilterInvert: false
|
|
|
|
|
};
|
|
|
|
|
case 'regex-index-variants':
|
|
|
|
|
return {
|
|
|
|
|
includeSubfoldersMode: 'exclude',
|
|
|
|
|
fileFilterMode: 'regex',
|
|
|
|
|
fileFilterPattern: '^index(?:\\..+)?(?:_summ)?$',
|
|
|
|
|
fileFilterTarget: 'basename',
|
|
|
|
|
fileFilterCaseSensitive: false,
|
|
|
|
|
fileFilterInvert: false
|
|
|
|
|
};
|
|
|
|
|
case 'glob-index-family':
|
|
|
|
|
return {
|
|
|
|
|
includeSubfoldersMode: 'exclude',
|
|
|
|
|
fileFilterMode: 'glob',
|
|
|
|
|
fileFilterPattern: 'index*',
|
|
|
|
|
fileFilterTarget: 'basename',
|
|
|
|
|
fileFilterCaseSensitive: false,
|
|
|
|
|
fileFilterInvert: false
|
|
|
|
|
};
|
|
|
|
|
case 'glob-index-cross-folder':
|
|
|
|
|
return {
|
|
|
|
|
includeSubfoldersMode: 'include',
|
|
|
|
|
fileFilterMode: 'glob',
|
|
|
|
|
fileFilterPattern: '**/index*.md',
|
|
|
|
|
fileFilterTarget: 'relativePath',
|
|
|
|
|
fileFilterCaseSensitive: false,
|
|
|
|
|
fileFilterInvert: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveFolderTaskProfileFolderSelectionUiState(params: {
|
|
|
|
|
availableFolders: string[];
|
|
|
|
|
selectedProfile: FolderTaskFileSelectionProfile | null;
|
|
|
|
|
currentFolderPath?: string;
|
|
|
|
|
allowTemporaryFolderOverride?: boolean;
|
|
|
|
|
fallbackFolderPath?: string;
|
|
|
|
|
}): FolderTaskProfileFolderSelectionUiState {
|
|
|
|
|
const availableFolders = params.availableFolders;
|
|
|
|
|
const fallbackFolderPath = availableFolders.includes(params.fallbackFolderPath || '/')
|
|
|
|
|
? (params.fallbackFolderPath || '/')
|
|
|
|
|
: '/';
|
|
|
|
|
const hasExplicitCurrentFolderPath = typeof params.currentFolderPath === 'string'
|
|
|
|
|
&& availableFolders.includes(params.currentFolderPath);
|
|
|
|
|
const normalizedCurrentFolderPath = hasExplicitCurrentFolderPath
|
|
|
|
|
? params.currentFolderPath!
|
|
|
|
|
: fallbackFolderPath;
|
|
|
|
|
const profileFolderHint = params.selectedProfile?.folderPathHint || '';
|
|
|
|
|
const hasProfileFolderHint = Boolean(profileFolderHint) && availableFolders.includes(profileFolderHint);
|
|
|
|
|
|
|
|
|
|
if (!hasProfileFolderHint) {
|
|
|
|
|
return {
|
|
|
|
|
folderPath: normalizedCurrentFolderPath,
|
|
|
|
|
hasProfileFolderHint: false,
|
|
|
|
|
profileFolderHint: null,
|
|
|
|
|
allowTemporaryFolderOverride: false,
|
|
|
|
|
folderSelectionDisabled: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const allowTemporaryFolderOverride = Boolean(params.allowTemporaryFolderOverride);
|
|
|
|
|
if (!allowTemporaryFolderOverride) {
|
|
|
|
|
return {
|
|
|
|
|
folderPath: profileFolderHint,
|
|
|
|
|
hasProfileFolderHint: true,
|
|
|
|
|
profileFolderHint,
|
|
|
|
|
allowTemporaryFolderOverride: false,
|
|
|
|
|
folderSelectionDisabled: true
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
folderPath: hasExplicitCurrentFolderPath ? normalizedCurrentFolderPath : profileFolderHint,
|
|
|
|
|
hasProfileFolderHint: true,
|
|
|
|
|
profileFolderHint,
|
|
|
|
|
allowTemporaryFolderOverride: true,
|
|
|
|
|
folderSelectionDisabled: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 08:02:41 +00:00
|
|
|
export function resolveFolderTaskFileSelectionProfile(
|
|
|
|
|
settings: NotemdSettings,
|
|
|
|
|
override?: FolderTaskFileSelectionOverride
|
|
|
|
|
): FolderTaskFileSelectionProfile | null {
|
|
|
|
|
if (!override) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const profiles = getFolderTaskFileSelectionProfiles(settings);
|
|
|
|
|
const normalizedProfileId = typeof override.profileId === 'string' ? override.profileId.trim() : '';
|
|
|
|
|
if (normalizedProfileId) {
|
|
|
|
|
return profiles.find(profile => profile.id === normalizedProfileId) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const normalizedProfileName = typeof override.profileName === 'string' ? override.profileName.trim() : '';
|
|
|
|
|
if (normalizedProfileName) {
|
|
|
|
|
return profiles.find(profile => profile.name === normalizedProfileName) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createFolderTaskSelectionOverrideFromProfile(
|
|
|
|
|
profile: FolderTaskFileSelectionProfile
|
|
|
|
|
): FolderTaskFileSelectionOverride {
|
|
|
|
|
return {
|
|
|
|
|
profileId: profile.id,
|
|
|
|
|
includeSubfoldersMode: profile.includeSubfoldersMode,
|
|
|
|
|
fileFilterMode: profile.fileFilterMode,
|
|
|
|
|
fileFilterPattern: profile.fileFilterPattern,
|
|
|
|
|
fileFilterTarget: profile.fileFilterTarget,
|
|
|
|
|
fileFilterCaseSensitive: profile.fileFilterCaseSensitive,
|
|
|
|
|
fileFilterInvert: profile.fileFilterInvert
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function mergeFolderTaskSelectionOverrides(
|
|
|
|
|
base?: FolderTaskFileSelectionOverride,
|
|
|
|
|
override?: FolderTaskFileSelectionOverride
|
|
|
|
|
): FolderTaskFileSelectionOverride | undefined {
|
|
|
|
|
if (!base) {
|
|
|
|
|
return override;
|
|
|
|
|
}
|
|
|
|
|
if (!override) {
|
|
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...base,
|
|
|
|
|
...override
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getFolderTaskRegexValidationError(pattern: string, caseSensitive: boolean): string | null {
|
|
|
|
|
const normalizedPattern = pattern.trim();
|
|
|
|
|
if (!normalizedPattern) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Compile-only validation for deterministic syntax checking.
|
|
|
|
|
// eslint-disable-next-line no-new
|
|
|
|
|
new RegExp(normalizedPattern, caseSensitive ? undefined : 'i');
|
|
|
|
|
return null;
|
|
|
|
|
} catch (error: unknown) {
|
|
|
|
|
if (error instanceof Error && error.message) {
|
|
|
|
|
return error.message;
|
|
|
|
|
}
|
|
|
|
|
return 'Unknown regex syntax error';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function applyFolderTaskSelectionOverride(
|
|
|
|
|
settings: NotemdSettings,
|
|
|
|
|
override?: FolderTaskFileSelectionOverride
|
|
|
|
|
): NotemdSettings {
|
2026-05-26 10:36:26 +00:00
|
|
|
const runtimeSettings = resolveFolderTaskRuntimeSettings(settings);
|
|
|
|
|
if (!isAdvancedFolderTaskFileSelectionEnabled(settings)) {
|
|
|
|
|
return runtimeSettings;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 08:02:41 +00:00
|
|
|
if (!override) {
|
2026-05-26 10:36:26 +00:00
|
|
|
return runtimeSettings;
|
2026-05-25 08:02:41 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-26 10:36:26 +00:00
|
|
|
const profile = resolveFolderTaskFileSelectionProfile(runtimeSettings, override);
|
2026-05-25 08:02:41 +00:00
|
|
|
const baseSettings = profile
|
|
|
|
|
? {
|
2026-05-26 10:36:26 +00:00
|
|
|
...runtimeSettings,
|
2026-05-25 08:02:41 +00:00
|
|
|
folderTaskIncludeSubfoldersMode: profile.includeSubfoldersMode,
|
|
|
|
|
folderTaskFileFilterMode: profile.fileFilterMode,
|
|
|
|
|
folderTaskFileFilterPattern: profile.fileFilterPattern,
|
|
|
|
|
folderTaskFileFilterTarget: profile.fileFilterTarget,
|
|
|
|
|
folderTaskFileFilterCaseSensitive: profile.fileFilterCaseSensitive,
|
|
|
|
|
folderTaskFileFilterInvert: profile.fileFilterInvert
|
|
|
|
|
}
|
2026-05-26 10:36:26 +00:00
|
|
|
: runtimeSettings;
|
2026-05-25 08:02:41 +00:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...baseSettings,
|
|
|
|
|
folderTaskIncludeSubfoldersMode: override.includeSubfoldersMode ?? baseSettings.folderTaskIncludeSubfoldersMode,
|
|
|
|
|
folderTaskFileFilterMode: override.fileFilterMode ?? baseSettings.folderTaskFileFilterMode,
|
|
|
|
|
folderTaskFileFilterPattern: override.fileFilterPattern ?? baseSettings.folderTaskFileFilterPattern,
|
|
|
|
|
folderTaskFileFilterTarget: override.fileFilterTarget ?? baseSettings.folderTaskFileFilterTarget,
|
|
|
|
|
folderTaskFileFilterCaseSensitive: override.fileFilterCaseSensitive ?? baseSettings.folderTaskFileFilterCaseSensitive,
|
|
|
|
|
folderTaskFileFilterInvert: override.fileFilterInvert ?? baseSettings.folderTaskFileFilterInvert
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createFilterMatcher(filterConfig: FolderTaskFileFilterConfig): FolderTaskFilterMatcher {
|
|
|
|
|
const normalizedPattern = filterConfig.pattern.trim();
|
|
|
|
|
if (filterConfig.mode === 'none' || !normalizedPattern) {
|
|
|
|
|
return {
|
|
|
|
|
matcher: () => true,
|
|
|
|
|
noOp: true
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filterConfig.mode === 'contains') {
|
|
|
|
|
if (filterConfig.caseSensitive) {
|
|
|
|
|
return {
|
|
|
|
|
matcher: (value: string) => value.includes(normalizedPattern),
|
|
|
|
|
noOp: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const lowerPattern = normalizedPattern.toLowerCase();
|
|
|
|
|
return {
|
|
|
|
|
matcher: (value: string) => value.toLowerCase().includes(lowerPattern),
|
|
|
|
|
noOp: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (filterConfig.mode === 'regex') {
|
|
|
|
|
const validationError = getFolderTaskRegexValidationError(normalizedPattern, filterConfig.caseSensitive);
|
|
|
|
|
if (validationError) {
|
|
|
|
|
throw new Error(`Invalid folder task regex pattern "${normalizedPattern}": ${validationError}`);
|
|
|
|
|
}
|
|
|
|
|
const regex = new RegExp(normalizedPattern, filterConfig.caseSensitive ? undefined : 'i');
|
|
|
|
|
return {
|
|
|
|
|
matcher: (value: string) => regex.test(value),
|
|
|
|
|
noOp: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const globRegex = compileGlobPattern(normalizedPattern, filterConfig.caseSensitive);
|
|
|
|
|
return {
|
|
|
|
|
matcher: (value: string) => globRegex.test(value),
|
|
|
|
|
noOp: false
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveFilterTargetValue(
|
|
|
|
|
filterTarget: FolderTaskFileFilterTarget,
|
|
|
|
|
file: TFile,
|
|
|
|
|
relativePath: string
|
|
|
|
|
): string {
|
|
|
|
|
if (filterTarget === 'relativePath') {
|
|
|
|
|
return relativePath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof file.basename === 'string' && file.basename.length > 0) {
|
|
|
|
|
return file.basename;
|
|
|
|
|
}
|
|
|
|
|
return file.name.replace(/\.[^.]+$/, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveFileExtension(file: TFile): string {
|
|
|
|
|
if (typeof file.extension === 'string' && file.extension.length > 0) {
|
|
|
|
|
return file.extension.toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const name = typeof file.name === 'string' && file.name.length > 0
|
|
|
|
|
? file.name
|
|
|
|
|
: (file.path.split('/').pop() || file.path);
|
|
|
|
|
const dotIndex = name.lastIndexOf('.');
|
|
|
|
|
if (dotIndex < 0 || dotIndex === name.length - 1) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
return name.slice(dotIndex + 1).toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function selectFolderTaskFiles(options: FolderTaskFileSelectionOptions): TFile[] {
|
|
|
|
|
const folderPath = normalizeFolderPath(options.folderPath);
|
2026-05-26 10:36:26 +00:00
|
|
|
const runtimeSettings = resolveFolderTaskRuntimeSettings(options.settings);
|
2026-05-25 08:02:41 +00:00
|
|
|
const includeSubfolders = resolveIncludeSubfolders(
|
|
|
|
|
options.taskKind,
|
2026-05-26 10:36:26 +00:00
|
|
|
runtimeSettings.folderTaskIncludeSubfoldersMode
|
2026-05-25 08:02:41 +00:00
|
|
|
);
|
|
|
|
|
const allowedExtensions = new Set(options.allowedExtensions.map(ext => ext.toLowerCase()));
|
2026-05-26 10:36:26 +00:00
|
|
|
const filterConfig = resolveFilterConfig(runtimeSettings);
|
2026-05-25 08:02:41 +00:00
|
|
|
const filterMatcher = createFilterMatcher(filterConfig);
|
|
|
|
|
|
|
|
|
|
const selectedFiles: TFile[] = [];
|
|
|
|
|
for (const file of options.files) {
|
|
|
|
|
const extension = resolveFileExtension(file);
|
|
|
|
|
if (!allowedExtensions.has(extension)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const relativePath = getRelativePathWithinFolder(file.path, folderPath);
|
|
|
|
|
if (!relativePath) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!matchesFolderScope(relativePath, includeSubfolders)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (options.exclude && options.exclude(file, relativePath)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetValue = resolveFilterTargetValue(filterConfig.target, file, relativePath);
|
|
|
|
|
const matched = filterMatcher.matcher(targetValue);
|
|
|
|
|
const finalMatch = filterMatcher.noOp ? true : (filterConfig.invert ? !matched : matched);
|
|
|
|
|
if (finalMatch) {
|
|
|
|
|
selectedFiles.push(file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return selectedFiles;
|
|
|
|
|
}
|