247 lines
8.0 KiB
TypeScript
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'
|
|
};
|
|
}
|
|
} |