mirror of
https://github.com/groldsf/obsidian_check_plugin.git
synced 2026-07-22 05:37:48 +00:00
feat(filter): Implement file filtering using 'ignore' library
This commit is contained in:
parent
8ac4b60509
commit
683b358053
7 changed files with 326 additions and 56 deletions
189
__tests__/FileFilter.test.ts
Normal file
189
__tests__/FileFilter.test.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import { FileFilter } from "src/FileFilter";
|
||||
import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "src/types";
|
||||
|
||||
const createSettings = (pathGlobs: string[]): Readonly<CheckboxSyncPluginSettings> => ({
|
||||
...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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
52
package-lock.json
generated
52
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.5.0"
|
||||
"async-mutex": "^0.5.0",
|
||||
"ignore": "^7.0.4"
|
||||
}
|
||||
}
|
||||
44
src/FileFilter.ts
Normal file
44
src/FileFilter.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { CheckboxSyncPluginSettings } from './types';
|
||||
import ignore, { Ignore } from 'ignore';
|
||||
|
||||
export class FileFilter {
|
||||
private settings: Readonly<CheckboxSyncPluginSettings>;
|
||||
private ignorer: Ignore | null = null;
|
||||
|
||||
constructor(settings: Readonly<CheckboxSyncPluginSettings>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,32 +27,22 @@ export class PathGlobsSettingComponent extends BaseSettingComponent {
|
|||
const multilineStringValue = this.arrayToMultilineString(currentValue as string[] | undefined);
|
||||
|
||||
const descHtml =
|
||||
`<p>List files or folders that the plugin should IGNORE.
|
||||
This uses "glob patterns" (similar to <code>.gitignore</code> syntax) to match paths.</p>
|
||||
<ul>
|
||||
<li>Each pattern on a new line. If this list is empty, no files are ignored (plugin processes all).</li>
|
||||
<li>Example to IGNORE: <code>My Folder/</code> or <code>*.log</code></li>
|
||||
<li>Example to PROCESS an item within an IGNORED path: <code>!My Folder/My Important File.md</code></li>
|
||||
<li>The last rule that matches a file determines its fate.</li>
|
||||
<li>For more on glob syntax, search "glob patterns online" or see
|
||||
<a href="https://en.wikipedia.org/wiki/Glob_(programming)" target="_blank" rel="noopener noreferrer">Glob (programming) on Wikipedia</a>.
|
||||
</li>
|
||||
</ul>`;
|
||||
`<p>Define rules to specify which files and folders the plugin should IGNORE.
|
||||
Uses the same pattern syntax as <code>.gitignore</code> files (powered by the 'ignore' library).</p>
|
||||
<p>For detailed syntax, refer to the
|
||||
<a href="https://git-scm.com/docs/gitignore" target="_blank" rel="noopener noreferrer">official .gitignore documentation</a>.
|
||||
</p>`;
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue