From 683b358053ef7fc1869b0aff9d2559fb020f4ed1 Mon Sep 17 00:00:00 2001 From: Grol Grol Date: Sun, 18 May 2025 18:21:50 +0300 Subject: [PATCH] feat(filter): Implement file filtering using 'ignore' library --- .gitignore | 48 ++--- __tests__/FileFilter.test.ts | 189 ++++++++++++++++++ package-lock.json | 52 ++++- package.json | 5 +- src/FileFilter.ts | 44 ++++ .../pathGroup/PathGlobsSettingComponent.ts | 28 +-- tsconfig.json | 16 +- 7 files changed, 326 insertions(+), 56 deletions(-) create mode 100644 __tests__/FileFilter.test.ts create mode 100644 src/FileFilter.ts diff --git a/.gitignore b/.gitignore index ad1b3fe..295c0d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,24 @@ -# vscode -.vscode - -# Intellij -*.iml -.idea - -# npm -node_modules - -# Don't include the compiled main.js file in the repo. -# They should be uploaded to GitHub releases instead. -main.js - -# Exclude sourcemaps -*.map - -# obsidian -data.json - -# Exclude macOS Finder (System Explorer) View States -.DS_Store - -coverage/ +# vscode +.vscode + +# Intellij +*.iml +.idea + +# npm +node_modules + +# Don't include the compiled main.js file in the repo. +# They should be uploaded to GitHub releases instead. +main.js + +# Exclude sourcemaps +*.map + +# obsidian +data.json + +# Exclude macOS Finder (System Explorer) View States +.DS_Store + +coverage/ diff --git a/__tests__/FileFilter.test.ts b/__tests__/FileFilter.test.ts new file mode 100644 index 0000000..3e552c4 --- /dev/null +++ b/__tests__/FileFilter.test.ts @@ -0,0 +1,189 @@ +import { FileFilter } from "src/FileFilter"; +import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "src/types"; + +const createSettings = (pathGlobs: string[]): Readonly => ({ + ...DEFAULT_SETTINGS, + pathGlobs, +}); + +describe('FileFilter with "ignore" library (.gitignore-style logic)', () => { + describe('isPathAllowed should return true if file is NOT ignored, false if IGNORED', () => { + const testCases = [ + // --- Базовые случаи --- + { name: 'No globs: any/file.md', globs: [], path: 'any/file.md', expected: true }, // Not ignored + { name: 'Empty glob string: any/file.md', globs: [''], path: 'any/file.md', expected: true }, // Not ignored + { name: 'Comment only: any/file.md', globs: ['#comment'], path: 'any/file.md', expected: true }, // Not ignored + { name: 'Empty and comment: any/file.md', globs: ['', '#comment'], path: 'any/file.md', expected: true }, // Not ignored + + // --- Простое исключение (файл ИГНОРИРУЕТСЯ -> isPathAllowed: false) --- + { name: 'Exclude folder logs/: logs/error.log', globs: ['logs/'], path: 'logs/error.log', expected: false }, + { name: 'Exclude folder logs/: data/file.md (not in folder)', globs: ['logs/'], path: 'data/file.md', expected: true }, + { name: 'Exclude *.tmp: file.tmp', globs: ['*.tmp'], path: 'file.tmp', expected: false }, + { name: 'Exclude *.tmp: file.md (different type)', globs: ['*.tmp'], path: 'file.md', expected: true }, + { name: 'Exclude secret.md: secret.md', globs: ['secret.md'], path: 'secret.md', expected: false }, + + // --- Правило: "Нельзя пере-включить файл, если его родительская директория исключена" --- + // Тесты, которые раньше падали из-за моего неверного expected: true + { + name: 'Parent Excluded 1: logs/, !logs/important.log -> logs/important.log', + globs: ['logs/', '!logs/important.log'], + path: 'logs/important.log', + expected: false // Stays ignored because 'logs/' is excluded + }, + { + name: 'Parent Excluded 2: docs/, !docs/README.md -> docs/README.md', + globs: ['docs/', '!docs/README.md'], + path: 'docs/README.md', + expected: false // Stays ignored + }, + { + name: 'Parent Excluded 3: *, !projects/** -> projects/my/file.md', + globs: ['*', '!projects/**'], + path: 'projects/my/file.md', + expected: false // Stays ignored because 'projects/' (or '*' matching it) is excluded + }, + { + name: 'Parent Excluded 4: .obsidian/, !.obsidian/config -> .obsidian/config', + globs: ['.obsidian/', '!.obsidian/config'], + path: '.obsidian/config', + expected: false // Stays ignored + }, + + // --- Случаи, где правило о родительской директории НЕ применяется, и ! отменяет предыдущее --- + // Здесь мы не исключаем родительскую директорию целиком, а только файлы по паттерну + { + name: 'Negation Works 1: *.js, !src/safe.js -> src/safe.js', + globs: ['*.js', '!src/safe.js'], + path: 'src/safe.js', + expected: true // Not ignored + }, + { + name: 'Negation Works 2: *.js, !src/safe.js -> lib/bad.js', + globs: ['*.js', '!src/safe.js'], + path: 'lib/bad.js', + expected: false // Ignored by *.js + }, + { + name: 'Negation Works 3: specific.md, !specific.md -> specific.md', + globs: ['specific.md', '!specific.md'], + path: 'specific.md', + expected: true // Not ignored + }, + + + // --- Проверка правила о родительской директории более явно --- + // Сначала исключаем родителя, потом пытаемся включить ребенка И родителя + { + name: 'Parent Excluded then Re-include Parent: logs/, !logs/ -> logs/important.log', + globs: ['logs/', '!logs/'], // !logs/ отменяет logs/ для самой папки logs и ее содержимого + path: 'logs/important.log', + expected: true // logs/important.log НЕ игнорируется + }, + { + name: 'Parent Excluded then Re-include Parent & Child: logs/, !logs/, !logs/important.log -> logs/important.log', + globs: ['logs/', '!logs/', '!logs/important.log'], // !logs/important.log здесь избыточно, но для теста + path: 'logs/important.log', + expected: true // НЕ игнорируется + }, + // Исключаем папку, потом пытаемся включить подпапку - НЕ СРАБОТАЕТ для файлов ВНУТРИ подпапки + { + name: 'Exclude parent, try un-ignore sub-folder: top/, !top/mid/ -> top/mid/file.txt', + globs: ['top/', '!top/mid/'], + path: 'top/mid/file.txt', + expected: false // 'top/' excluded, so 'top/mid/file.txt' is ignored. '!top/mid/' cannot un-ignore contents. + }, + // Но если мы хотим работать с top/mid/, нам нужно отменить top/ СНАЧАЛА для top/mid/ + { + name: 'Un-ignore sub-folder first, then parent: !top/mid/, top/ -> top/mid/file.txt', + globs: ['!top/mid/', 'top/'], // 'top/' will re-ignore 'top/mid/file.txt'. Last rule wins if parent rule is not violated. + path: 'top/mid/file.txt', + expected: false + }, + { + name: 'Un-ignore sub-folder from wildcard: *, !top/mid/** -> top/mid/file.txt', + globs: ['*', '!top/mid/**'], // '*' excludes 'top/', then '!top/mid/**' tries to un-exclude. Parent 'top/' is excluded. + path: 'top/mid/file.txt', + expected: false // Parent 'top/' is excluded by '*', so '!top/mid/**' has no effect on contents. + }, + { + name: 'Correct way for "only top/mid/**" (other file in top): *, !top/, top/*, !top/mid/** -> top/other.txt', + globs: ['*', '!top/', 'top/*', '!top/mid/**'], + path: 'top/other.txt', + expected: false // Ignored by 'top/*' + }, + + + // --- Другие случаи с порядком (должны работать как раньше, т.к. не нарушают правило о родителях) --- + { + name: 'Order matters: !file.md, file.md -> file.md', + globs: ['!file.md', 'file.md'], + path: 'file.md', + expected: false // Ignored by 'file.md' (last rule) + }, + + // --- Случай "только это" (перепроверяем с учетом правила о родителях) --- + // Globs: ["*", "!projects/**"], Path: "projects/my/file.md" + // '*' excludes 'projects/'. So '!projects/**' cannot re-include contents. + // Expected: false + // (Этот тест уже был в секции Parent Excluded и остался false) + + // --- Файлы с точкой (dot files/folders) --- + // Globs: [".obsidian/", "!.obsidian/config"], Path: ".obsidian/config" + // '.obsidian/' excludes parent. + // Expected: false + // (Этот тест уже был в секции Parent Excluded и остался false) + + // --- "Противоречивые" случаи --- + { + name: 'Contradictory 1: !src/**, *.js -> src/subdir/file.js', + globs: ['!src/**', '*.js'], // !src/** makes src/ not ignored. Then *.js ignores file.js. + path: 'src/subdir/file.js', + expected: false + }, + { + name: 'Contradictory 1: !src/**, *.js -> src/subdir/file.txt', + globs: ['!src/**', '*.js'], // !src/** makes src/ not ignored. *.js does not match. + path: 'src/subdir/file.txt', + expected: true + }, + { + name: 'Contradictory 2: *.js, !src/** -> src/file.js', + globs: ['*.js', '!src/**'], // *.js ignores src/file.js. !src/** makes src/ (and its contents) not ignored. Last rule wins. + path: 'src/file.js', + expected: true + }, + { + name: 'Contradictory 2: *.js, !src/** -> lib/file.js', + globs: ['*.js', '!src/**'], // *.js ignores lib/file.js. !src/** does not match. + path: 'lib/file.js', + expected: false + }, + + // --- Только правило на разрешение --- + { + name: 'Only allow rule !important.txt: other.txt', + globs: ['!important.txt'], // No rule ignores other.txt. !important.txt does not match. + path: 'other.txt', + expected: true // Not ignored + }, + { + name: 'Only allow rule !important.txt: important.txt', + globs: ['!important.txt'], // !important.txt matches, makes it not ignored. + path: 'important.txt', + expected: true // Not ignored + }, + ]; + + testCases.forEach(tc => { + it(`should correctly process: ${tc.name} (path: "${tc.path}", globs: ${JSON.stringify(tc.globs)})`, () => { + const filter = new FileFilter(createSettings(tc.globs)); + const received = filter.isPathAllowed(tc.path); + + // Логирование только для упавших тестов (убрано из предыдущей версии, т.к. expect сам покажет) + // if (received !== tc.expected) { ... } + + expect(received).toBe(tc.expected); + }); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index ce0380a..7c4c947 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "license": "0BSD", "dependencies": { - "async-mutex": "^0.5.0" + "async-mutex": "^0.5.0", + "ignore": "^7.0.4" }, "devDependencies": { "@types/jest": "^29.5.14", @@ -1085,6 +1086,17 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/@eslint/js": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", @@ -1892,6 +1904,16 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz", @@ -3027,6 +3049,17 @@ "node": ">=4.0" } }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -3510,6 +3543,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3566,10 +3609,9 @@ } }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", "license": "MIT", "engines": { "node": ">= 4" diff --git a/package.json b/package.json index 09e08ff..310d583 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "typescript": "4.7.4" }, "dependencies": { - "async-mutex": "^0.5.0" + "async-mutex": "^0.5.0", + "ignore": "^7.0.4" } -} \ No newline at end of file +} diff --git a/src/FileFilter.ts b/src/FileFilter.ts new file mode 100644 index 0000000..4aa4e17 --- /dev/null +++ b/src/FileFilter.ts @@ -0,0 +1,44 @@ +import { CheckboxSyncPluginSettings } from './types'; +import ignore, { Ignore } from 'ignore'; + +export class FileFilter { + private settings: Readonly; + private ignorer: Ignore | null = null; + + constructor(settings: Readonly) { + this.settings = settings; + this.initIgnorer(); + } + + private initIgnorer(): void { + const globs = this.settings.pathGlobs; + if (globs && globs.length > 0) { + this.ignorer = ignore(); + // Добавляем только непустые строки и не комментарии + const validGlobs = globs.filter(g => g && g.trim() !== '' && !g.trim().startsWith('#')); + if (validGlobs.length > 0) { + this.ignorer.add(validGlobs); + } else { + // Если все глобы были пустыми или комментариями + this.ignorer = null; + } + } else { + this.ignorer = null; + } + } + + /** + * Checks if a given file path is allowed based on the pathGlobs setting + * using .gitignore-like logic provided by the 'ignore' library. + * @param filePath The file path to check (relative to the vault root). + * @returns True if the file is allowed for processing (i.e., NOT ignored), false otherwise. + */ + public isPathAllowed(filePath: string): boolean { + if (!this.ignorer) { + return true; // Если нет правил (или все были комментариями), все разрешено + } + // Метод ignorer.ignores(filePath) вернет true, если файл должен быть ИГНОРИРОВАН. + // Нам нужно инвертировать это, чтобы получить "разрешен ли файл". + return !this.ignorer.ignores(filePath); + } +} diff --git a/src/ui/components/pathGroup/PathGlobsSettingComponent.ts b/src/ui/components/pathGroup/PathGlobsSettingComponent.ts index 3c317f1..b63b0cb 100644 --- a/src/ui/components/pathGroup/PathGlobsSettingComponent.ts +++ b/src/ui/components/pathGroup/PathGlobsSettingComponent.ts @@ -27,32 +27,22 @@ export class PathGlobsSettingComponent extends BaseSettingComponent { const multilineStringValue = this.arrayToMultilineString(currentValue as string[] | undefined); const descHtml = - `

List files or folders that the plugin should IGNORE. - This uses "glob patterns" (similar to .gitignore syntax) to match paths.

-
    -
  • Each pattern on a new line. If this list is empty, no files are ignored (plugin processes all).
  • -
  • Example to IGNORE: My Folder/ or *.log
  • -
  • Example to PROCESS an item within an IGNORED path: !My Folder/My Important File.md
  • -
  • The last rule that matches a file determines its fate.
  • -
  • For more on glob syntax, search "glob patterns online" or see - Glob (programming) on Wikipedia. -
  • -
`; + `

Define rules to specify which files and folders the plugin should IGNORE. + Uses the same pattern syntax as .gitignore files (powered by the 'ignore' library).

+

For detailed syntax, refer to the + official .gitignore documentation. +

`; this.setting = new Setting(container) - .setName('Ignored Files & Folders') + .setName('File Ignore Rules (.gitignore style)') // Используем createFragmentWithHTML для описания .setDesc(this.createFragmentWithHTML(descHtml)) .addTextArea(text => { this.textInput = text; text.setPlaceholder( - '# Lines starting with # are comments and will be ignored\n' + - 'Drafts/\n' + - '*.tmp\n' + - '!Drafts/ReadyToPublish.md\n\n' + - '# To process ONLY files in "Projects" folder:\n' + - '*\n' + - '!Projects/**' + '# Examples:\n' + + '# Ignore all files in the "Drafts" folder\n' + + 'Drafts/\n\n' ); text.setValue(multilineStringValue); diff --git a/tsconfig.json b/tsconfig.json index e35b253..b8340ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "ES6", + "target": "ES2018", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", @@ -13,18 +13,22 @@ "strictNullChecks": true, "lib": [ "DOM", - "ES5", - "ES6", - "ES7" + "ES2018", + "DOM.Iterable" ], "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + // "strict": true, // Включение этой опции активирует множество строгих проверок (включая noImplicitAny, strictNullChecks, и др.) - очень рекомендуется для новых проектов. Если сейчас вызовет много ошибок, можно добавлять постепенно. // "rootDir": "src", // Указываем, где исходный код // "outDir": "dist", // Куда компилировать - "resolveJsonModule": true // Поддержка импорта JSON файлов + "resolveJsonModule": true, // Поддержка импорта JSON файлов + "skipLibCheck": true, }, "include": [ "**/*.ts", "src/**/*.ts", // Включаем только исходники из папки src "__tests__/**/*.ts" // Включаем тесты из папки __tests__ - ] + ], + "exclude": ["node_modules"], }