Skip to content

Commit af2424d

Browse files
feat: Added the option to ignore files and folders via settings (#289)
* added rudimentary support for ignoring files * Added File Ignore Settings * Increased width of the ignore file settings textarea * fixed issue that occured when ignore settings were not initialized * fixed problem with loading settings if ignore settings don't exist * bumped version for release in niposch/obsidian_to_anki * Style.css and manifest are copied to build output folder * created prerelease 3.5.0-prerelease2 * handle undefined value when setting isn't initialized properly * reverted changes to rollup config * added missing dependency * reverted change to format.ts * added tests for ignore setting * Extended ignore_setting test to test if scandir and ignore setting work properly together * Added default glob for excalidraw; description in readme and plugin setting * fixed python tests * Squashed commit of the following: commit 867f230 Author: niposch <niposch@gmail.com> Date: Tue Jan 23 16:34:09 2024 +0100 fixed python tests * revert folder scan changes * fix ignore_setting.py * Update README.md * revert changes in test_folder_scan --------- Co-authored-by: Harsha Raghu <narnindi.raghu@gmail.com>
1 parent fd29403 commit af2424d

16 files changed

Lines changed: 375 additions & 6 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ Current features (check out the wiki for more details):
6363
* **Custom scan directory**
6464
* The plugin will scan the entire vault by default
6565
* You can also set which directory (includes all sub-directories as well) to scan via plugin settings
66+
* **Ignore Folders and Files**
67+
* You can specify which files and folders to ignore
68+
* This can be done in the settings of this plugin with [Glob syntax](https://en.wikipedia.org/wiki/Glob_(programming)#Syntax).
69+
* If you're working on your own globs, you can test them out [here](https://globster.xyz/)
70+
* Examples:
71+
* `**/*.excalidraw.md` - Ignore all files that end in `.excalidraw.md`
72+
* => avoids excalidraw files from being scanned which can be extremely slow
73+
* `Template/**` - Ignore all files in the `Template` folder (including subfolders)
74+
* `**/private/**` - Ignore all files in folders that are called `private` no matter where they are in the vault
75+
* `[Pp]rivate*/**` - Ignore all files and folders in the root of the vault that start with `private` or with `Private`
6676
* **Updating notes from file** - Your text files are the canonical source of the notes.
6777
* **Tags**, including **tags for an entire file**.
6878
* **Adding to user-specified deck** on a *per-file* basis.

main.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Notice, Plugin, addIcon, TFile, TFolder } from 'obsidian'
22
import * as AnkiConnect from './src/anki'
33
import { PluginSettings, ParsedSettings } from './src/interfaces/settings-interface'
4-
import { SettingsTab } from './src/settings'
4+
import { DEFAULT_IGNORED_FILE_GLOBS, SettingsTab } from './src/settings'
55
import { ANKI_ICON } from './src/constants'
66
import { settingToData } from './src/setting-to-data'
77
import { FileManager } from './src/files-manager'
@@ -42,7 +42,8 @@ export default class MyPlugin extends Plugin {
4242
"CurlyCloze - Highlights to Clozes": false,
4343
"ID Comments": true,
4444
"Add Obsidian Tags": false,
45-
}
45+
},
46+
IGNORED_FILE_GLOBS: DEFAULT_IGNORED_FILE_GLOBS,
4647
}
4748
/*Making settings from scratch, so need note types*/
4849
this.note_types = await AnkiConnect.invoke('modelNames') as Array<string>

package-lock.json

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
},
4141
"dependencies": {
4242
"byte-base64": "^1.1.0",
43+
"multimatch": "^7.0.0",
4344
"showdown": "^2.1.0",
4445
"ts-md5": "^1.2.7",
4546
"webdriver": "^8.5.5"

src/files-manager.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { App, TFile, TFolder, TAbstractFile, CachedMetadata, FileSystemAdapter,
44
import { AllFile } from './file'
55
import * as AnkiConnect from './anki'
66
import { basename } from 'path'
7-
7+
import multimatch from "multimatch"
88
interface addNoteResponse {
99
result: number,
1010
error: string | null
@@ -63,16 +63,25 @@ export class FileManager {
6363
constructor(app: App, data:ParsedSettings, files: TFile[], file_hashes: Record<string, string>, added_media: string[]) {
6464
this.app = app
6565
this.data = data
66-
this.files = files
66+
67+
this.files = this.findFilesThatAreNotIgnored(files, data);
68+
6769
this.ownFiles = []
6870
this.file_hashes = file_hashes
6971
this.added_media_set = new Set(added_media)
7072
}
71-
7273
getUrl(file: TFile): string {
7374
return "obsidian://open?vault=" + encodeURIComponent(this.data.vault_name) + String.raw`&file=` + encodeURIComponent(file.path)
7475
}
7576

77+
findFilesThatAreNotIgnored(files:TFile[], data:ParsedSettings):TFile[]{
78+
let ignoredFiles = []
79+
ignoredFiles = multimatch(files.map(file => file.path), data.ignored_file_globs)
80+
81+
let notIgnoredFiles = files.filter(file => !ignoredFiles.contains(file.path))
82+
return notIgnoredFiles;
83+
}
84+
7685
getFolderPathList(file: TFile): TFolder[] {
7786
let result: TFolder[] = []
7887
let abstractFile: TAbstractFile = file

src/interfaces/settings-interface.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export interface PluginSettings {
2828
"CurlyCloze - Highlights to Clozes": boolean,
2929
"ID Comments": boolean,
3030
"Add Obsidian Tags": boolean
31-
}
31+
},
32+
IGNORED_FILE_GLOBS:string[]
3233
}
3334

3435
export interface FileData {
@@ -59,4 +60,5 @@ export interface ParsedSettings extends FileData {
5960
add_file_link: boolean
6061
folder_decks: Record<string, string>
6162
folder_tags: Record<string, string>
63+
ignored_file_globs: string[]
6264
}

src/setting-to-data.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export async function settingToData(app: App, settings: PluginSettings, fields_d
4242
result.comment = settings.Defaults["ID Comments"]
4343
result.add_context = settings.Defaults["Add Context"]
4444
result.add_obs_tags = settings.Defaults["Add Obsidian Tags"]
45+
result.ignored_file_globs = settings.IGNORED_FILE_GLOBS ?? [];
4546

4647
return result
4748
}

src/settings.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ const defaultDescs = {
1414
"Add Obsidian Tags": "Interpret #tags in the fields of a note as Anki tags, removing them from the note text in Anki."
1515
}
1616

17+
export const DEFAULT_IGNORED_FILE_GLOBS = [
18+
'**/*.excalidraw.md'
19+
];
20+
1721
export class SettingsTab extends PluginSettingTab {
1822

1923
setup_custom_regexp(note_type: string, row_cells: HTMLCollection) {
@@ -397,6 +401,35 @@ export class SettingsTab extends PluginSettingTab {
397401
}
398402
)
399403
}
404+
setup_ignore_files() {
405+
let { containerEl } = this;
406+
const plugin = (this as any).plugin
407+
let ignored_files_settings = containerEl.createEl('h3', { text: 'Ignored File Settings' })
408+
plugin.settings["IGNORED_FILE_GLOBS"] = plugin.settings.hasOwnProperty("IGNORED_FILE_GLOBS") ? plugin.settings["IGNORED_FILE_GLOBS"] : DEFAULT_IGNORED_FILE_GLOBS
409+
const descriptionFragment = document.createDocumentFragment();
410+
descriptionFragment.createEl("span", { text: "Glob patterns for files to ignore. You can add multiple patterns. One per line. Have a look at the " })
411+
descriptionFragment.createEl("a", { text: "README.md", href: "https://github.com/Pseudonium/Obsidian_to_Anki?tab=readme-ov-file#features" });
412+
descriptionFragment.createEl("span", { text: " for more information, examples and further resources." })
413+
414+
415+
new Setting(ignored_files_settings)
416+
.setName("Patterns to ignore")
417+
.setDesc(descriptionFragment)
418+
.addTextArea(text => {
419+
text.setValue(plugin.settings.IGNORED_FILE_GLOBS.join("\n"))
420+
.setPlaceholder("Examples: '**/*.excalidraw.md', 'Templates/**'")
421+
.onChange((value) => {
422+
let ignoreLines = value.split("\n")
423+
ignoreLines = ignoreLines.filter(e => e.trim() != "") //filter out empty lines and blank lines
424+
plugin.settings.IGNORED_FILE_GLOBS = ignoreLines
425+
426+
plugin.saveAllData()
427+
}
428+
)
429+
text.inputEl.rows = 10
430+
text.inputEl.cols = 30
431+
});
432+
}
400433

401434
setup_display() {
402435
let {containerEl} = this
@@ -409,6 +442,7 @@ export class SettingsTab extends PluginSettingTab {
409442
this.setup_syntax()
410443
this.setup_defaults()
411444
this.setup_buttons()
445+
this.setup_ignore_files()
412446
}
413447

414448
async display() {

tests/anki/test_ignore_setting.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import re
2+
import pytest
3+
from anki.errors import NotFoundError # noqa
4+
from anki.collection import Collection
5+
from anki.collection import SearchNode
6+
7+
# from conftest import col
8+
9+
test_name = "ignore_setting"
10+
col_path = "tests/test_outputs/{}/Anki2/User 1/collection.anki2".format(test_name)
11+
12+
test_file_paths = [
13+
"tests/test_outputs/{}/Obsidian/{}/scan_dir/included_file.md".format(
14+
test_name, test_name
15+
),
16+
"tests/test_outputs/{}/Obsidian/{}/scan_dir/some/other/subdir/also_included_file.md".format(
17+
test_name, test_name
18+
),
19+
]
20+
21+
test_file_no_cards_paths = [
22+
"tests/test_outputs/{}/Obsidian/{}/outside_of_scandir/not_supposed_to_be_scanned.md".format(
23+
test_name, test_name
24+
),
25+
"tests/test_outputs/{}/Obsidian/{}/scan_dir/ignored_by_setting_ignored/not_supposed_to_be_scanned.md".format(
26+
test_name, test_name
27+
),
28+
"tests/test_outputs/{}/Obsidian/{}/scan_dir/ignored_by_setting_ignored/some/other/subdir/not_supposed_to_be_scanned.md".format(
29+
test_name, test_name
30+
),
31+
]
32+
33+
34+
@pytest.fixture()
35+
def col():
36+
col = Collection(col_path)
37+
yield col
38+
col.close()
39+
40+
41+
def test_col_exists(col):
42+
assert not col.is_empty()
43+
44+
45+
def test_deck_default_exists(col: Collection):
46+
assert col.decks.id_for_name("Default") is not None
47+
48+
49+
def test_cards_count(col: Collection):
50+
assert len(col.find_cards(col.build_search_string(SearchNode(deck="Default")))) == 6
51+
52+
53+
def test_cards_ids_from_obsidian(col: Collection):
54+
ID_REGEXP_STR = r"\n?(?:<!--)?(?:ID: (\d+).*)"
55+
56+
obs_IDs = []
57+
for obsidian_test_md in test_file_paths:
58+
with open(obsidian_test_md) as file:
59+
for line in file:
60+
output = re.search(ID_REGEXP_STR, line.rstrip())
61+
if output is not None:
62+
output = output.group(1)
63+
obs_IDs.append(output)
64+
65+
anki_IDs = col.find_notes(col.build_search_string(SearchNode(deck="Default")))
66+
for aid, oid in zip(anki_IDs, obs_IDs):
67+
assert str(aid) == oid
68+
69+
70+
def test_no_cards_added_from_ignored_paths(col: Collection):
71+
ID_REGEXP_STR = r"\n?(?:<!--)?(?:ID: (\d+).*)"
72+
73+
for obsidian_test_md in test_file_no_cards_paths:
74+
obs_IDs = []
75+
with open(obsidian_test_md) as file:
76+
for line in file:
77+
output = re.search(ID_REGEXP_STR, line.rstrip())
78+
if output is not None:
79+
output = output.group(1)
80+
obs_IDs.append(output)
81+
82+
assert len(obs_IDs) == 0
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"settings": {
3+
"CUSTOM_REGEXPS": {
4+
"Basic": "",
5+
"Basic (and reversed card)": "",
6+
"Basic (optional reversed card)": "",
7+
"Basic (type in the answer)": "",
8+
"Cloze": ""
9+
},
10+
"FILE_LINK_FIELDS": {
11+
"Basic": "Front",
12+
"Basic (and reversed card)": "Front",
13+
"Basic (optional reversed card)": "Front",
14+
"Basic (type in the answer)": "Front",
15+
"Cloze": "Text"
16+
},
17+
"CONTEXT_FIELDS": {},
18+
"FOLDER_DECKS": {},
19+
"FOLDER_TAGS": {},
20+
"Syntax": {
21+
"Begin Note": "START",
22+
"End Note": "END",
23+
"Begin Inline Note": "STARTI",
24+
"End Inline Note": "ENDI",
25+
"Target Deck Line": "TARGET DECK",
26+
"File Tags Line": "FILE TAGS",
27+
"Delete Note Line": "DELETE",
28+
"Frozen Fields Line": "FROZEN"
29+
},
30+
"Defaults": {
31+
"Scan Directory": "ignore_setting/scan_dir",
32+
"Tag": "Obsidian_to_Anki",
33+
"Deck": "Default",
34+
"Scheduling Interval": 0,
35+
"Add File Link": false,
36+
"Add Context": false,
37+
"CurlyCloze": true,
38+
"CurlyCloze - Highlights to Clozes": false,
39+
"ID Comments": true,
40+
"Add Obsidian Tags": false
41+
},
42+
"IGNORED_FILE_GLOBS": [
43+
"ignore_setting/scan_dir/*ignored/**"
44+
]
45+
},
46+
"Added Media": [],
47+
"File Hashes": {},
48+
"fields_dict": {
49+
"Basic": [
50+
"Front",
51+
"Back"
52+
],
53+
"Basic (and reversed card)": [
54+
"Front",
55+
"Back"
56+
],
57+
"Basic (optional reversed card)": [
58+
"Front",
59+
"Back",
60+
"Add Reverse"
61+
],
62+
"Basic (type in the answer)": [
63+
"Front",
64+
"Back"
65+
],
66+
"Cloze": [
67+
"Text",
68+
"Back Extra"
69+
]
70+
}
71+
}

0 commit comments

Comments
 (0)