Files
note2any/src/batch-filter.ts
2025-09-25 22:35:01 +08:00

247 lines
8.0 KiB
TypeScript

/*
* 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<TFile[]> {
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'
};
}
}