/* * Copyright (c) 2024-2025 Sun Booshi * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import { App, TFile, MetadataCache } from 'obsidian'; export interface FilterCondition { type: 'tag' | 'filename' | 'folder' | 'frontmatter'; operator: 'contains' | 'equals' | 'startsWith' | 'endsWith' | 'exists'; value?: string; key?: string; // for frontmatter } export interface BatchFilterConfig { conditions: FilterCondition[]; logic: 'and' | 'or'; orderBy?: 'name' | 'created' | 'modified'; orderDirection?: 'asc' | 'desc'; } export class BatchArticleFilter { private app: App; private metadataCache: MetadataCache; constructor(app: App) { this.app = app; this.metadataCache = app.metadataCache; } /** * 筛选文章 */ async filterArticles(config: BatchFilterConfig): Promise { const allMarkdownFiles = this.app.vault.getMarkdownFiles(); const filtered = allMarkdownFiles.filter(file => this.matchesConditions(file, config)); return this.sortFiles(filtered, config.orderBy || 'name', config.orderDirection || 'asc'); } /** * 检查文件是否匹配条件 */ private matchesConditions(file: TFile, config: BatchFilterConfig): boolean { const { conditions, logic } = config; if (conditions.length === 0) return true; const results = conditions.map(condition => this.checkCondition(file, condition)); if (logic === 'and') { return results.every(result => result); } else { return results.some(result => result); } } /** * 检查单个条件 */ private checkCondition(file: TFile, condition: FilterCondition): boolean { const { type, operator, value, key } = condition; switch (type) { case 'tag': return this.checkTagCondition(file, operator, value); case 'filename': return this.checkFilenameCondition(file, operator, value); case 'folder': return this.checkFolderCondition(file, operator, value); case 'frontmatter': return this.checkFrontmatterCondition(file, operator, key, value); default: return false; } } /** * 检查标签条件 */ private checkTagCondition(file: TFile, operator: string, value?: string): boolean { if (!value) return false; const fileCache = this.metadataCache.getFileCache(file); const tags = fileCache?.tags?.map(t => t.tag.replace('#', '')) || []; const frontmatterTags = fileCache?.frontmatter?.tags || []; const allTags = [...tags, ...frontmatterTags].map(tag => typeof tag === 'string' ? tag : String(tag) ); switch (operator) { case 'contains': return allTags.some(tag => tag.includes(value)); case 'equals': return allTags.includes(value); case 'exists': return allTags.length > 0; default: return false; } } /** * 检查文件名条件 */ private checkFilenameCondition(file: TFile, operator: string, value?: string): boolean { if (!value) return false; const filename = file.basename; switch (operator) { case 'contains': return filename.includes(value); case 'equals': return filename === value; case 'startsWith': return filename.startsWith(value); case 'endsWith': return filename.endsWith(value); default: return false; } } /** * 检查文件夹条件 */ private checkFolderCondition(file: TFile, operator: string, value?: string): boolean { if (!value) return false; const folderPath = file.parent?.path || ''; switch (operator) { case 'contains': return folderPath.includes(value); case 'equals': return folderPath === value; case 'startsWith': return folderPath.startsWith(value); default: return false; } } /** * 检查 frontmatter 条件 */ private checkFrontmatterCondition(file: TFile, operator: string, key?: string, value?: string): boolean { if (!key) return false; const fileCache = this.metadataCache.getFileCache(file); const frontmatter = fileCache?.frontmatter; if (!frontmatter) return false; const fieldValue = frontmatter[key]; switch (operator) { case 'exists': return fieldValue !== undefined; case 'equals': return String(fieldValue) === value; case 'contains': return value ? String(fieldValue).includes(value) : false; default: return false; } } /** * 排序文件 */ private sortFiles(files: TFile[], orderBy: string, direction: string): TFile[] { return files.sort((a, b) => { let compareResult = 0; switch (orderBy) { case 'name': compareResult = a.basename.localeCompare(b.basename); break; case 'created': compareResult = a.stat.ctime - b.stat.ctime; break; case 'modified': compareResult = a.stat.mtime - b.stat.mtime; break; default: compareResult = a.basename.localeCompare(b.basename); } return direction === 'desc' ? -compareResult : compareResult; }); } /** * 从类似 database view 的配置创建筛选条件 */ static fromDatabaseConfig(config: any): BatchFilterConfig { const conditions: FilterCondition[] = []; if (config.filters?.and) { for (const filter of config.filters.and) { if (filter['file.tags.contains']) { conditions.push({ type: 'tag', operator: 'contains', value: filter['file.tags.contains'] }); } // 可以扩展更多条件类型 } } let orderBy: 'name' | 'created' | 'modified' = 'name'; if (config.order?.[0] === 'file.name') { orderBy = 'name'; } else if (config.order?.[0] === 'file.ctime') { orderBy = 'created'; } else if (config.order?.[0] === 'file.mtime') { orderBy = 'modified'; } return { conditions, logic: 'and', orderBy, orderDirection: 'asc' }; } }