Работает для тегов. Осталось прикрутить проверку fontmetters и правил regex.
Необходимо сделать окно настроек
This commit is contained in:
parent
973ad11daa
commit
464359f494
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testMatch: ['**/__tests__/**/*.test.js', '**/?(*.)+(spec|test).js'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.jsx?$': 'babel-jest',
|
||||||
|
},
|
||||||
|
};
|
|
@ -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
177
main.ts
|
@ -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 fileCache = this.app.metadataCache.getFileCache(activeFile);// Получает кэш-файл для указанного файла.
|
||||||
// console.log('FILECACHE', fileCache);
|
const frontmatterTags = (fileCache?.frontmatter?.tags || fileCache?.frontmatter?.tag || []).map((tag:string) => { // Извлекает теги из метаданных файла.
|
||||||
|
return tag ? tag.replace(/#/g, '') : ''; // Удаляет символ # из тега.
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue