Работает для тегов. Осталось прикрутить проверку 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);
});
});

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

View File

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

3399
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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