Работает для тегов. Осталось прикрутить проверку fontmetters и правил regex.

Необходимо сделать окно настроек
This commit is contained in:
NoRFoLK 2025-01-19 21:03:52 +04:00
parent 973ad11daa
commit 464359f494
6 changed files with 3538 additions and 132 deletions

7
jest.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.js', '**/?(*.)+(spec|test).js'],
transform: {
'^.+\\.jsx?$': 'babel-jest',
},
};

74
main.test.ts Normal file
View File

@ -0,0 +1,74 @@
import { TFile, MetadataCache, Vault, Workspace, MarkdownView } from 'obsidian';
import MoveNotePlugin from './main';
describe('MoveNotePlugin', () => {
let plugin: MoveNotePlugin;
let app: { metadataCache: MetadataCache, vault: Vault, workspace: Workspace };
beforeEach(() => {
app = {
metadataCache: {
getFileCache: jest.fn()
} as unknown as MetadataCache,
vault: {} as Vault,
workspace: {
getActiveViewOfType: jest.fn()
} as unknown as Workspace
};
plugin = new MoveNotePlugin(app as any, {} as any);
});
it('should return tags from the provided file', async () => {
const file = { path: 'test.md' } as TFile;
const fileCache = {
frontmatter: { tags: ['#tag1', '#tag2'] },
tags: [{ tag: '#tag3' }]
};
(app.metadataCache.getFileCache as jest.Mock).mockReturnValue(fileCache);
const tags = await plugin.getTagsFromNote(file);
expect(tags).toEqual(['tag1', 'tag2', 'tag3']);
expect(app.metadataCache.getFileCache).toHaveBeenCalledWith(file);
});
it('should return tags from the active file if no file is provided', async () => {
const file = { path: 'active.md' } as TFile;
const fileCache = {
frontmatter: { tags: ['#tag1'] },
tags: [{ tag: '#tag2' }]
};
(app.workspace.getActiveViewOfType as jest.Mock).mockReturnValue({
file
} as MarkdownView);
(app.metadataCache.getFileCache as jest.Mock).mockReturnValue(fileCache);
const tags = await plugin.getTagsFromNote();
expect(tags).toEqual(['tag1', 'tag2']);
expect(app.metadataCache.getFileCache).toHaveBeenCalledWith(file);
});
it('should return undefined if no file is provided and no active file is found', async () => {
(app.workspace.getActiveViewOfType as jest.Mock).mockReturnValue(null);
const tags = await plugin.getTagsFromNote();
expect(tags).toBeUndefined();
expect(app.metadataCache.getFileCache).not.toHaveBeenCalled();
});
it('should return an empty array if no tags are found', async () => {
const file = { path: 'test.md' } as TFile;
const fileCache = {
frontmatter: { tags: [] },
tags: []
};
(app.metadataCache.getFileCache as jest.Mock).mockReturnValue(fileCache);
const tags = await plugin.getTagsFromNote(file);
expect(tags).toEqual([]);
expect(app.metadataCache.getFileCache).toHaveBeenCalledWith(file);
});
});

177
main.ts
View File

@ -1,39 +1,60 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, Tasks, TFile } from 'obsidian';
import { App, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
// Не забудьте переименовать эти классы и интерфейсы!
interface MyPluginSettings {
interface PluginConfiguration {
mySetting: string;
rules?: any; // Add the rules property
}
const DEFAULT_SETTINGS: MyPluginSettings = {
const DEFAULT_SETTINGS: PluginConfiguration = {
mySetting: 'default'
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
export default class MoveNotePlugin extends Plugin {
settings: PluginConfiguration;
/**
* Этот метод вызывается при загрузке плагина.
* Он выполняет следующие действия:
* - Загружает настройки плагина.
* - Отображает уведомление для пользователя.
* - Создает иконку в левой панели, при клике на которую извлекаются и отображаются теги из текущей заметки.
* - Добавляет команду в редактор для сканирования заметок в корневой папке и перемещения файлов с определенными тегами в временную папку.
* - Добавляет вкладку настроек для плагина.
* - Регистрирует интервал, который выводит сообщение в консоль каждые 5 минут.
*
* @returns {Promise<void>} Обещание, которое разрешается при завершении процесса загрузки.
*/
async onload() {
await this.loadSettings();
new Notice('This is a notice!');
const rules = this.settings.rules;
// Создает иконку в левой боковой панели.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
const ribbonIconEl = this.addRibbonIcon('dice', 'Move File', (evt: MouseEvent) => {
// Вызывается при клике на иконку.
this.findTagsInNote().then(tags => {
if (tags) {
new Notice('Tags: ' + tags.join(', '));
} else {
new Notice('No tags found.');
}
rules.forEach((rule: any) => {
const includeTags = rule?.include?.tags||[];
const excludeTags = rule?.exclude?.tags||[];
this.getTagsFromNote().then(tags => {
if (tags) {
const activeFile = this.app.workspace.getActiveFile();
if (activeFile) {
//вставить сюда код
if (includeTags.every((tag) => tags.includes(tag)) && !excludeTags.some((tag) => tags.includes(tag))) {//проверяем наличие всех тегов из списка в тегах файла
new Notice(`Удовлетворяет правилу ${rule.name}`);
this.moveFileToFolder(activeFile, rule.targetFolder);
}
} else {
new Notice('Не выбран активный файл.');
}
} else {
new Notice('Не удовлетворяет правилам для переноса');
}
});
});
});
});
// Добавляет дополнительные стили к иконке.
ribbonIconEl.addClass('my-plugin-ribbon-class');
// Добавляет элемент в статус-бар внизу приложения. Не работает в мобильных приложениях.
// const statusBarItemEl = this.addStatusBarItem();
// statusBarItemEl.setText('Status Bar Text');
// Добавляет команду для редактора, которая может выполнять операции с текущим экземпляром редактора.
this.addCommand({
id: 'scan-root-folder',
@ -41,26 +62,23 @@ export default class MyPlugin extends Plugin {
callback: async () => {
const fileList = await this.scanFolder();
fileList.forEach((file) => {
const tagsForScan = ['art', '🍆']
const tags = this.findTagsInNote(file);
tags.then((tags)=>{
if (tagsForScan.every(tag => (tags ?? []).includes(tag))) {
console.log('FOUND', file.name);
this.moveFileToFolder(file, 'temp')
}
this.settings.rules.forEach((rule: any) => {
const includeTags = rule?.include?.tags||[];
const excludeTags = rule?.exclude?.tags||[];
const tags = this.getTagsFromNote(file);
tags.then((tags)=>{//получаем теги из файла
if (includeTags.every((tag) => tags.includes(tag)) && !excludeTags.some((tag) => tags.includes(tag))) {//проверяем наличие всех тегов из списка в тегах файла
this.moveFileToFolder(file, rule.targetFolder)//перемещаем файл в папку
new Notice(` ${file.name} удовлетворяет правилу ${rule.name}`);
}
})
})
});
}
});
// Добавляет вкладку настроек, чтобы пользователь мог настроить различные аспекты плагина.
this.addSettingTab(new SampleSettingTab(this.app, this));
// Если плагин подключает глобальные события DOM (на частях приложения, которые не принадлежат этому плагину),
// использование этой функции автоматически удалит обработчик события при отключении плагина.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
// console.log('click', evt);
});
this.addSettingTab(new MoveNoteSettingTab(this.app, this));
// При регистрации интервалов эта функция автоматически очистит интервал при отключении плагина.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
@ -79,23 +97,12 @@ export default class MyPlugin extends Plugin {
}
/**
* Асинхронно находит и отображает теги в текущей активной Markdown заметке.
* Извлекает теги из указанного файла заметки или из текущего активного файла заметки.
*
* Этот метод получает активное представление типа `MarkdownView` из рабочей области Obsidian.
* Если активное представление не найдено, метод завершает выполнение.
*
* Затем он получает содержимое активного представления и выводит его в консоль.
*
* Метод продолжает получать файл, связанный с активным представлением. Если файл не найден, метод завершает выполнение.
*
* Используя файл, он извлекает кэш файла из метаданных Obsidian. Он извлекает теги как из frontmatter, так и из тела заметки.
* Теги очищаются путем удаления ведущего символа '#'.
*
* Наконец, метод выводит найденные теги в консоль и отображает уведомление с найденными тегами.
*
* @returns {Promise<void>} Обещание, которое разрешается, когда теги найдены и отображены.
* @param {TFile} [file] - Файл заметки, из которого нужно извлечь теги. Если не указан, будет использован текущий активный файл заметки.
* @returns {Promise<string[] | undefined>} Обещание, которое разрешается массивом уникальных тегов, найденных в файле заметки, или undefined, если файл не указан или не активен.
*/
async findTagsInNote(file?: TFile) {
async getTagsFromNote(file?: TFile) {
let activeFile = file;
if (!activeFile) {
@ -112,29 +119,29 @@ export default class MyPlugin extends Plugin {
return;
}
const content = await this.app.vault.read(activeFile);
// console.log('CONTENT \n', content);
const fileCache = this.app.metadataCache.getFileCache(activeFile);
// console.log('FILECACHE', fileCache);
const frontmatterTags = (fileCache?.frontmatter?.tag || []).map((tag) => {
return tag ? tag.replace(/#/g, '') : '';
const fileCache = this.app.metadataCache.getFileCache(activeFile);// Получает кэш-файл для указанного файла.
const frontmatterTags = (fileCache?.frontmatter?.tags || fileCache?.frontmatter?.tag || []).map((tag:string) => { // Извлекает теги из метаданных файла.
return tag ? tag.replace(/#/g, '') : ''; // Удаляет символ # из тега.
});
const fileCacheTags = (fileCache?.tags || []).map((tag) => {
return tag.tag ? tag.tag.replace(/#/g, '') : '';
const fileCacheTags = (fileCache?.tags || []).map((tag) => { // Извлекает теги из кэш-файла.
return tag.tag ? tag.tag.replace(/#/g, '') : ''; // Удаляет символ # из тега.
});
let tags = [...new Set([...frontmatterTags, ...fileCacheTags])];
// console.log('frontmatterTags', frontmatterTags);
// console.log('fileCacheTags', fileCacheTags);
// console.log('TAGS', tags);
// new Notice(`Found tags: ${tags.join(', ')}`);
let tags = [...new Set([...frontmatterTags, ...fileCacheTags])]; // Объединяет теги из метаданных и кэш-файла, удаляя дубликаты.
return tags;
}
async scanFolder(path?: string, recursive: boolean = true) {
/**
* Сканирует папку и возвращает список файлов Markdown.
*
* @param {string} [path] - Путь к папке, которую нужно сканировать. Если не указан, сканируется корневая папка.
* @param {boolean} [recursive=true] - Флаг, указывающий, нужно ли сканировать папку рекурсивно.
* @returns {Promise<TFile[]>} Обещание, которое разрешается массивом файлов Markdown, найденных в указанной папке.
*/
async scanFolder(path?: string, recursive: boolean = true): Promise<TFile[]> {
recursive = false;
if (!path || path === '/') {
path = '';
@ -146,43 +153,37 @@ export default class MyPlugin extends Plugin {
} else {
return file.path.startsWith(path) && file.path.split('/').length === path.split('/').length;
}
})
});
return filteredFiles;
}
/**
* Перемещает файл в указанную целевую папку в хранилище.
*
* @param file - Файл, который нужно переместить.
* @param targetFolder - Путь к целевой папке, куда нужно переместить файл.
* @returns Обещание, которое разрешается, когда файл успешно перемещен.
* @throws Выбрасывает ошибку, если файл не может быть перемещен.
*/
async moveFileToFolder(file: TFile, targetFolder: string) {
try {
// Переименовываем файл, чтобы переместить его в целевую
const newPath = `${targetFolder}/${file.name}`;
await this.app.vault.rename(file, newPath);
console.log(`Moved ${file.name} to ${newPath}`);
}
catch (e) {
console.error(`Failed to move file ${file.name} to ${targetFolder}` ,e);
new Notice(`Файл перемещен ${file.name} to ${newPath}`);
} catch (e) {
new Notice(`Failed to move file ${file.name} to ${targetFolder}`);
console.error(e);
}
}
}
class SampleModal extends Modal {
constructor(app: App) {
super(app);
}
class MoveNoteSettingTab extends PluginSettingTab {
plugin: MoveNotePlugin;
onOpen() {
const {contentEl} = this;
contentEl.setText('Woah!');
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
constructor(app: App, plugin: MoveNotePlugin) {
super(app, plugin);
this.plugin = plugin;
}

View File

@ -1,10 +1,10 @@
{
"id": "AAA-sample-plugin",
"name": "AA Sample Plugin",
"version": "1.0.0",
"id": "AAA-move-note-plugin",
"name": "AA Move Note Plugin",
"version": "0.0.1a",
"minAppVersion": "0.15.0",
"description": "Demonstrates some of the capabilities of the Obsidian API.",
"author": "Obsidian",
"author": "NoRFoLK",
"authorUrl": "https://obsidian.md",
"fundingUrl": "https://obsidian.md/pricing",
"isDesktopOnly": false

3399
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,12 +12,15 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"jest": "^29.7.0",
"obsidian": "latest",
"ts-jest": "^29.2.5",
"tslib": "2.4.0",
"typescript": "4.7.4"
}