update at 2025-09-25 22:35:01
This commit is contained in:
247
src/batch-filter.ts
Normal file
247
src/batch-filter.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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'
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user