├── .dockerignore
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ └── custom-syntax-request.md
├── dependabot.yml
└── workflows
│ ├── build.yml
│ ├── codeql-analysis.yml
│ ├── obsidian-release.yml
│ ├── ok-to-test.yml
│ └── test-e2e.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── Images
├── AnkiConnect_ConfigREAL.png
├── Cloze_1.png
├── Header_1.png
├── Neuracache_1.png
├── Question_1.png
├── Remnote_1.png
├── Ruled_1.png
└── Table_2.png
├── LICENSE
├── README.md
├── main.ts
├── manifest.json
├── obsidian_to_anki.py
├── obsidian_to_anki_config.ini
├── obsidian_to_anki_data.json
├── obstoanki_setup.py
├── package-lock.json
├── package.json
├── prepare-wdio.sh
├── requirements.txt
├── rollup.config.js
├── root
├── defaults
│ ├── autostart
│ ├── menu.xml
│ ├── obsidian_anki.sh
│ ├── reset_perms.sh
│ └── startwm.sh
└── etc
│ └── cont-init.d
│ ├── 50-config
│ └── 56-openboxcopy
├── src
├── anki.ts
├── constants.ts
├── file.ts
├── files-manager.ts
├── format.ts
├── interfaces
│ ├── field-interface.ts
│ ├── note-interface.ts
│ └── settings-interface.ts
├── note.ts
├── setting-to-data.ts
└── settings.ts
├── styles.css
├── tests
├── anki
│ ├── test_basic_para.py
│ ├── test_basic_para_3.py
│ ├── test_basic_sync.py
│ ├── test_cloze_highlight.py
│ ├── test_cloze_para.py
│ ├── test_cloze_sync.py
│ ├── test_context_test.py
│ ├── test_folder_deck.py
│ ├── test_folder_deck_tags.py
│ ├── test_folder_scan.py
│ ├── test_frozen_notes.py
│ ├── test_image_sync.py
│ ├── test_inline_notes.py
│ ├── test_markdown_table.py
│ ├── test_markdown_test.py
│ ├── test_math_test.py
│ ├── test_music_embed.py
│ ├── test_neuracache_sync.py
│ ├── test_ng_basic_update.py
│ ├── test_ng_delete_sync.py
│ ├── test_question_answer.py
│ ├── test_remnote_inline.py
│ ├── test_ruled_style.py
│ ├── test_tag_sync.py
│ └── test_target_deck.py
├── defaults
│ ├── specs
│ │ └── template.e2e.ts
│ ├── test_config
│ │ ├── .config
│ │ │ └── obsidian
│ │ │ │ ├── Preferences
│ │ │ │ ├── e697835dbb2e89b2.json
│ │ │ │ └── obsidian.json
│ │ └── .local
│ │ │ └── share
│ │ │ ├── Anki2
│ │ │ ├── User 1
│ │ │ │ ├── backups
│ │ │ │ │ └── backup-2023-03-06-00.48.36.colpkg
│ │ │ │ └── collection.anki2
│ │ │ ├── addons21
│ │ │ │ └── 2055492159
│ │ │ │ │ ├── __init__.py
│ │ │ │ │ ├── config.json
│ │ │ │ │ ├── config.md
│ │ │ │ │ ├── edit.py
│ │ │ │ │ ├── meta.json
│ │ │ │ │ ├── util.py
│ │ │ │ │ └── web.py
│ │ │ └── prefs21.db
│ │ │ └── Anki2default
│ │ │ ├── User 1
│ │ │ ├── backups
│ │ │ │ └── backup-2023-03-06-00.48.36.colpkg
│ │ │ └── collection.anki2
│ │ │ ├── addons21
│ │ │ └── 2055492159
│ │ │ │ ├── __init__.py
│ │ │ │ ├── config.json
│ │ │ │ ├── config.md
│ │ │ │ ├── edit.py
│ │ │ │ ├── meta.json
│ │ │ │ ├── util.py
│ │ │ │ └── web.py
│ │ │ └── prefs21.db
│ ├── test_vault
│ │ └── .obsidian
│ │ │ ├── community-plugins.json
│ │ │ ├── hotkeys.json
│ │ │ ├── plugins
│ │ │ └── obsidian-to-anki-plugin
│ │ │ │ ├── manifest.json
│ │ │ │ └── styles.css
│ │ │ └── workspace
│ └── test_vault_suites
│ │ ├── basic_para
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── basic_para.md
│ │ ├── basic_para_3
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── basic_para_3.md
│ │ ├── basic_sync
│ │ └── basic_sync.md
│ │ ├── cloze_highlight
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── cloze_highlight.md
│ │ ├── cloze_para
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── cloze_para.md
│ │ ├── cloze_sync
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── cloze_sync.md
│ │ ├── context_test
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── context_test.md
│ │ ├── folder_deck
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ ├── English
│ │ │ └── No Deck
│ │ │ │ └── folder_deck.parent.md
│ │ ├── Math meow
│ │ │ └── folder_deck.math.md
│ │ ├── Science meow
│ │ │ └── folder_deck.science.md
│ │ └── folder_deck.md
│ │ ├── folder_deck_tags
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ ├── English
│ │ │ └── No Deck
│ │ │ │ └── folder_deck_tags.parent.md
│ │ ├── Math meow
│ │ │ └── folder_deck_tags.math.md
│ │ ├── Science meow
│ │ │ └── folder_deck_tags.science.md
│ │ └── folder_deck_tags.md
│ │ ├── folder_scan
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ ├── folder_scan.md
│ │ ├── scan_dir
│ │ │ ├── also_scan
│ │ │ │ └── folder_scan_subdir.md
│ │ │ └── folder_scan.md
│ │ └── should_not_scan_dir
│ │ │ └── should_not_scan.md
│ │ ├── frozen_notes
│ │ └── frozen_notes.md
│ │ ├── image_sync
│ │ └── image_sync.md
│ │ ├── inline_notes
│ │ └── inline_notes.md
│ │ ├── markdown_table
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── markdown_table.md
│ │ ├── markdown_test
│ │ └── markdown_test.md
│ │ ├── math_test
│ │ └── math_test.md
│ │ ├── music_embed
│ │ ├── music_embed.md
│ │ └── test.mp3
│ │ ├── neuracache_sync
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── neuracache_sync.md
│ │ ├── ng_basic_update
│ │ ├── .obsidian
│ │ │ └── workspace
│ │ └── ng_basic_update.md
│ │ ├── ng_delete_sync
│ │ └── ng_delete_sync.md
│ │ ├── question_answer
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── question_answer.md
│ │ ├── remnote_inline
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── remnote_inline.md
│ │ ├── ruled_style
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ └── ruled_style.md
│ │ ├── tag_sync
│ │ ├── .obsidian
│ │ │ └── plugins
│ │ │ │ └── obsidian-to-anki-plugin
│ │ │ │ └── data.json
│ │ ├── tag_sync.file.inline.md
│ │ ├── tag_sync.file.md
│ │ └── tag_sync.md
│ │ └── target_deck
│ │ ├── target_deck.md
│ │ └── target_deck.sameline.md
├── specs
│ ├── ng_basic_update.e2e.ts
│ └── ng_delete_sync.e2e.ts
└── tsconfig.json
├── tsconfig.json
├── versions.json
└── wdio.conf.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom-syntax-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom syntax request
3 | about: Request to add custom flashcard syntax to regex.md
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | env:
4 | PLUGIN_NAME: obsidian-to-anki-plugin
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [18.x]
20 | install-exact: [1, 0]
21 |
22 | steps:
23 | - uses: actions/checkout@v3
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | # obsidian package often fails integrity checks since its a tarball, hence will get the get the tarball -> npm will cache it -> npm ci will test integrity of that cache vs package-lock.json
29 | # https://github.com/ShootingKing-AM/Obsidian_to_Anki/pull/122
30 | # Installing exact tree would be used in CI/CD to exactly reproduce tests
31 | - name: Node install exact tree
32 | if: matrix.install-exact == 1
33 | run: |
34 | npm install obsidian
35 | npm ci
36 |
37 | # Install latest tree would be usually done by devs/who locally build the plugin
38 | - name: Node install latest tree
39 | if: matrix.install-exact == 0
40 | run: |
41 | npm update
42 | npm install
43 |
44 | - run: npm run build --if-present
45 | # - run: npm test
46 |
47 | - name: Package
48 | run: |
49 | mkdir ${{ env.PLUGIN_NAME }}
50 | cp main.js manifest.json styles.css README.md ${{ env.PLUGIN_NAME }}
51 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
52 |
53 | - name: Upload build artifacts
54 | uses: actions/upload-artifact@v3
55 | with:
56 | name: ${{ env.PLUGIN_NAME }}
57 | path: |
58 | ${{ env.PLUGIN_NAME }}.zip
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '15 20 * * 6'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | language: [ 'javascript', 'python' ]
32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
33 | # Learn more:
34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v3
39 |
40 | # Initializes the CodeQL tools for scanning.
41 | - name: Initialize CodeQL
42 | uses: github/codeql-action/init@v2
43 | with:
44 | languages: ${{ matrix.language }}
45 | # If you wish to specify custom queries, you can do so here or in a config file.
46 | # By default, queries listed here will override any specified in a config file.
47 | # Prefix the list here with "+" to use these queries and those in the config file.
48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
49 |
50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
51 | # If this step fails, then you should remove it and run the build manually (see below)
52 | - name: Autobuild
53 | uses: github/codeql-action/autobuild@v2
54 |
55 | # ℹ️ Command-line programs to run using the OS shell.
56 | # 📚 https://git.io/JvXDl
57 |
58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
59 | # and modify them (or add more) to build your code if your project
60 | # uses a compiled language
61 |
62 | #- run: |
63 | # make bootstrap
64 | # make release
65 |
66 | - name: Perform CodeQL Analysis
67 | uses: github/codeql-action/analyze@v2
68 |
--------------------------------------------------------------------------------
/.github/workflows/obsidian-release.yml:
--------------------------------------------------------------------------------
1 | name: Release Obsidian Plugin
2 |
3 | env:
4 | PLUGIN_NAME: obsidian-to-anki-plugin # Change this to match the id of your plugin.
5 |
6 | on:
7 | workflow_dispatch:
8 | push:
9 | # Sequence of patterns matched against refs/tags
10 | tags:
11 | - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v3
19 | with:
20 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
21 | - name: Use Node.js
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: "18.x" # You might need to adjust this value to your own version.
25 |
26 | # Get the version number and put it in an environment file
27 | - name: Get Version
28 | id: version
29 | run: |
30 | echo "tag=$(git describe --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_ENV
31 |
32 | # Build the plugin
33 | - name: Build
34 | id: build
35 | run: |
36 | npm install
37 | npm run build
38 |
39 | # Package the required files into a zip
40 | - name: Package
41 | run: |
42 | mkdir ${{ env.PLUGIN_NAME }}
43 | cp main.js manifest.json styles.css README.md ${{ env.PLUGIN_NAME }}
44 | zip -r ${{ env.PLUGIN_NAME }}-${{ env.tag }}.zip ${{ env.PLUGIN_NAME }}
45 |
46 | - name: Release
47 | uses: softprops/action-gh-release@v1
48 | with:
49 | # main.css
50 | generate_release_notes: true
51 | files: |
52 | main.js
53 | manifest.json
54 | styles.css
55 | ${{ env.PLUGIN_NAME }}-${{ env.tag }}.zip
56 |
--------------------------------------------------------------------------------
/.github/workflows/ok-to-test.yml:
--------------------------------------------------------------------------------
1 | # If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event
2 | name: Ok To Test
3 |
4 | on:
5 | issue_comment:
6 | types: [created]
7 |
8 | jobs:
9 | ok-to-test:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | pull-requests: write
13 | # Only run for PRs, not issue comments
14 | if: ${{ github.event.issue.pull_request }}
15 | steps:
16 | # Generate a GitHub App installation access token from an App ID and private key
17 | # To create a new GitHub App:
18 | # https://developer.github.com/apps/building-github-apps/creating-a-github-app/
19 | # See app.yml for an example app manifest - you can use dummy URLs when generating
20 | # the app such as example.com urls. They do not need to properly resolve for the
21 | # purpose that they app is being used for.
22 | - name: Generate token
23 | id: generate_token
24 | uses: tibdex/github-app-token@v2
25 | with:
26 | app_id: ${{ secrets.APP_ID }}
27 | private_key: ${{ secrets.PRIVATE_KEY }}
28 |
29 | - name: Slash Command Dispatch
30 | uses: peter-evans/slash-command-dispatch@v3
31 | env:
32 | TOKEN: ${{ steps.generate_token.outputs.token }}
33 | with:
34 | token: ${{ env.TOKEN }} # GitHub App installation access token
35 | # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
36 | reaction-token: ${{ secrets.GITHUB_TOKEN }}
37 | issue-type: pull-request
38 | commands: ok-to-test
39 | permission: write
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # Created by https://www.toptal.com/developers/gitignore/api/node
132 | # Edit at https://www.toptal.com/developers/gitignore?templates=node
133 |
134 | ### Node ###
135 | # Logs
136 | logs
137 | *.log
138 | npm-debug.log*
139 | yarn-debug.log*
140 | yarn-error.log*
141 | lerna-debug.log*
142 |
143 | # Diagnostic reports (https://nodejs.org/api/report.html)
144 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
145 |
146 | # Runtime data
147 | pids
148 | *.pid
149 | *.seed
150 | *.pid.lock
151 |
152 | # Directory for instrumented libs generated by jscoverage/JSCover
153 | lib-cov
154 |
155 | # Coverage directory used by tools like istanbul
156 | coverage
157 | *.lcov
158 |
159 | # nyc test coverage
160 | .nyc_output
161 |
162 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
163 | .grunt
164 |
165 | # Bower dependency directory (https://bower.io/)
166 | bower_components
167 |
168 | # node-waf configuration
169 | .lock-wscript
170 |
171 | # Compiled binary addons (https://nodejs.org/api/addons.html)
172 | build/Release
173 |
174 | # Dependency directories
175 | node_modules/
176 | jspm_packages/
177 |
178 | # TypeScript v1 declaration files
179 | typings/
180 |
181 | # TypeScript cache
182 | *.tsbuildinfo
183 |
184 | # Optional npm cache directory
185 | .npm
186 |
187 | # Optional eslint cache
188 | .eslintcache
189 |
190 | # Microbundle cache
191 | .rpt2_cache/
192 | .rts2_cache_cjs/
193 | .rts2_cache_es/
194 | .rts2_cache_umd/
195 |
196 | # Optional REPL history
197 | .node_repl_history
198 |
199 | # Output of 'npm pack'
200 | *.tgz
201 |
202 | # Yarn Integrity file
203 | .yarn-integrity
204 |
205 | # dotenv environment variables file
206 | .env
207 | .env.test
208 | .env*.local
209 |
210 | # parcel-bundler cache (https://parceljs.org/)
211 | .cache
212 | .parcel-cache
213 |
214 | # Next.js build output
215 | .next
216 |
217 | # Nuxt.js build / generate output
218 | .nuxt
219 | dist
220 |
221 | # Gatsby files
222 | .cache/
223 | # Comment in the public line in if your project uses Gatsby and not Next.js
224 | # https://nextjs.org/blog/next-9-1#public-directory-support
225 | # public
226 |
227 | # vuepress build output
228 | .vuepress/dist
229 |
230 | # Serverless directories
231 | .serverless/
232 |
233 | # FuseBox cache
234 | .fusebox/
235 |
236 | # DynamoDB Local files
237 | .dynamodb/
238 |
239 | # TernJS port file
240 | .tern-port
241 |
242 | # Stores VSCode versions used for testing VSCode extensions
243 | .vscode-test
244 |
245 | # End of https://www.toptal.com/developers/gitignore/api/node
246 |
247 | # Docker project generated files to ignore
248 | # if you want to ignore files created by your editor/tools,
249 | # please consider a global .gitignore https://help.github.com/articles/ignoring-files
250 | # https://github.com/atlassian/docker/blob/master/.gitignore
251 | .vagrant*
252 | bin
253 | docker/docker
254 | .*.swp
255 | a.out
256 | *.orig
257 | build_src
258 | .flymake*
259 | .idea
260 | .DS_Store
261 | docs/_build
262 | docs/_static
263 | docs/_templates
264 | .gopath/
265 | .dotcloud
266 | *.test
267 | bundles/
268 | .hg/
269 | .git/
270 | vendor/pkg/
271 | pyenv
272 | Vagrantfile
273 |
274 | *.cid
275 |
276 | # Obsidian-to-Anki plugin Specifc
277 | # Complied Plugin Outputs
278 | main.js
279 |
280 | tests/test_config/*
281 | tests/test_vault/*
282 | tests/specs_gen/*
283 | tests/test_outputs/*
284 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at rubaiyat.khondaker@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Fixed and Derived from https://github.com/sytone/obsidian-remote and https://gist.github.com/ondrik/95850021e9046483df91c46d9a23ad2b
2 |
3 | FROM ghcr.io/linuxserver/baseimage-rdesktop-web:focal-1.2.0-ls101
4 |
5 | RUN \
6 | echo "**** install packages ****" && \
7 | # Update and install extra packages.
8 | apt-get update && \
9 | apt-get install -y --force-yes --no-install-recommends \
10 | # Packages needed to download and extract obsidian.
11 | curl \
12 | libnss3 \
13 | aptitude \
14 | xz-utils zstd xdg-utils libxcb-xinerama0 libxkbcommon-x11-0\
15 | software-properties-common \
16 | # Install Chrome dependencies.
17 | dbus-x11 \
18 | uuid-runtime \
19 | locales locales-all \
20 | dbus-x11 x11-xkb-utils rename
21 |
22 | # Credits: https://wiki.debian.org/Locale
23 | # RUN apt-get install -y aptitude
24 | # RUN add-apt-repository "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc) main universe restricted multiverse"
25 | # RUN aptitude install -y locales locales-all
26 | # RUN aptitude install -y libzstd1
27 | RUN echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && locale-gen
28 |
29 | # fonts-vlgothic is for Japanese fonts. Depending on what you study with
30 | # Anki, you might want to install other packages.
31 | # RUN aptitude install -y fonts-vlgothic
32 | # RUN aptitude install -y fonts-arphic-uming fonts-wqy-zenhei
33 | # RUN aptitude install -y fcitx fcitx-chewing
34 | # RUN aptitude install -y dbus-x11 x11-xkb-utils
35 | # RUN aptitude install -y anki
36 |
37 | # Might only work if your host user and group IDs are both 1000.
38 |
39 | RUN \
40 | echo "**** install runtime packages ****" && \
41 | apt-get update && \
42 | apt-get install -y \
43 | logrotate \
44 | nano \
45 | netcat-openbsd sshpass \
46 | sudo && \
47 | echo "**** install openssh-server ****" && \
48 | apt-get install -y \
49 | openssh-client \
50 | openssh-server && \
51 | ## openssh-sftp-server && \
52 | echo "**** setup openssh environment ****" && \
53 | ## sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config && \
54 | usermod --shell /bin/bash abc && \
55 | rm -rf \
56 | /tmp/*
57 |
58 | RUN apt-get install -y '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libxcb-xinerama0 libxcb-image0 libxcb-icccm4 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 gnome-screenshot
59 |
60 | RUN echo "**** download anki ****" && curl https://github.com/ankitects/anki/releases/download/2.1.60/anki-2.1.60-linux-qt6.tar.zst -L -o anki.tar.zst
61 | RUN chmod +x ./anki.tar.zst && \
62 | mkdir anki && \
63 | mkdir /usr/share/desktop-directories && \
64 | tar --use-compress-program=unzstd -xvf ./anki.tar.zst -C ./anki/ && \
65 | cd anki/anki-2.1.60-linux-qt6/ && \
66 | chmod +x ./install.sh && \
67 | ./install.sh
68 |
69 | ENV LC_ALL en_US.UTF-8
70 | ENV LANG en_US.UTF-8
71 | ENV LANGUAGE en_US.UTF-8
72 |
73 | RUN update-locale LANG=en_US.UTF-8
74 | ENV QT_DEBUG_PLUGINS 1
75 | # ENV XMODIFIERS @im=fcitx
76 | # ENV XMODIFIERS @im=ibus
77 | # CMD /bin/bash -c "(/usr/bin/ibus-daemon -xd; /usr/bin/anki;)"
78 | # CMD /bin/bash -c "/usr/bin/fcitx-autostart ; /usr/bin/anki"
79 | # CMD /bin/bash -c "/usr/bin/anki"
80 | # CMD /bin/bash -c "anki"
81 |
82 | # # set version label
83 | ARG OBSIDIAN_VERSION=1.5.3
84 |
85 | RUN \
86 | echo "**** download obsidian ****" && \
87 | curl \
88 | https://github.com/obsidianmd/obsidian-releases/releases/download/v$OBSIDIAN_VERSION/Obsidian-$OBSIDIAN_VERSION.AppImage \
89 | -L \
90 | -o ./obsidian.AppImage
91 |
92 | RUN \
93 | echo "**** extract obsidian ****" && \
94 | chmod +x ./obsidian.AppImage && \
95 | ./obsidian.AppImage --appimage-extract
96 |
97 | ENV \
98 | CUSTOM_PORT="8080" \
99 | GUIAUTOSTART="true" \
100 | HOME="/vaults" \
101 | TITLE="Obsidian v$OBSIDIAN_VERSION"
102 |
103 | RUN echo "**** cleanup ****" && \
104 | apt-get autoclean && \
105 | rm -rf \
106 | /var/lib/apt/lists/* \
107 | /var/tmp/* \
108 | /tmp/*
109 |
110 |
111 | # add local files
112 | COPY root/ /
113 |
114 | EXPOSE 8080
115 | EXPOSE 8888
116 |
117 | VOLUME ["/config","/vaults"]
--------------------------------------------------------------------------------
/Images/AnkiConnect_ConfigREAL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/Images/AnkiConnect_ConfigREAL.png
--------------------------------------------------------------------------------
/Images/Cloze_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/Images/Cloze_1.png
--------------------------------------------------------------------------------
/Images/Header_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/Images/Header_1.png
--------------------------------------------------------------------------------
/Images/Neuracache_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/Images/Neuracache_1.png
--------------------------------------------------------------------------------
/Images/Question_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/Images/Question_1.png
--------------------------------------------------------------------------------
/Images/Remnote_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/Images/Remnote_1.png
--------------------------------------------------------------------------------
/Images/Ruled_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/Images/Ruled_1.png
--------------------------------------------------------------------------------
/Images/Table_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/Images/Table_2.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Obsidian_to_Anki
2 | Plugin to add flashcards from a text or markdown file to Anki. Run in Obsidian as a plugin, or from the command-line as a python script. Built with [Obsidian](https://obsidian.md/) markdown syntax in mind. Supports **user-defined custom syntax for flashcards.**
3 | See the [Trello](https://trello.com/b/6MXEizGg/obsidiantoanki) for planned features.
4 |
5 | ## Getting started
6 |
7 | Check out the [Wiki](https://github.com/Pseudonium/Obsidian_to_Anki/wiki)! It has a ton of information, including setup instructions for new users. I will include a copy of the instructions here:
8 |
9 | ## Setup
10 |
11 | ### All users
12 | 1. Start up [Anki](https://apps.ankiweb.net/), and navigate to your desired profile.
13 | 2. Ensure that you've installed [AnkiConnect](https://github.com/FooSoft/anki-connect).
14 |
15 | ### Obsidian plugin users
16 | 3. Have [Obsidian](https://obsidian.md/) downloaded
17 | 4. Search the 'Community plugins' list for this plugin
18 | 5. Install the plugin.
19 | 6. In Anki, navigate to Tools->Addons->AnkiConnect->Config, and change it to look like this:
20 |
21 | {
22 | "apiKey": null,
23 | "apiLogPath": null,
24 | "webBindAddress": "127.0.0.1",
25 | "webBindPort": 8765,
26 | "webCorsOrigin": "http://localhost",
27 | "webCorsOriginList": [
28 | "http://localhost",
29 | "app://obsidian.md"
30 | ]
31 | }
32 |
33 |
34 | 7. Restart Anki to apply the above changes
35 | 8. With Anki running in the background, load the plugin. This will generate the plugin settings.
36 |
37 |
38 | You shouldn't need Anki running to load Obsidian in the future, though of course you will need it for using the plugin!
39 |
40 | To run the plugin, look for an Anki icon on your ribbon (the place where buttons such as 'open Graph view' and 'open Quick Switcher' are).
41 | For more information on use, please check out the [Wiki](https://github.com/Pseudonium/Obsidian_to_Anki/wiki)!
42 |
43 | ### Python script users
44 | 3. Install the latest version of [Python](https://www.python.org/downloads/).
45 | 4. If you are a new user, download `obstoanki_setup.py` from the [releases page](https://github.com/Pseudonium/Obsidian_to_Anki/releases), and place it in the folder you want the script installed (for example your notes folder).
46 | 5. Run `obstoanki_setup.py`, for example by double-clicking it in a file explorer. This will download the latest version of the script and required dependencies automatically. Existing users should be able to run their existing `obstoanki_setup.py` to get the latest version of the script.
47 | 6. Check the Permissions tab below to ensure the script is able to run.
48 | 7. Run `obsidian_to_anki.py`, for example by double-clicking it in a file explorer. This will generate a config file, `obsidian_to_anki_config.ini`.
49 |
50 | #### Permissions
51 | The script needs to be able to:
52 | * Make a config file in the directory the script is installed.
53 | * Read the file in the directory the script is used.
54 | * Make a backup file in the directory the script is used.
55 | * Rename files in the directory the script is used.
56 | * Remove a backup file in the directory the script is used.
57 | * Change the current working directory temporarily (so that local image paths are resolved correctly).
58 |
59 | ## Features
60 |
61 | Current features (check out the wiki for more details):
62 | * **Custom note types** - You're not limited to the 6 built-in note types of Anki.
63 | * **Custom scan directory**
64 | * The plugin will scan the entire vault by default
65 | * You can also set which directory (includes all sub-directories as well) to scan via plugin settings
66 | * **Updating notes from file** - Your text files are the canonical source of the notes.
67 | * **Tags**, including **tags for an entire file**.
68 | * **Adding to user-specified deck** on a *per-file* basis.
69 | * **Markdown formatting**.
70 | * **Math formatting**.
71 | * **Embedded images**. GIFs should work too.
72 | * **Audio**.
73 | * **Auto-deleting notes from the file**.
74 | * **Reading from all files in a directory automatically** - recursively too!
75 | * **Inline Notes** - Shorter syntax for typing out notes on a single line.
76 | * **Easy cloze formatting** - A more compact syntax to do Cloze text
77 | * **Frozen Fields**
78 | * **Obsidian integration** - A link to the file that made the flashcard, full link and image embed support.
79 | * **Custom syntax** - Using **regular expressions**, add custom syntax to generate **notes that make sense for you.** Some examples:
80 | * RemNote single-line style. `This is how to use::Remnote single-line style`
81 | 
82 | * Header paragraph style.
83 |
84 | # Style
85 | This style is suitable for having the header as the front, and the answer as the back
86 |
87 | 
88 | * Question answer style.
89 |
90 | Q: How do you use this style?
91 | A: Just like this.
92 |
93 | 
94 | * Neuracache #flashcard style.
95 |
96 | In Neuracache style, to make a flashcard you do #flashcard
97 | The next lines then become the back of the flashcard
98 |
99 | 
100 | * Ruled style
101 |
102 | How do you use ruled style?
103 | ---
104 | You need at least three '-' between the front and back of the card.
105 |
106 | 
107 | * Markdown table style
108 |
109 | | Why might this style be useful? |
110 | | ------ |
111 | | It looks nice when rendered as HTML in a markdown editor. |
112 |
113 | 
114 | * Cloze paragraph style
115 |
116 | The idea of {cloze paragraph style} is to be able to recognise any paragraphs that contain {cloze deletions}.
117 |
118 | 
119 |
120 | Note that **all custom syntax is off by default**, and must be programmed into the script via the config file - see the Wiki for more details.
121 |
122 |
123 |
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
1 | import { Notice, Plugin, addIcon, TFile, TFolder } from 'obsidian'
2 | import * as AnkiConnect from './src/anki'
3 | import { PluginSettings, ParsedSettings } from './src/interfaces/settings-interface'
4 | import { SettingsTab } from './src/settings'
5 | import { ANKI_ICON } from './src/constants'
6 | import { settingToData } from './src/setting-to-data'
7 | import { FileManager } from './src/files-manager'
8 |
9 | export default class MyPlugin extends Plugin {
10 |
11 | settings: PluginSettings
12 | note_types: Array
13 | fields_dict: Record
14 | added_media: string[]
15 | file_hashes: Record
16 |
17 | async getDefaultSettings(): Promise {
18 | let settings: PluginSettings = {
19 | CUSTOM_REGEXPS: {},
20 | FILE_LINK_FIELDS: {},
21 | CONTEXT_FIELDS: {},
22 | FOLDER_DECKS: {},
23 | FOLDER_TAGS: {},
24 | Syntax: {
25 | "Begin Note": "START",
26 | "End Note": "END",
27 | "Begin Inline Note": "STARTI",
28 | "End Inline Note": "ENDI",
29 | "Target Deck Line": "TARGET DECK",
30 | "File Tags Line": "FILE TAGS",
31 | "Delete Note Line": "DELETE",
32 | "Frozen Fields Line": "FROZEN"
33 | },
34 | Defaults: {
35 | "Scan Directory": "",
36 | "Tag": "Obsidian_to_Anki",
37 | "Deck": "Default",
38 | "Scheduling Interval": 0,
39 | "Add File Link": false,
40 | "Add Context": false,
41 | "CurlyCloze": false,
42 | "CurlyCloze - Highlights to Clozes": false,
43 | "ID Comments": true,
44 | "Add Obsidian Tags": false,
45 | }
46 | }
47 | /*Making settings from scratch, so need note types*/
48 | this.note_types = await AnkiConnect.invoke('modelNames') as Array
49 | this.fields_dict = await this.generateFieldsDict()
50 | for (let note_type of this.note_types) {
51 | settings["CUSTOM_REGEXPS"][note_type] = ""
52 | const field_names: string[] = await AnkiConnect.invoke(
53 | 'modelFieldNames', {modelName: note_type}
54 | ) as string[]
55 | this.fields_dict[note_type] = field_names
56 | settings["FILE_LINK_FIELDS"][note_type] = field_names[0]
57 | }
58 | return settings
59 | }
60 |
61 | async generateFieldsDict(): Promise> {
62 | let fields_dict = {}
63 | for (let note_type of this.note_types) {
64 | const field_names: string[] = await AnkiConnect.invoke(
65 | 'modelFieldNames', {modelName: note_type}
66 | ) as string[]
67 | fields_dict[note_type] = field_names
68 | }
69 | return fields_dict
70 | }
71 |
72 | async saveDefault(): Promise {
73 | const default_sets = await this.getDefaultSettings()
74 | this.saveData(
75 | {
76 | settings: default_sets,
77 | "Added Media": [],
78 | "File Hashes": {},
79 | fields_dict: {}
80 | }
81 | )
82 | }
83 |
84 | async loadSettings(): Promise {
85 | let current_data = await this.loadData()
86 | if (current_data == null || Object.keys(current_data).length != 4) {
87 | new Notice("Need to connect to Anki generate default settings...")
88 | const default_sets = await this.getDefaultSettings()
89 | this.saveData(
90 | {
91 | settings: default_sets,
92 | "Added Media": [],
93 | "File Hashes": {},
94 | fields_dict: {}
95 | }
96 | )
97 | new Notice("Default settings successfully generated!")
98 | return default_sets
99 | } else {
100 | return current_data.settings
101 | }
102 | }
103 |
104 | async loadAddedMedia(): Promise {
105 | let current_data = await this.loadData()
106 | if (current_data == null) {
107 | await this.saveDefault()
108 | return []
109 | } else {
110 | return current_data["Added Media"]
111 | }
112 | }
113 |
114 | async loadFileHashes(): Promise> {
115 | let current_data = await this.loadData()
116 | if (current_data == null) {
117 | await this.saveDefault()
118 | return {}
119 | } else {
120 | return current_data["File Hashes"]
121 | }
122 | }
123 |
124 | async loadFieldsDict(): Promise> {
125 | let current_data = await this.loadData()
126 | if (current_data == null) {
127 | await this.saveDefault()
128 | const fields_dict = await this.generateFieldsDict()
129 | return fields_dict
130 | }
131 | return current_data.fields_dict
132 | }
133 |
134 | async saveAllData(): Promise {
135 | this.saveData(
136 | {
137 | settings: this.settings,
138 | "Added Media": this.added_media,
139 | "File Hashes": this.file_hashes,
140 | fields_dict: this.fields_dict
141 | }
142 | )
143 | }
144 |
145 | regenerateSettingsRegexps() {
146 | let regexp_section = this.settings["CUSTOM_REGEXPS"]
147 | // For new note types
148 | for (let note_type of this.note_types) {
149 | this.settings["CUSTOM_REGEXPS"][note_type] = regexp_section.hasOwnProperty(note_type) ? regexp_section[note_type] : ""
150 | }
151 | // Removing old note types
152 | for (let note_type of Object.keys(this.settings["CUSTOM_REGEXPS"])) {
153 | if (!this.note_types.includes(note_type)) {
154 | delete this.settings["CUSTOM_REGEXPS"][note_type]
155 | }
156 | }
157 | }
158 |
159 | /**
160 | * Recursively traverse a TFolder and return all TFiles.
161 | * @param tfolder - The TFolder to start the traversal from.
162 | * @returns An array of TFiles found within the folder and its subfolders.
163 | */
164 | getAllTFilesInFolder(tfolder) {
165 | const allTFiles = [];
166 | // Check if the provided object is a TFolder
167 | if (!(tfolder instanceof TFolder)) {
168 | return allTFiles;
169 | }
170 | // Iterate through the contents of the folder
171 | tfolder.children.forEach((child) => {
172 | // If it's a TFile, add it to the result
173 | if (child instanceof TFile) {
174 | allTFiles.push(child);
175 | } else if (child instanceof TFolder) {
176 | // If it's a TFolder, recursively call the function on it
177 | const filesInSubfolder = this.getAllTFilesInFolder(child);
178 | allTFiles.push(...filesInSubfolder);
179 | }
180 | // Ignore other types of files or objects
181 | });
182 | return allTFiles;
183 | }
184 |
185 | async scanVault() {
186 | new Notice('Scanning vault, check console for details...');
187 | console.info("Checking connection to Anki...")
188 | try {
189 | await AnkiConnect.invoke('modelNames')
190 | }
191 | catch(e) {
192 | new Notice("Error, couldn't connect to Anki! Check console for error message.")
193 | return
194 | }
195 | new Notice("Successfully connected to Anki! This could take a few minutes - please don't close Anki until the plugin is finished")
196 | const data: ParsedSettings = await settingToData(this.app, this.settings, this.fields_dict)
197 | const scanDir = this.app.vault.getAbstractFileByPath(this.settings.Defaults["Scan Directory"])
198 | let manager = null;
199 | if (scanDir !== null) {
200 | let markdownFiles = [];
201 | if (scanDir instanceof TFolder) {
202 | console.info("Using custom scan directory: " + scanDir.path)
203 | markdownFiles = this.getAllTFilesInFolder(scanDir);
204 | } else {
205 | new Notice("Error: incorrect path for scan directory " + this.settings.Defaults["Scan Directory"])
206 | return
207 | }
208 | manager = new FileManager(this.app, data, markdownFiles, this.file_hashes, this.added_media)
209 | } else {
210 | manager = new FileManager(this.app, data, this.app.vault.getMarkdownFiles(), this.file_hashes, this.added_media);
211 | }
212 |
213 | await manager.initialiseFiles()
214 | await manager.requests_1()
215 | this.added_media = Array.from(manager.added_media_set)
216 | const hashes = manager.getHashes()
217 | for (let key in hashes) {
218 | this.file_hashes[key] = hashes[key]
219 | }
220 | new Notice("All done! Saving file hashes and added media now...")
221 | this.saveAllData()
222 | }
223 |
224 | async onload() {
225 | console.log('loading Obsidian_to_Anki...');
226 | addIcon('anki', ANKI_ICON)
227 |
228 | try {
229 | this.settings = await this.loadSettings()
230 | }
231 | catch(e) {
232 | new Notice("Couldn't connect to Anki! Check console for error message.")
233 | return
234 | }
235 |
236 | this.note_types = Object.keys(this.settings["CUSTOM_REGEXPS"])
237 | this.fields_dict = await this.loadFieldsDict()
238 | if (Object.keys(this.fields_dict).length == 0) {
239 | new Notice('Need to connect to Anki to generate fields dictionary...')
240 | try {
241 | this.fields_dict = await this.generateFieldsDict()
242 | new Notice("Fields dictionary successfully generated!")
243 | }
244 | catch(e) {
245 | new Notice("Couldn't connect to Anki! Check console for error message.")
246 | return
247 | }
248 | }
249 | this.added_media = await this.loadAddedMedia()
250 | this.file_hashes = await this.loadFileHashes()
251 |
252 | this.addSettingTab(new SettingsTab(this.app, this));
253 |
254 | this.addRibbonIcon('anki', 'Obsidian_to_Anki - Scan Vault', async () => {
255 | await this.scanVault()
256 | })
257 |
258 | this.addCommand({
259 | id: 'anki-scan-vault',
260 | name: 'Scan Vault',
261 | callback: async () => {
262 | await this.scanVault()
263 | }
264 | })
265 | }
266 |
267 | async onunload() {
268 | console.log("Saving settings for Obsidian_to_Anki...")
269 | this.saveAllData()
270 | console.log('unloading Obsidian_to_Anki...');
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-to-anki-plugin",
3 | "name": "Obsidian_to_Anki",
4 | "version": "3.4.2",
5 | "minAppVersion": "0.9.20",
6 | "description": "This is an Anki integration plugin! Designed for efficient bulk exporting.",
7 | "author": "Pseudonium",
8 | "authorUrl": "https://github.com/Pseudonium/Obsidian_to_Anki",
9 | "isDesktopOnly": true
10 | }
11 |
--------------------------------------------------------------------------------
/obsidian_to_anki_config.ini:
--------------------------------------------------------------------------------
1 | [Custom Regexps]
2 | Basic =
3 | Basic (and reversed card) =
4 | Basic (optional reversed card) =
5 | Basic (type in the answer) =
6 | Cloze =
7 |
8 | [Syntax]
9 | Begin Note = START
10 | End Note = END
11 | Begin Inline Note = STARTI
12 | End Inline Note = ENDI
13 | Target Deck Line = TARGET DECK
14 | File Tags Line = FILE TAGS
15 | Delete Regex Note Line = DELETE
16 | Frozen Fields Line = FROZEN
17 | Delete Note Line = DELETE
18 |
19 | [Obsidian]
20 | Vault name =
21 | Add file link = False
22 |
23 | [Defaults]
24 | Tag = Obsidian_to_Anki
25 | Deck = Default
26 | CurlyCloze = False
27 | GUI = True
28 | Regex = False
29 | ID Comments = True
30 | Anki Path =
31 | Anki Profile =
32 |
33 |
--------------------------------------------------------------------------------
/obsidian_to_anki_data.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/obstoanki_setup.py:
--------------------------------------------------------------------------------
1 | import urllib.request
2 | import sys
3 | import subprocess
4 | import os
5 |
6 | SCRIPT_URL = "".join(
7 | [
8 | "https://github.com/Pseudonium/Obsidian_to_Anki/releases/latest",
9 | "/download/obsidian_to_anki.py"
10 | ]
11 | )
12 |
13 | REQUIRE_URL = "".join(
14 | [
15 | "https://github.com/Pseudonium/Obsidian_to_Anki/releases/latest",
16 | "/download/requirements.txt"
17 | ]
18 | )
19 |
20 | with urllib.request.urlopen(SCRIPT_URL) as script:
21 | with open("obsidian_to_anki.py", "wb") as f:
22 | f.write(script.read())
23 |
24 | with urllib.request.urlopen(REQUIRE_URL) as require:
25 | with open("obstoankirequire.txt", "wb") as f:
26 | f.write(require.read())
27 | subprocess.check_call(
28 | [sys.executable, "-m", "pip", "install", "-r", "obstoankirequire.txt"]
29 | )
30 | os.remove("obstoankirequire.txt")
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-to-anki-plugin",
3 | "version": "3.4.2",
4 | "description": "This is an Anki integration plugin! Designed for efficient bulk exporting.",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "rollup --config rollup.config.js -w",
8 | "build": "rollup --config rollup.config.js",
9 | "copy": "rm -rf tests/defaults/test_vault/.obsidian/plugins/obsidian-to-anki-plugin && mkdir -p tests/defaults/test_vault/.obsidian/plugins/obsidian-to-anki-plugin && cp manifest.json styles.css main.js tests/defaults/test_vault/.obsidian/plugins/obsidian-to-anki-plugin/",
10 | "prep-wdio": "bash prepare-wdio.sh",
11 | "test-wdio": "npm run prep-wdio && docker build -t anki-obsidian . && wdio run ./wdio.conf.ts",
12 | "test-py": "pip install pytest anki && pytest -vvvs tests/anki/ --junitxml logs/test-reports/pytest.xml",
13 | "test": "npm run test-wdio && npm run test-py"
14 | },
15 | "keywords": [],
16 | "author": "",
17 | "license": "MIT",
18 | "devDependencies": {
19 | "@rollup/plugin-commonjs": "15.1.0",
20 | "@rollup/plugin-node-resolve": "9.0.0",
21 | "@rollup/plugin-typescript": "^11.1.5",
22 | "@types/node": "^20.8.2",
23 | "@types/showdown": "^2.0.0",
24 | "@wdio/cli": "^8.6.9",
25 | "@wdio/junit-reporter": "^8.12.2",
26 | "@wdio/local-runner": "^8.6.9",
27 | "@wdio/mocha-framework": "^8.16.17",
28 | "@wdio/spec-reporter": "^8.12.2",
29 | "glob": "^10.3.10",
30 | "obsidian": "^1.4.11",
31 | "rollup": "2.32.1",
32 | "rollup-plugin-node-polyfills": "^0.2.1",
33 | "showdown-highlight": "^3.1.0",
34 | "ts-node": "^10.9.1",
35 | "tslib": "^2.6.1",
36 | "typescript": "^5.2.2",
37 | "wdio-chromedriver-service": "^8.1.1",
38 | "wdio-docker-service": "^3.2.1",
39 | "wdio-wait-for": "^3.0.7"
40 | },
41 | "dependencies": {
42 | "byte-base64": "^1.1.0",
43 | "showdown": "^2.1.0",
44 | "ts-md5": "^1.2.7",
45 | "webdriver": "^8.5.5"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/prepare-wdio.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | mkdir -p tests/test_config
4 | mkdir -p tests/test_vault
5 | mkdir -p tests/specs_gen
6 | mkdir -p tests/test_outputs
7 |
8 | # Copy Built plugin
9 | rm -rf tests/defaults/test_vault/.obsidian/plugins/obsidian-to-anki-plugin
10 | mkdir -p tests/defaults/test_vault/.obsidian/plugins/obsidian-to-anki-plugin
11 | cp manifest.json styles.css main.js tests/defaults/test_vault/.obsidian/plugins/obsidian-to-anki-plugin/
12 |
13 | # Setup docker volumes
14 | rm -rf tests/test_vault
15 | rm -rf tests/test_config
16 |
17 | cp -Rf tests/defaults/test_vault tests/
18 | cp -Rf tests/defaults/test_config tests/
19 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Gooey==1.0.4.0.0.0
2 | Markdown==3.2.2
3 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { nodeResolve } from '@rollup/plugin-node-resolve'
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import typescript from '@rollup/plugin-typescript';
4 | import nodePolyfills from 'rollup-plugin-node-polyfills'
5 |
6 | export default {
7 | input: 'main.ts',
8 | output: {
9 | file: 'main.js',
10 | format: 'cjs',
11 | exports: "default"
12 | },
13 | plugins: [nodeResolve(), commonjs(), typescript()],
14 | external: ["obsidian", "path"]
15 | };
16 |
--------------------------------------------------------------------------------
/root/defaults/autostart:
--------------------------------------------------------------------------------
1 | xset -b
2 | mkdir -p /config/logs
3 |
4 | {
5 |
6 | echo "abc" | sudo -s chmod +x /defaults/reset_perms.sh
7 | (sleep 1s && echo "abc" | sudo -s /defaults/reset_perms.sh) &
8 |
9 | sudo -u abc
10 |
11 | # # ss_dir =
12 | for file in /vaults/**/*.md; do test_name=$(basename $file); done
13 | test_name=$(echo $test_name | awk -F [.] '{print $1}')
14 |
15 | (sleep 2s && export LANG="LANG=C.UTF-8";LANGUAGE="en_US.UTF-8";LC_CTYPE="C.UTF-8" anki >> /config/logs/anki.log 2>&1) &
16 | (sleep 8s && echo "Executing PreTest ss" >> /config/logs/gnome.log 2>&1 && gnome-screenshot >> /config/logs/gnome.log 2>&1 && rename "s/Screenshot from .*/Anki PreTest.png/" /config/*.png) &
17 |
18 | ls -alh /defaults/ >> /config/logs/obsidian.log 2>&1
19 |
20 | echo "abc" | sudo -S chmod +x /defaults/obsidian_anki.sh
21 |
22 | ls -alh /defaults/ >> /config/logs/obsidian.log 2>&1
23 |
24 | (sleep 10s && /defaults/obsidian_anki.sh >> /config/logs/obsidian.log 2>&1) &
25 | # (sleep 5s && cd /obs_to_anki/ && npm run wdio >> logs/npm.log)
26 |
27 | (sleep 12s && sudo /etc/init.d/ssh start >> /config/logs/sshd.log 2>&1 )
28 |
29 | # remove any left over ssh client config from previous run
30 | rm -rf /config/.ssh
31 | (sleep 1s && sshpass -p "abc" ssh -o StrictHostKeyChecking=no -4 -L 0.0.0.0:8888:localhost:8890 abc@localhost -N >> /config/logs/ssh.log ) &
32 |
33 | } >> /config/logs/autostart.log 2>&1
--------------------------------------------------------------------------------
/root/defaults/menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
24 |
--------------------------------------------------------------------------------
/root/defaults/obsidian_anki.sh:
--------------------------------------------------------------------------------
1 |
2 | echo "Starting Obsisidan .... " >> /config/logs/obsidian.log 2>&1
3 |
4 | # permissions
5 | echo "abc" | sudo -S chown -R abc:abc /vaults
6 |
7 | testFound=false
8 | while [ ! testFound ]
9 | do
10 | for f in /vaults/*/*.md;
11 | do
12 | [ -e "$f" ] && testFound=true || testFound=false
13 | ## Check if the glob gets expanded to existing files.
14 | ## If not, f here will be exactly the pattern above
15 | ## and the exists test will evaluate to false.
16 | break
17 | done
18 | sleep 0.1s
19 | echo "waiting for Obsidian test files..."
20 | done
21 |
22 | sleep 1s
23 |
24 | /squashfs-root/obsidian --no-sandbox --disable-dev-shm-usage --disable-gpu --disable-software-rasterizer --remote-debugging-port=8890 --window-position=400,10
25 |
26 | echo "Obsisidan Ended .... " >> /config/logs/obsidian.log 2>&1
27 |
28 | # ss_dir =
29 | for file in /vaults/**/*.md; do test_name=$(basename $file); done
30 | test_name=$(echo $test_name | awk -F [.] '{print $1}')
31 |
32 | echo "Executing PostTest ss" >> /config/logs/gnome.log 2>&1 && gnome-screenshot >> /config/logs/gnome.log 2>&1 && rename "s/Screenshot from .*/Anki PostTest_${test_name}.png/" /config/*.png
33 | rename "s/Anki PreTest.png/Anki PreTest_${test_name}.png/" /config/*.png
34 |
35 | # ls -alh >> /config/logs/gnome.log
36 | # ls -alh /config/ >> /config/logs/gnome.log
37 | # ls -alh "/config/.local/share/Anki2/User 1/" >> /config/logs/gnome.log
38 |
39 |
40 | # echo "abc" | sudo -S chown -R 1000:1000 \
41 | # /config \
42 | # /vaults \
43 | # /squashfs-root
44 |
45 | # echo "abc" | sudo -S chmod 775 -R 1000:1000 \
46 | # /config \
47 | # /vaults \
48 | # /squashfs-root
49 |
50 | # ls -alh >> /config/logs/gnome.log
51 | # ls -alh /config/ >> /config/logs/gnome.log
52 | # ls -alh "/config/.local/share/Anki2/User 1/" >> /config/logs/gnome.log
53 |
54 | sleep 2s
55 |
56 | pkill anki
57 |
58 | sleep 2s # permissions denied since, anki would be still tearning down and locking sqlite db
59 |
60 | test_name_anki="${test_name}_Anki"
61 |
62 | # echo "abc" | sudo chown -R abc:abc /config
63 |
64 | echo "abc" | sudo -S mkdir -p "/config/.local/share/test_outputs/$test_name"
65 | echo "abc" | sudo -S mv -f /config/.local/share/Anki2 "/config/.local/share/test_outputs/$test_name"
66 | echo "abc" | sudo -S mkdir -p /config/.local/share/Anki2
67 | echo "abc" | sudo -S cp -Raf /config/.local/share/Anki2default/* /config/.local/share/Anki2
68 |
69 | # sleep 3s
70 | # Let wdio complete its post test checks or other checks with obsidian files
71 | while [ ! -f /vaults/unlock ]
72 | do
73 | sleep 0.1s
74 | done
75 |
76 | echo "abc" | sudo -S mkdir -p "/config/.local/share/test_outputs/$test_name/Obsidian/"
77 | echo "abc" | sudo -S mv -f "/vaults/$test_name" "/config/.local/share/test_outputs/$test_name/Obsidian"
78 | echo "abc" | sudo -S rm -rf /vaults/*
79 |
80 | echo "abc" | sudo chown -R abc:abc /config
81 |
82 | # sleep 5s # Let wdio copy test_vault_suite
83 | testFound=false
84 | while [ ! testFound ]
85 | do
86 | for f in /vaults/*/*.md;
87 | do
88 | [ -e "$f" ] && testFound=true || testFound=false
89 | ## Check if the glob gets expanded to existing files.
90 | ## If not, f here will be exactly the pattern above
91 | ## and the exists test will evaluate to false.
92 | break
93 | done
94 | sleep 0.1s
95 | echo "waiting for Obsidian test files..."
96 | done
97 |
98 | sleep 1s
99 |
100 | echo "abc" | sudo -S chmod +x /defaults/autostart
101 | /defaults/autostart
--------------------------------------------------------------------------------
/root/defaults/reset_perms.sh:
--------------------------------------------------------------------------------
1 | while [ 1 ]
2 | do
3 | if [ -f /config/reset_perms ];
4 | then
5 | echo "abc" | sudo -s chown -R abc:abc /vaults /config
6 | echo "abc" | sudo -s chmod -R 775 /vaults /config
7 | echo "abc" | rm -rf /config/reset_perms
8 | fi
9 | sleep 1 # or less like 0.2
10 | done
--------------------------------------------------------------------------------
/root/defaults/startwm.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | /startpulse.sh &
3 | /usr/bin/openbox-session > /dev/null 2>&1
4 |
--------------------------------------------------------------------------------
/root/etc/cont-init.d/50-config:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 |
3 | if [ -n "$PASSWORD" ]; then
4 | echo "abc:${PASSWORD}" | chpasswd
5 | echo "**** Setting password from environment variable. ****"
6 | else
7 | echo "**** No auth enabled. To enable auth, you can set the PASSWORD var in docker arguments. ****"
8 | fi
9 |
10 |
11 | if [ ! -d /vaults ]; then
12 | echo "**** Creating vaults folder. ****"
13 | mkdir -p /vaults;
14 | fi
15 |
16 | echo "********************************"
17 | echo "**** Debug Information ****"
18 | echo "********************************"
19 | echo ""
20 | echo "********************************"
21 | echo "**** Start Date Information ****"
22 | echo "********************************"
23 | echo "TZ: ${TZ}"
24 | echo "Running dpkg-reconfigure -f noninteractive tzdata"
25 | echo "${TZ}" >/etc/timezone
26 | dpkg-reconfigure -f noninteractive tzdata
27 | echo "Date UTC"
28 | date --utc
29 | echo "Date Local"
30 | date
31 | echo "Zone Info"
32 | zdump /usr/share/zoneinfo/${TZ}
33 | echo "Time Zone Offsets"
34 | zdump -v /etc/localtime
35 | echo "********************************"
36 | echo "**** End Date Information ****"
37 | echo "********************************"
38 |
39 | # permissions
40 | chown -R abc:abc \
41 | /config \
42 | /vaults \
43 | /squashfs-root
44 |
--------------------------------------------------------------------------------
/root/etc/cont-init.d/56-openboxcopy:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 |
3 | # default file copies first run
4 | [[ ! -f /config/.config/openbox/menu.xml ]] && \
5 | mkdir -p /config/.config/openbox && \
6 | cp /defaults/menu.xml /config/.config/openbox/menu.xml && \
7 | chown -R abc:abc /config/.config
8 |
--------------------------------------------------------------------------------
/src/anki.ts:
--------------------------------------------------------------------------------
1 | const ANKI_PORT: number = 8765
2 |
3 | import { AnkiConnectNote } from './interfaces/note-interface'
4 |
5 | export interface AnkiConnectRequest {
6 | action: string,
7 | version: 6,
8 | params: any
9 | }
10 |
11 | export function invoke(action: string, params={}) {
12 | return new Promise((resolve, reject) => {
13 | const xhr = new XMLHttpRequest()
14 | xhr.addEventListener('error', () => reject('failed to issue request'));
15 | xhr.addEventListener('load', () => {
16 | try {
17 | const response = JSON.parse(xhr.responseText);
18 | if (Object.getOwnPropertyNames(response).length != 2) {
19 | throw 'response has an unexpected number of fields';
20 | }
21 | if (!response.hasOwnProperty('error')) {
22 | throw 'response is missing required error field';
23 | }
24 | if (!response.hasOwnProperty('result')) {
25 | throw 'response is missing required result field';
26 | }
27 | if (response.error) {
28 | throw response.error;
29 | }
30 | resolve(response.result);
31 | } catch (e) {
32 | reject(e);
33 | }
34 | });
35 |
36 | xhr.open('POST', 'http://127.0.0.1:' + ANKI_PORT.toString());
37 | xhr.send(JSON.stringify({action, version: 6, params}));
38 | });
39 | }
40 |
41 | export function parse(response: {error: string, result: T}): T {
42 | //Helper function for parsing the result of a multi
43 | if (Object.getOwnPropertyNames(response).length != 2) {
44 | throw 'response has an unexpected number of fields'
45 | }
46 | if (!(response.hasOwnProperty('error'))) {
47 | throw 'response is missing required error field'
48 | }
49 | if (!(response.hasOwnProperty('result'))) {
50 | throw 'response is missing required result field';
51 | }
52 | if (response.error) {
53 | throw response.error
54 | }
55 | return response.result
56 | }
57 |
58 | // All the rest of these functions only return request objects as opposed to actually carrying out the action. For efficiency!
59 |
60 | function request(action: string, params={}): AnkiConnectRequest {
61 | return {action, version:6, params}
62 | }
63 |
64 | export function multi(actions: AnkiConnectRequest[]): AnkiConnectRequest {
65 | return request('multi', {actions: actions})
66 | }
67 |
68 | export function addNote(note: AnkiConnectNote): AnkiConnectRequest {
69 | return request('addNote', {note: note})
70 | }
71 |
72 | export function createDeck(deck: string): AnkiConnectRequest {
73 | return request('createDeck', {deck: deck})
74 | }
75 |
76 | export function deleteNotes(note_ids: number[]): AnkiConnectRequest {
77 | return request('deleteNotes', {notes: note_ids})
78 | }
79 |
80 | export function updateNoteFields(id: number, fields: Record): AnkiConnectRequest {
81 | return request(
82 | 'updateNoteFields', {
83 | note: {
84 | id: id,
85 | fields: fields
86 | }
87 | }
88 | )
89 | }
90 |
91 | export function notesInfo(note_ids: number[]): AnkiConnectRequest {
92 | return request(
93 | 'notesInfo', {
94 | notes: note_ids
95 | }
96 | )
97 | }
98 |
99 | export function changeDeck(card_ids: number[], deck: string): AnkiConnectRequest {
100 | return request(
101 | 'changeDeck', {
102 | cards: card_ids,
103 | deck: deck
104 | }
105 | )
106 | }
107 |
108 | export function removeTags(note_ids: number[], tags: string): AnkiConnectRequest {
109 | return request(
110 | 'removeTags', {
111 | notes: note_ids,
112 | tags: tags
113 | }
114 | )
115 | }
116 |
117 | export function addTags(note_ids: number[], tags: string): AnkiConnectRequest {
118 | return request(
119 | 'addTags', {
120 | notes: note_ids,
121 | tags: tags
122 | }
123 | )
124 | }
125 |
126 | export function getTags(): AnkiConnectRequest {
127 | return request('getTags')
128 | }
129 |
130 | export function storeMediaFile(filename: string, data: string): AnkiConnectRequest {
131 | return request(
132 | 'storeMediaFile', {
133 | filename: filename,
134 | data: data
135 | }
136 | )
137 | }
138 |
139 | export function storeMediaFileByPath(filename: string, path: string): AnkiConnectRequest {
140 | return request(
141 | 'storeMediaFile', {
142 | filename: filename,
143 | path: path
144 | }
145 | )
146 | }
147 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const ANKI_ICON: string = ` `
2 |
3 | export const OBS_INLINE_MATH_REGEXP: RegExp = /(?"
22 | const PARA_CLOSE:string = ""
23 |
24 | let cloze_unset_num: number = 1
25 |
26 | let converter: Converter = new Converter({
27 | simplifiedAutoLink: true,
28 | literalMidWordUnderscores: true,
29 | tables: true, tasklists: true,
30 | simpleLineBreaks: true,
31 | requireSpaceBeforeHeadingText: true,
32 | extensions: [showdownHighlight]
33 | })
34 |
35 | function escapeHtml(unsafe: string): string {
36 | return unsafe
37 | .replace(/&/g, "&")
38 | .replace(//g, ">")
40 | .replace(/"/g, """)
41 | .replace(/'/g, "'");
42 | }
43 |
44 | export class FormatConverter {
45 |
46 | file_cache: CachedMetadata
47 | vault_name: string
48 | detectedMedia: Set
49 |
50 | constructor(file_cache: CachedMetadata, vault_name: string) {
51 | this.vault_name = vault_name
52 | this.file_cache = file_cache
53 | this.detectedMedia = new Set()
54 | }
55 |
56 | getUrlFromLink(link: string): string {
57 | return "obsidian://open?vault=" + encodeURIComponent(this.vault_name) + String.raw`&file=` + encodeURIComponent(link)
58 | }
59 |
60 | format_note_with_url(note: AnkiConnectNote, url: string, field: string): void {
61 | note.fields[field] += 'Obsidian '
62 | }
63 |
64 | format_note_with_frozen_fields(note: AnkiConnectNote, frozen_fields_dict: Record>): void {
65 | for (let field in note.fields) {
66 | note.fields[field] += frozen_fields_dict[note.modelName][field]
67 | }
68 | }
69 |
70 | obsidian_to_anki_math(note_text: string): string {
71 | return note_text.replace(
72 | c.OBS_DISPLAY_MATH_REGEXP, "\\[$1\\]"
73 | ).replace(
74 | c.OBS_INLINE_MATH_REGEXP,
75 | "\\($1\\)"
76 | )
77 | }
78 |
79 | cloze_repl(_1: string, match_id: string, match_content: string): string {
80 | if (match_id == undefined) {
81 | let result = "{{c" + cloze_unset_num.toString() + "::" + match_content + "}}"
82 | cloze_unset_num += 1
83 | return result
84 | }
85 | let result = "{{c" + match_id + "::" + match_content + "}}"
86 | return result
87 | }
88 |
89 | curly_to_cloze(text: string): string {
90 | /*Change text in curly brackets to Anki-formatted cloze.*/
91 | text = text.replace(CLOZE_REGEXP, this.cloze_repl)
92 | cloze_unset_num = 1
93 | return text
94 | }
95 |
96 | getAndFormatMedias(note_text: string): string {
97 | if (!(this.file_cache.hasOwnProperty("embeds"))) {
98 | return note_text
99 | }
100 | for (let embed of this.file_cache.embeds) {
101 | if (note_text.includes(embed.original)) {
102 | this.detectedMedia.add(embed.link)
103 | if (AUDIO_EXTS.includes(extname(embed.link))) {
104 | note_text = note_text.replace(new RegExp(c.escapeRegex(embed.original), "g"), "[sound:" + basename(embed.link) + "]")
105 | } else if (IMAGE_EXTS.includes(extname(embed.link))) {
106 | note_text = note_text.replace(
107 | new RegExp(c.escapeRegex(embed.original), "g"),
108 | ' '
109 | )
110 | } else {
111 | console.warn("Unsupported extension: ", extname(embed.link))
112 | }
113 | }
114 | }
115 | return note_text
116 | }
117 |
118 | formatLinks(note_text: string): string {
119 | if (!(this.file_cache.hasOwnProperty("links"))) {
120 | return note_text
121 | }
122 | for (let link of this.file_cache.links) {
123 | note_text = note_text.replace(new RegExp(c.escapeRegex(link.original), "g"), '' + link.displayText + " ")
124 | }
125 | return note_text
126 | }
127 |
128 | censor(note_text: string, regexp: RegExp, mask: string): [string, string[]] {
129 | /*Take note_text and replace every match of regexp with mask, simultaneously adding it to a string array*/
130 | let matches: string[] = []
131 | for (let match of note_text.matchAll(regexp)) {
132 | matches.push(match[0])
133 | }
134 | return [note_text.replace(regexp, mask), matches]
135 | }
136 |
137 | decensor(note_text: string, mask:string, replacements: string[], escape: boolean): string {
138 | for (let replacement of replacements) {
139 | note_text = note_text.replace(
140 | mask, escape ? escapeHtml(replacement) : replacement
141 | )
142 | }
143 | return note_text
144 | }
145 |
146 | format(note_text: string, cloze: boolean, highlights_to_cloze: boolean): string {
147 | note_text = this.obsidian_to_anki_math(note_text)
148 | //Extract the parts that are anki math
149 | let math_matches: string[]
150 | let inline_code_matches: string[]
151 | let display_code_matches: string[]
152 | const add_highlight_css: boolean = note_text.match(c.OBS_DISPLAY_CODE_REGEXP) ? true : false;
153 | [note_text, math_matches] = this.censor(note_text, ANKI_MATH_REGEXP, MATH_REPLACE);
154 | [note_text, display_code_matches] = this.censor(note_text, c.OBS_DISPLAY_CODE_REGEXP, DISPLAY_CODE_REPLACE);
155 | [note_text, inline_code_matches] = this.censor(note_text, c.OBS_CODE_REGEXP, INLINE_CODE_REPLACE);
156 | if (cloze) {
157 | if (highlights_to_cloze) {
158 | note_text = note_text.replace(HIGHLIGHT_REGEXP, "{$1}")
159 | }
160 | note_text = this.curly_to_cloze(note_text)
161 | }
162 | note_text = this.getAndFormatMedias(note_text)
163 | note_text = this.formatLinks(note_text)
164 | //Special for formatting highlights now, but want to avoid any == in code
165 | note_text = note_text.replace(HIGHLIGHT_REGEXP, String.raw`$1 `)
166 | note_text = this.decensor(note_text, DISPLAY_CODE_REPLACE, display_code_matches, false)
167 | note_text = this.decensor(note_text, INLINE_CODE_REPLACE, inline_code_matches, false)
168 | note_text = converter.makeHtml(note_text)
169 | note_text = this.decensor(note_text, MATH_REPLACE, math_matches, true).trim()
170 | // Remove unnecessary paragraph tag
171 | if (note_text.startsWith(PARA_OPEN) && note_text.endsWith(PARA_CLOSE)) {
172 | note_text = note_text.slice(PARA_OPEN.length, -1 * PARA_CLOSE.length)
173 | }
174 | if (add_highlight_css) {
175 | note_text = ' ' + note_text
176 | }
177 | return note_text
178 | }
179 |
180 |
181 |
182 |
183 | }
184 |
--------------------------------------------------------------------------------
/src/interfaces/field-interface.ts:
--------------------------------------------------------------------------------
1 | export type FIELDS_DICT = Record
2 |
3 | export type FROZEN_FIELDS_DICT = Record>
4 |
--------------------------------------------------------------------------------
/src/interfaces/note-interface.ts:
--------------------------------------------------------------------------------
1 | export interface AnkiConnectNote {
2 | deckName: string,
3 | modelName: string,
4 | fields: Record,
5 | options: {
6 | allowDuplicate: boolean,
7 | duplicateScope: string
8 | }
9 | tags: Array,
10 | }
11 |
12 | export interface AnkiConnectNoteAndID {
13 | note: AnkiConnectNote,
14 | identifier: number | null
15 | }
16 |
--------------------------------------------------------------------------------
/src/interfaces/settings-interface.ts:
--------------------------------------------------------------------------------
1 | import { FIELDS_DICT } from './field-interface'
2 | import { AnkiConnectNote } from './note-interface'
3 |
4 | export interface PluginSettings {
5 | CUSTOM_REGEXPS: Record,
6 | FILE_LINK_FIELDS: Record,
7 | CONTEXT_FIELDS: Record,
8 | FOLDER_DECKS: Record,
9 | FOLDER_TAGS: Record,
10 | Syntax: {
11 | "Begin Note": string,
12 | "End Note": string,
13 | "Begin Inline Note": string,
14 | "End Inline Note": string,
15 | "Target Deck Line": string,
16 | "File Tags Line": string,
17 | "Delete Note Line": string,
18 | "Frozen Fields Line": string
19 | },
20 | Defaults: {
21 | "Scan Directory": string,
22 | "Tag": string,
23 | "Deck": string,
24 | "Scheduling Interval": number
25 | "Add File Link": boolean,
26 | "Add Context": boolean,
27 | "CurlyCloze": boolean,
28 | "CurlyCloze - Highlights to Clozes": boolean,
29 | "ID Comments": boolean,
30 | "Add Obsidian Tags": boolean
31 | }
32 | }
33 |
34 | export interface FileData {
35 | //All the data that a file would need.
36 | fields_dict: FIELDS_DICT
37 | custom_regexps: Record
38 | file_link_fields: Record
39 | context_fields: Record
40 | template: AnkiConnectNote
41 | EXISTING_IDS: number[]
42 | vault_name: string
43 |
44 | FROZEN_REGEXP: RegExp
45 | DECK_REGEXP: RegExp
46 | TAG_REGEXP: RegExp
47 | NOTE_REGEXP: RegExp
48 | INLINE_REGEXP: RegExp
49 | EMPTY_REGEXP: RegExp
50 |
51 | curly_cloze: boolean
52 | highlights_to_cloze: boolean
53 | comment: boolean
54 | add_context: boolean
55 | add_obs_tags: boolean
56 | }
57 |
58 | export interface ParsedSettings extends FileData {
59 | add_file_link: boolean
60 | folder_decks: Record
61 | folder_tags: Record
62 | }
63 |
--------------------------------------------------------------------------------
/src/setting-to-data.ts:
--------------------------------------------------------------------------------
1 | import { PluginSettings, ParsedSettings } from './interfaces/settings-interface'
2 | import { App } from 'obsidian'
3 | import * as AnkiConnect from './anki'
4 | import { ID_REGEXP_STR } from './note'
5 | import { escapeRegex } from './constants'
6 |
7 | export async function settingToData(app: App, settings: PluginSettings, fields_dict: Record): Promise {
8 | let result: ParsedSettings = {}
9 |
10 | //Some processing required
11 | result.vault_name = app.vault.getName()
12 | result.fields_dict = fields_dict
13 | result.custom_regexps = settings.CUSTOM_REGEXPS
14 | result.file_link_fields = settings.FILE_LINK_FIELDS
15 | result.context_fields = settings.CONTEXT_FIELDS
16 | result.folder_decks = settings.FOLDER_DECKS
17 | result.folder_tags = settings.FOLDER_TAGS
18 | result.template = {
19 | deckName: settings.Defaults.Deck,
20 | modelName: "",
21 | fields: {},
22 | options: {
23 | allowDuplicate: false,
24 | duplicateScope: "deck"
25 | },
26 | tags: [settings.Defaults.Tag]
27 | }
28 | result.EXISTING_IDS = await AnkiConnect.invoke('findNotes', {query: ""}) as number[]
29 |
30 | //RegExp section
31 | result.FROZEN_REGEXP = new RegExp(escapeRegex(settings.Syntax["Frozen Fields Line"]) + String.raw` - (.*?):\n((?:[^\n][\n]?)+)`, "g")
32 | result.DECK_REGEXP = new RegExp(String.raw`^` + escapeRegex(settings.Syntax["Target Deck Line"]) + String.raw`(?:\n|: )(.*)`, "m")
33 | result.TAG_REGEXP = new RegExp(String.raw`^` + escapeRegex(settings.Syntax["File Tags Line"]) + String.raw`(?:\n|: )(.*)`, "m")
34 | result.NOTE_REGEXP = new RegExp(String.raw`^` + escapeRegex(settings.Syntax["Begin Note"]) + String.raw`\n([\s\S]*?\n)` + escapeRegex(settings.Syntax["End Note"]), "gm")
35 | result.INLINE_REGEXP = new RegExp(escapeRegex(settings.Syntax["Begin Inline Note"]) + String.raw`(.*?)` + escapeRegex(settings.Syntax["End Inline Note"]), "g")
36 | result.EMPTY_REGEXP = new RegExp(escapeRegex(settings.Syntax["Delete Note Line"]) + ID_REGEXP_STR, "g")
37 |
38 | //Just a simple transfer
39 | result.curly_cloze = settings.Defaults.CurlyCloze
40 | result.highlights_to_cloze = settings.Defaults["CurlyCloze - Highlights to Clozes"]
41 | result.add_file_link = settings.Defaults["Add File Link"]
42 | result.comment = settings.Defaults["ID Comments"]
43 | result.add_context = settings.Defaults["Add Context"]
44 | result.add_obs_tags = settings.Defaults["Add Obsidian Tags"]
45 |
46 | return result
47 | }
48 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | .anki-settings-table td, .anki-settings-table th {
2 | border: 1px solid var(--background-modifier-border);
3 | padding: 4px 10px;
4 | }
5 |
6 | .anki-settings-table {
7 | border-collapse: collapse;
8 | display: none;
9 | }
10 |
11 | .anki-center {
12 | margin: auto;
13 | padding: 5px;
14 | padding-top: 30px;
15 | }
16 |
17 | .anki-rotated {
18 | transform: rotate(-90deg);
19 | }
20 |
--------------------------------------------------------------------------------
/tests/anki/test_basic_para.py:
--------------------------------------------------------------------------------
1 |
2 | import re
3 | import pytest
4 | from anki.errors import NotFoundError # noqa
5 | from anki.collection import Collection
6 | from anki.collection import SearchNode
7 | # from conftest import col
8 |
9 | test_name = 'basic_para'
10 | col_path = 'tests/test_outputs/{}/Anki2/User 1/collection.anki2'.format(test_name)
11 | test_file_path = 'tests/test_outputs/{}/Obsidian/{}/{}.md'.format(test_name, test_name, test_name)
12 |
13 | @pytest.fixture()
14 | def col():
15 | col = Collection(col_path)
16 | yield col
17 | col.close()
18 |
19 | def test_col_exists(col):
20 | assert not col.is_empty()
21 |
22 | def test_deck_default_exists(col: Collection):
23 | assert col.decks.id_for_name('Default') is not None
24 |
25 | def test_cards_count(col: Collection):
26 | assert len(col.find_cards( col.build_search_string(SearchNode(deck='Default')) )) == 4
27 |
28 | def test_cards_ids_from_obsidian(col: Collection):
29 |
30 | ID_REGEXP_STR = r'\n?(?:/g;
153 |
154 | const files = await glob('tests/test_vault/**/*.md')
155 |
156 | for (const file of files)
157 | {
158 | const filePostTest = fse.readFileSync(file, 'utf-8');
159 |
160 | let number_of_cards = (filePostTest.match(ID_REGEXP_STR) || []).length;
161 | let number_of_test_cards = (filePostTest.match(ID_REGEXP_STR_CARD) || []).length;
162 |
163 | console.log(`Number of cards in test file ${file} are - ${number_of_cards}, number_of_test_cards - ${number_of_test_cards}`);
164 |
165 | assert (number_of_cards == number_of_test_cards);
166 | }
167 |
168 | fse.writeFile('tests/test_vault/unlock', 'meow', (err) => {
169 | if (err)
170 | console.log('unlock file could not be created. Err: ', err);
171 | });
172 | await delay(5000); // >3000ms req; the last test of this spec, wait for anki and obsidian to close properly
173 | })
174 | })
175 |
176 |
--------------------------------------------------------------------------------
/tests/defaults/test_config/.config/obsidian/Preferences:
--------------------------------------------------------------------------------
1 | {"browser":{"enable_spellchecking":true},"electron":{"devtools":{"preferences":{"Inspector.drawerSplitViewState":"{\"horizontal\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","InspectorView.splitViewState":"{\"vertical\":{\"size\":0},\"horizontal\":{\"size\":353}}","Styles-pane-sidebar-tabOrder":"{\"Styles\":10,\"Computed\":20}","adornerSettings":"[{\"adorner\":\"grid\",\"isEnabled\":true},{\"adorner\":\"flex\",\"isEnabled\":true},{\"adorner\":\"ad\",\"isEnabled\":true},{\"adorner\":\"scroll-snap\",\"isEnabled\":true},{\"adorner\":\"container\",\"isEnabled\":true}]","closeableTabs":"{\"security\":true}","console.sidebar.width":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","console.sidebarSelectedFilter":"\"message\"","currentDockState":"\"bottom\"","elements.styles.sidebar.width":"{\"vertical\":{\"size\":0,\"showMode\":\"OnlyMain\"}}","elementsPanelSplitViewState":"{\"horizontal\":{\"size\":145}}","inspectorVersion":"31","lastDockState":"\"right\"","panel-selectedTab":"\"console\"","sourcesPanelNavigatorSplitViewState":"{\"vertical\":{\"size\":0,\"showMode\":\"Both\"}}","sourcesPanelSplitViewState":"{\"vertical\":{\"size\":216,\"showMode\":\"Both\"},\"horizontal\":{\"size\":68,\"showMode\":\"Both\"}}","undefined-tabOrder":"{\"sources.scopeChain\":10,\"sources.watch\":20,\"Styles\":1,\"Computed\":2}"}}},"partition":{"per_host_zoom_levels":{"2394503984146568780":{}}},"spellcheck":{"dictionaries":["en-US"],"dictionary":""}}
--------------------------------------------------------------------------------
/tests/defaults/test_config/.config/obsidian/e697835dbb2e89b2.json:
--------------------------------------------------------------------------------
1 | {"x":677,"y":24,"width":1024,"height":767,"isMaximized":false,"devTools":false,"zoom":0}
--------------------------------------------------------------------------------
/tests/defaults/test_config/.config/obsidian/obsidian.json:
--------------------------------------------------------------------------------
1 | {"vaults":{"e697835dbb2e89b2":{"path":"/vaults","ts":1677877063523,"open":true}}}
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2/User 1/backups/backup-2023-03-06-00.48.36.colpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/tests/defaults/test_config/.local/share/Anki2/User 1/backups/backup-2023-03-06-00.48.36.colpkg
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2/User 1/collection.anki2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/tests/defaults/test_config/.local/share/Anki2/User 1/collection.anki2
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2/addons21/2055492159/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiKey": null,
3 | "apiLogPath": null,
4 | "webBindAddress": "127.0.0.1",
5 | "webBindPort": 8765,
6 | "webCorsOriginList": ["http://localhost"],
7 | "ignoreOriginList": []
8 | }
9 |
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2/addons21/2055492159/config.md:
--------------------------------------------------------------------------------
1 | Read the documentation on the [AnkiConnect](https://foosoft.net/projects/anki-connect/) project page for details.
2 |
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2/addons21/2055492159/meta.json:
--------------------------------------------------------------------------------
1 | {"name": "AnkiConnect", "mod": 1663546263, "min_point_version": 45, "max_point_version": 45, "branch_index": 1, "disabled": false, "config": {"apiKey": null, "apiLogPath": null, "webBindAddress": "127.0.0.1", "webBindPort": 8765, "webCorsOrigin": "http://localhost", "webCorsOriginList": ["http://localhost", "app://obsidian.md"]}}
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2/addons21/2055492159/util.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016-2021 Alex Yatskov
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import os
17 | import sys
18 |
19 | import anki
20 | import anki.sync
21 | import aqt
22 | import enum
23 |
24 | #
25 | # Utilities
26 | #
27 |
28 | class MediaType(enum.Enum):
29 | Audio = 1
30 | Video = 2
31 | Picture = 3
32 |
33 |
34 | def download(url):
35 | client = anki.sync.AnkiRequestsClient()
36 | client.timeout = setting('webTimeout') / 1000
37 |
38 | resp = client.get(url)
39 | if resp.status_code != 200:
40 | raise Exception('{} download failed with return code {}'.format(url, resp.status_code))
41 |
42 | return client.streamContent(resp)
43 |
44 |
45 | def api(*versions):
46 | def decorator(func):
47 | setattr(func, 'versions', versions)
48 | setattr(func, 'api', True)
49 | return func
50 |
51 | return decorator
52 |
53 |
54 | def cardQuestion(card):
55 | if getattr(card, 'question', None) is None:
56 | return card._getQA()['q']
57 |
58 | return card.question()
59 |
60 |
61 | def cardAnswer(card):
62 | if getattr(card, 'answer', None) is None:
63 | return card._getQA()['a']
64 |
65 | return card.answer()
66 |
67 |
68 | DEFAULT_CONFIG = {
69 | 'apiKey': None,
70 | 'apiLogPath': None,
71 | 'apiPollInterval': 25,
72 | 'apiVersion': 6,
73 | 'webBacklog': 5,
74 | 'webBindAddress': os.getenv('ANKICONNECT_BIND_ADDRESS', '127.0.0.1'),
75 | 'webBindPort': 8765,
76 | 'webCorsOrigin': os.getenv('ANKICONNECT_CORS_ORIGIN', None),
77 | 'webCorsOriginList': ['http://localhost'],
78 | 'ignoreOriginList': [],
79 | 'webTimeout': 10000,
80 | }
81 |
82 | def setting(key):
83 | try:
84 | return aqt.mw.addonManager.getConfig(__name__).get(key, DEFAULT_CONFIG[key])
85 | except:
86 | raise Exception('setting {} not found'.format(key))
87 |
88 |
89 | # see https://github.com/FooSoft/anki-connect/issues/308
90 | # fixed in https://github.com/ankitects/anki/commit/0b2a226d
91 | def patch_anki_2_1_50_having_null_stdout_on_windows():
92 | if sys.stdout is None:
93 | sys.stdout = open(os.devnull, "w", encoding="utf8")
94 |
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2/prefs21.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/tests/defaults/test_config/.local/share/Anki2/prefs21.db
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2default/User 1/backups/backup-2023-03-06-00.48.36.colpkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/tests/defaults/test_config/.local/share/Anki2default/User 1/backups/backup-2023-03-06-00.48.36.colpkg
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2default/User 1/collection.anki2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/tests/defaults/test_config/.local/share/Anki2default/User 1/collection.anki2
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2default/addons21/2055492159/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiKey": null,
3 | "apiLogPath": null,
4 | "webBindAddress": "127.0.0.1",
5 | "webBindPort": 8765,
6 | "webCorsOriginList": ["http://localhost"],
7 | "ignoreOriginList": []
8 | }
9 |
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2default/addons21/2055492159/config.md:
--------------------------------------------------------------------------------
1 | Read the documentation on the [AnkiConnect](https://foosoft.net/projects/anki-connect/) project page for details.
2 |
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2default/addons21/2055492159/meta.json:
--------------------------------------------------------------------------------
1 | {"name": "AnkiConnect", "mod": 1663546263, "min_point_version": 45, "max_point_version": 45, "branch_index": 1, "disabled": false, "config": {"apiKey": null, "apiLogPath": null, "webBindAddress": "127.0.0.1", "webBindPort": 8765, "webCorsOrigin": "http://localhost", "webCorsOriginList": ["http://localhost", "app://obsidian.md"]}}
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2default/addons21/2055492159/util.py:
--------------------------------------------------------------------------------
1 | # Copyright 2016-2021 Alex Yatskov
2 | #
3 | # This program is free software: you can redistribute it and/or modify
4 | # it under the terms of the GNU General Public License as published by
5 | # the Free Software Foundation, either version 3 of the License, or
6 | # (at your option) any later version.
7 | #
8 | # This program is distributed in the hope that it will be useful,
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 | # GNU General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU General Public License
14 | # along with this program. If not, see .
15 |
16 | import os
17 | import sys
18 |
19 | import anki
20 | import anki.sync
21 | import aqt
22 | import enum
23 |
24 | #
25 | # Utilities
26 | #
27 |
28 | class MediaType(enum.Enum):
29 | Audio = 1
30 | Video = 2
31 | Picture = 3
32 |
33 |
34 | def download(url):
35 | client = anki.sync.AnkiRequestsClient()
36 | client.timeout = setting('webTimeout') / 1000
37 |
38 | resp = client.get(url)
39 | if resp.status_code != 200:
40 | raise Exception('{} download failed with return code {}'.format(url, resp.status_code))
41 |
42 | return client.streamContent(resp)
43 |
44 |
45 | def api(*versions):
46 | def decorator(func):
47 | setattr(func, 'versions', versions)
48 | setattr(func, 'api', True)
49 | return func
50 |
51 | return decorator
52 |
53 |
54 | def cardQuestion(card):
55 | if getattr(card, 'question', None) is None:
56 | return card._getQA()['q']
57 |
58 | return card.question()
59 |
60 |
61 | def cardAnswer(card):
62 | if getattr(card, 'answer', None) is None:
63 | return card._getQA()['a']
64 |
65 | return card.answer()
66 |
67 |
68 | DEFAULT_CONFIG = {
69 | 'apiKey': None,
70 | 'apiLogPath': None,
71 | 'apiPollInterval': 25,
72 | 'apiVersion': 6,
73 | 'webBacklog': 5,
74 | 'webBindAddress': os.getenv('ANKICONNECT_BIND_ADDRESS', '127.0.0.1'),
75 | 'webBindPort': 8765,
76 | 'webCorsOrigin': os.getenv('ANKICONNECT_CORS_ORIGIN', None),
77 | 'webCorsOriginList': ['http://localhost'],
78 | 'ignoreOriginList': [],
79 | 'webTimeout': 10000,
80 | }
81 |
82 | def setting(key):
83 | try:
84 | return aqt.mw.addonManager.getConfig(__name__).get(key, DEFAULT_CONFIG[key])
85 | except:
86 | raise Exception('setting {} not found'.format(key))
87 |
88 |
89 | # see https://github.com/FooSoft/anki-connect/issues/308
90 | # fixed in https://github.com/ankitects/anki/commit/0b2a226d
91 | def patch_anki_2_1_50_having_null_stdout_on_windows():
92 | if sys.stdout is None:
93 | sys.stdout = open(os.devnull, "w", encoding="utf8")
94 |
--------------------------------------------------------------------------------
/tests/defaults/test_config/.local/share/Anki2default/prefs21.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/tests/defaults/test_config/.local/share/Anki2default/prefs21.db
--------------------------------------------------------------------------------
/tests/defaults/test_vault/.obsidian/community-plugins.json:
--------------------------------------------------------------------------------
1 | [
2 | "obsidian-to-anki-plugin"
3 | ]
--------------------------------------------------------------------------------
/tests/defaults/test_vault/.obsidian/hotkeys.json:
--------------------------------------------------------------------------------
1 | {
2 | "app:reload": [
3 | {
4 | "modifiers": [
5 | "Mod",
6 | "Shift"
7 | ],
8 | "key": "R"
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/tests/defaults/test_vault/.obsidian/plugins/obsidian-to-anki-plugin/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-to-anki-plugin",
3 | "name": "Obsidian_to_Anki",
4 | "version": "3.4.2",
5 | "minAppVersion": "0.9.20",
6 | "description": "This is an Anki integration plugin! Designed for efficient bulk exporting.",
7 | "author": "Pseudonium",
8 | "authorUrl": "https://github.com/Pseudonium/Obsidian_to_Anki",
9 | "isDesktopOnly": true
10 | }
11 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault/.obsidian/plugins/obsidian-to-anki-plugin/styles.css:
--------------------------------------------------------------------------------
1 | .anki-settings-table td, .anki-settings-table th {
2 | border: 1px solid var(--background-modifier-border);
3 | padding: 4px 10px;
4 | }
5 |
6 | .anki-settings-table {
7 | border-collapse: collapse;
8 | display: none;
9 | }
10 |
11 | .anki-center {
12 | margin: auto;
13 | padding: 5px;
14 | padding-top: 30px;
15 | }
16 |
17 | .anki-rotated {
18 | transform: rotate(-90deg);
19 | }
20 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault/.obsidian/workspace:
--------------------------------------------------------------------------------
1 | {
2 | "main": {
3 | "id": "1387a50a5b3cd053",
4 | "type": "split",
5 | "children": [
6 | {
7 | "id": "d82287a3c9d718cb",
8 | "type": "leaf",
9 | "state": {
10 | "type": "markdown",
11 | "state": {
12 | "file": "Test.md",
13 | "mode": "source",
14 | "source": false
15 | }
16 | }
17 | }
18 | ],
19 | "direction": "vertical"
20 | },
21 | "left": {
22 | "id": "6e7466e34c5de4fe",
23 | "type": "split",
24 | "children": [
25 | {
26 | "id": "ccec4e3e13c829d9",
27 | "type": "tabs",
28 | "children": [
29 | {
30 | "id": "5f108fcfd2cbd93c",
31 | "type": "leaf",
32 | "state": {
33 | "type": "file-explorer",
34 | "state": {}
35 | }
36 | },
37 | {
38 | "id": "98063e8d1dfcb387",
39 | "type": "leaf",
40 | "state": {
41 | "type": "search",
42 | "state": {
43 | "query": "",
44 | "matchingCase": false,
45 | "explainSearch": false,
46 | "collapseAll": false,
47 | "extraContext": false,
48 | "sortOrder": "alphabetical"
49 | }
50 | }
51 | },
52 | {
53 | "id": "435d984a32d09111",
54 | "type": "leaf",
55 | "state": {
56 | "type": "starred",
57 | "state": {}
58 | }
59 | }
60 | ]
61 | }
62 | ],
63 | "direction": "horizontal",
64 | "width": 300
65 | },
66 | "right": {
67 | "id": "64731270e45fee11",
68 | "type": "split",
69 | "children": [
70 | {
71 | "id": "0e8d83ae10d5efdb",
72 | "type": "tabs",
73 | "children": [
74 | {
75 | "id": "0abfe1904d28ac0c",
76 | "type": "leaf",
77 | "state": {
78 | "type": "backlink",
79 | "state": {
80 | "file": "Test.md",
81 | "collapseAll": false,
82 | "extraContext": false,
83 | "sortOrder": "alphabetical",
84 | "showSearch": false,
85 | "searchQuery": "",
86 | "backlinkCollapsed": false,
87 | "unlinkedCollapsed": true
88 | }
89 | }
90 | },
91 | {
92 | "id": "b7ae389c0f519419",
93 | "type": "leaf",
94 | "state": {
95 | "type": "outgoing-link",
96 | "state": {
97 | "file": "Test.md",
98 | "linksCollapsed": false,
99 | "unlinkedCollapsed": true
100 | }
101 | }
102 | },
103 | {
104 | "id": "c06201c00da8c125",
105 | "type": "leaf",
106 | "state": {
107 | "type": "tag",
108 | "state": {
109 | "sortOrder": "frequency",
110 | "useHierarchy": true
111 | }
112 | }
113 | },
114 | {
115 | "id": "bd0a9a3fabfcd432",
116 | "type": "leaf",
117 | "state": {
118 | "type": "outline",
119 | "state": {
120 | "file": "Test.md"
121 | }
122 | }
123 | }
124 | ]
125 | }
126 | ],
127 | "direction": "horizontal",
128 | "width": 300,
129 | "collapsed": true
130 | },
131 | "active": "d82287a3c9d718cb",
132 | "lastOpenFiles": []
133 | }
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/basic_para/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "CUSTOM_REGEXPS": {
4 | "Basic": "^#+(.+)\n*((?:\n(?:^[^\n#].{0,2}$|^[^\n#].{3}(?
2 | # Style
3 | This style is suitable for having the header as the front, and the answer as the back
4 | # Overall heading
5 |
6 | ## Subheading 1
7 | You're allowed to nest headers within each other
8 |
9 | ## Subheading 2
10 | It'll take the deepest level for the question
11 |
12 | ## Subheading 3
13 |
14 |
15 |
16 | It'll even
17 | Span over
18 | Multiple lines, and ignore preceding whitespace
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/basic_para_3/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "CUSTOM_REGEXPS": {
4 | "Basic": "^#{3,}(.+)\n*((?:\n(?:^[^\n#].{0,2}$|^[^\n#].{3}(?
9 | ### Subheading 2
10 | It'll take the deepest level for the question. This should have a card
11 |
12 | ### Subheading 3
13 | This should too
14 |
15 | #### Subheading 3.1
16 | Yeah this too
17 |
18 |
19 |
20 | It'll even
21 | Span over
22 | Multiple lines, and ignore preceding whitespace
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/basic_sync/basic_sync.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test.
5 | Back: Test successful!
6 | Tags: Testing
7 | END
8 |
9 |
10 | START
11 | Basic
12 | Front: This is a test with Front specified.
13 | Back: Test successful!
14 | Tags: Testing 2
15 | END
16 |
17 |
18 | START
19 | Basic
20 | This is a test.
21 | And the test is continuing.
22 | Back: Test successful!
23 | END
24 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/cloze_highlight/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
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": "((?:.+\n)*(?:.*==.*)(?:\n(?:^.{1,3}$|^.{4}(?
4 |
5 | This is a ==c1::test==.
6 | This is also a Test ==c2::meow== !
7 |
8 |
9 |
10 | This must be a new card ==test2== !!
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/cloze_para/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
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": "((?:.+\n)*(?:.*{.*)(?:\n(?:^.{1,3}$|^.{4}(?
2 |
3 | The idea of {cloze paragraph style} is to be able to recognise any paragraphs that contain {cloze deletions}.
4 |
5 | The script should ignore paragraphs that have math formatting like $\frac{3}{4}$ but no actual cloze deletions.
6 |
7 |
8 |
9 | With {2:CurlyCloze} enabled, you can also use the {c1|easier cloze formatting},
10 | but of course {{c3::Anki}}'s formatting is always an option.
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/cloze_sync/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
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": "",
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 | },
43 | "Added Media": [],
44 | "File Hashes": {},
45 | "fields_dict": {
46 | "Basic": [
47 | "Front",
48 | "Back"
49 | ],
50 | "Basic (and reversed card)": [
51 | "Front",
52 | "Back"
53 | ],
54 | "Basic (optional reversed card)": [
55 | "Front",
56 | "Back",
57 | "Add Reverse"
58 | ],
59 | "Basic (type in the answer)": [
60 | "Front",
61 | "Back"
62 | ],
63 | "Cloze": [
64 | "Text",
65 | "Back Extra"
66 | ]
67 | }
68 | }
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/cloze_sync/cloze_sync.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Cloze
4 |
5 | This is a {{c1::cloze note}}
6 |
7 | END
8 |
9 |
10 |
11 | START
12 | Cloze
13 |
14 | This is a {cloze} note with {two clozes}
15 |
16 | END
17 |
18 |
19 |
20 | START
21 | Cloze
22 |
23 | This is a {2:cloze} note with {1:id syntax}
24 |
25 | END
26 |
27 |
28 |
29 | START
30 | Cloze
31 |
32 | This is a {2|cloze} {3|note} with {1|alternate id syntax}
33 |
34 | END
35 |
36 |
37 |
38 | START
39 | Cloze
40 |
41 | This is a {c1:cloze} note with {c2:another} type of {c3:id syntax}
42 |
43 | END
44 |
45 |
46 |
47 | START
48 | Cloze
49 |
50 | This is a {c1|cloze} note with {c2|yet another} type of {c3|id syntax}
51 |
52 | END
53 |
54 |
55 |
56 | START
57 | Cloze
58 |
59 | This is a {cloze} note with {multiple} non-id clozes, as well as {2:some clozes} with {c1|other styles}
60 |
61 | END
62 |
63 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/context_test/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
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": "Back",
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 | "Basic": "Back"
19 | },
20 | "FOLDER_DECKS": {},
21 | "FOLDER_TAGS": {},
22 | "Syntax": {
23 | "Begin Note": "START",
24 | "End Note": "END",
25 | "Begin Inline Note": "STARTI",
26 | "End Inline Note": "ENDI",
27 | "Target Deck Line": "TARGET DECK",
28 | "File Tags Line": "FILE TAGS",
29 | "Delete Note Line": "DELETE",
30 | "Frozen Fields Line": "FROZEN"
31 | },
32 | "Defaults": {
33 | "Scan Directory": "",
34 | "Tag": "Obsidian_to_Anki",
35 | "Deck": "Default",
36 | "Scheduling Interval": 0,
37 | "Add File Link": false,
38 | "Add Context": true,
39 | "CurlyCloze": true,
40 | "CurlyCloze - Highlights to Clozes": false,
41 | "ID Comments": true,
42 | "Add Obsidian Tags": false
43 | }
44 | },
45 | "Added Media": [],
46 | "File Hashes": {},
47 | "fields_dict": {
48 | "Basic": [
49 | "Front",
50 | "Back"
51 | ],
52 | "Basic (and reversed card)": [
53 | "Front",
54 | "Back"
55 | ],
56 | "Basic (optional reversed card)": [
57 | "Front",
58 | "Back",
59 | "Add Reverse"
60 | ],
61 | "Basic (type in the answer)": [
62 | "Front",
63 | "Back"
64 | ],
65 | "Cloze": [
66 | "Text",
67 | "Back Extra"
68 | ]
69 | }
70 | }
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/context_test/context_test.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Overall point
4 |
5 | ## Subheading 1
6 |
7 | ## Subheading 2
8 |
9 |
10 | START
11 | Basic
12 | This is a test
13 | Back: Test successful!
14 |
15 | END
16 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_deck/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
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": "((?:.+\n)*(?:.*{.*)(?:\n(?:^.{1,3}$|^.{4}(?
2 | START
3 | Basic
4 | This is a test. This card should be English Deck eventhough its in No Deck folder
5 | Back: Test successful!
6 | END
7 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_deck/Math meow/folder_deck.math.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test. Should be in Math deck.
5 | Back: Test successful!
6 | END
7 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_deck/Science meow/folder_deck.science.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test. Should be in Science deck
5 | Back: Test successful!
6 | END
7 |
8 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_deck/folder_deck.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test. Should be in Default deck.
5 | Back: Test successful!
6 | END
7 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_deck_tags/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
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": "((?:.+\n)*(?:.*{.*)(?:\n(?:^.{1,3}$|^.{4}(?
2 | START
3 | Basic
4 | This is a test. This card should be English Deck eventhough its in No Deck folder
5 | Back: Test successful!
6 | END
7 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_deck_tags/Math meow/folder_deck_tags.math.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test. Should be in Math deck.
5 | Back: Test successful!
6 | END
7 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_deck_tags/Science meow/folder_deck_tags.science.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test. Should be in Science deck
5 | Back: Test successful!
6 | END
7 |
8 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_deck_tags/folder_deck_tags.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test. Should be in Default deck.
5 | Back: Test successful!
6 | Tags: Testing
7 | END
8 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_scan/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "CUSTOM_REGEXPS": {
4 | "Basic": "^#+(.+)\n*((?:\n(?:^[^\n#].{0,2}$|^[^\n#].{3}(?
2 | # Style Subdir
3 | This style is suitable for having the header as the front, and the answer as the back
4 | # Overall heading Subdir
5 |
6 | ## Subheading 1 Subdir
7 | You're allowed to nest headers within each other
8 |
9 | ## Subheading 2 Subdir
10 | It'll take the deepest level for the question
11 |
12 | ## Subheading 3 Subdir
13 |
14 |
15 |
16 | It'll even
17 | Span over
18 | Multiple lines, and ignore preceding whitespace
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_scan/scan_dir/folder_scan.md:
--------------------------------------------------------------------------------
1 |
2 | # Style
3 | This style is suitable for having the header as the front, and the answer as the back
4 | # Overall heading
5 |
6 | ## Subheading 1
7 | You're allowed to nest headers within each other
8 |
9 | ## Subheading 2
10 | It'll take the deepest level for the question
11 |
12 | ## Subheading 3
13 |
14 |
15 |
16 | It'll even
17 | Span over
18 | Multiple lines, and ignore preceding whitespace
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/folder_scan/should_not_scan_dir/should_not_scan.md:
--------------------------------------------------------------------------------
1 |
2 | # Style snsmd
3 | This style is suitable for having the header as the front, and the answer as the back
4 | # Overall heading
5 |
6 | ## Subheading 1 snsmd
7 |
8 | You're allowed to nest headers within each other
9 |
10 | ## Subheading 2 snsmd
11 |
12 | It'll take the deepest level for the question
13 |
14 | ## Subheading 3 snsmd
15 |
16 |
17 |
18 | It'll even
19 | Span over
20 | Multiple lines, and ignore preceding whitespace
21 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/frozen_notes/frozen_notes.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | FROZEN - Basic:
4 | Front: What is the country's capital?
5 |
6 |
7 | START
8 | Basic
9 | India (This is a Test)
10 |
11 | Back: Delhi! Test successful!
12 | END
13 |
14 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/image_sync/image_sync.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Cloze
4 |
5 | Do . You'll want to do 'copy image address' on the image, and use that for the image url.
6 |
7 | END
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/inline_notes/inline_notes.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | STARTI [Basic] This is a test. Back: Test successful! ENDI
5 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/markdown_table/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "CUSTOM_REGEXPS": {
4 | "Basic": "\\|([^\n|]+)\\|\n\\|(?:[^\n|]+)\\|\n\\|([^\n|]+)\\|\n?",
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": "",
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 | },
43 | "Added Media": [],
44 | "File Hashes": {},
45 | "fields_dict": {
46 | "Basic": [
47 | "Front",
48 | "Back"
49 | ],
50 | "Basic (and reversed card)": [
51 | "Front",
52 | "Back"
53 | ],
54 | "Basic (optional reversed card)": [
55 | "Front",
56 | "Back",
57 | "Add Reverse"
58 | ],
59 | "Basic (type in the answer)": [
60 | "Front",
61 | "Back"
62 | ],
63 | "Cloze": [
64 | "Text",
65 | "Back Extra"
66 | ]
67 | }
68 | }
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/markdown_table/markdown_table.md:
--------------------------------------------------------------------------------
1 |
2 | | How do you use this style? |
3 | | ---- |
4 | | Just like this |
5 |
6 | Of course, the script will ignore anything outside a table.
7 |
8 | | Furthermore, the script | should also |
9 | | ----- | ----- |
10 | | Ignore any tables | with more than one column |
11 |
12 |
13 | | Why might this style be useful? |
14 | | --------- |
15 | | It looks nice when rendered as HTML in a markdown editor. |
16 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/markdown_test/markdown_test.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This note showcases a bunch of different markdown formatting.
5 | You can use *italics* or _italics_.
6 | **Bold** or __Bold__
7 | If you want to strongly emphasise, just **_do_** __*both*__
8 | # Headers are supported too
9 | ## At
10 | ### Varying
11 | #### Levels
12 |
13 | 1. You can get
14 | 2. Ordered lists
15 | 3. By doing numbers like this
16 |
17 | * Unordered lists
18 | * work like this.
19 | * Make sure to leave a gap between lists and other things
20 |
21 | Back: A few more elements to see.
22 | You can include [links](https://www.wikipedia.org/) to websites.
23 | `Code blocks` are supported
24 | Github-flavoured code blocks too, but Anki won't do syntax highlighting
25 | ```python
26 | print("Hello world!")
27 | ```
28 | Tables should hopefully work:
29 |
30 | First Header | Second Header
31 | ------------- | -------------
32 | Content Cell | Content Cell
33 | Content Cell | Content Cell
34 |
35 | Tags: Way_too_much_info
36 | END
37 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/math_test/math_test.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | Inline $x = 5$
5 | Back: Test successful!
6 | END
7 |
8 |
9 | START
10 | Basic
11 | Displayed $$z = 10$$
12 | Back: Test successful!
13 | END
14 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/music_embed/music_embed.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test with music
5 | ![[test.mp3]]
6 | Back: Test successful!
7 | END
8 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/music_embed/test.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ShootingKing-AM/Obsidian_to_Anki/85447851ecaf02ccb29b1155e1c10d338d11240e/tests/defaults/test_vault_suites/music_embed/test.mp3
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/neuracache_sync/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "CUSTOM_REGEXPS": {
4 | "Basic": "((?:[^\n][\n]?)+) #flashcard ?\n*((?:\n(?:^.{1,3}$|^.{4}(?
2 |
3 | In Neuracache style, to make a flashcard you do #flashcard
4 | The next lines then become the back of the flashcard
5 |
6 |
7 |
8 | If you want, it's certainly possible to
9 | do a multi-line question #flashcard
10 | You just need to make sure both
11 | the question and answer are one paragraph.
12 |
13 |
14 |
15 | And, of course #flashcard
16 |
17 |
18 | Whitespace is ignored!
19 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/ng_basic_update/.obsidian/workspace:
--------------------------------------------------------------------------------
1 | {
2 | "main": {
3 | "id": "1387a50a5b3cd053",
4 | "type": "split",
5 | "children": [
6 | {
7 | "id": "d82287a3c9d718cb",
8 | "type": "leaf",
9 | "state": {
10 | "type": "markdown",
11 | "state": {
12 | "file": "ng_basic_update/ng_basic_update.md",
13 | "mode": "source",
14 | "source": false
15 | }
16 | }
17 | }
18 | ],
19 | "direction": "vertical"
20 | },
21 | "left": {
22 | "id": "6e7466e34c5de4fe",
23 | "type": "split",
24 | "children": [
25 | {
26 | "id": "ccec4e3e13c829d9",
27 | "type": "tabs",
28 | "children": [
29 | {
30 | "id": "5f108fcfd2cbd93c",
31 | "type": "leaf",
32 | "state": {
33 | "type": "file-explorer",
34 | "state": {}
35 | }
36 | },
37 | {
38 | "id": "98063e8d1dfcb387",
39 | "type": "leaf",
40 | "state": {
41 | "type": "search",
42 | "state": {
43 | "query": "",
44 | "matchingCase": false,
45 | "explainSearch": false,
46 | "collapseAll": false,
47 | "extraContext": false,
48 | "sortOrder": "alphabetical"
49 | }
50 | }
51 | },
52 | {
53 | "id": "435d984a32d09111",
54 | "type": "leaf",
55 | "state": {
56 | "type": "starred",
57 | "state": {}
58 | }
59 | }
60 | ]
61 | }
62 | ],
63 | "direction": "horizontal",
64 | "width": 300
65 | },
66 | "right": {
67 | "id": "64731270e45fee11",
68 | "type": "split",
69 | "children": [
70 | {
71 | "id": "0e8d83ae10d5efdb",
72 | "type": "tabs",
73 | "children": [
74 | {
75 | "id": "0abfe1904d28ac0c",
76 | "type": "leaf",
77 | "state": {
78 | "type": "backlink",
79 | "state": {
80 | "file": "ng_basic_update/ng_basic_update.md",
81 | "collapseAll": false,
82 | "extraContext": false,
83 | "sortOrder": "alphabetical",
84 | "showSearch": false,
85 | "searchQuery": "",
86 | "backlinkCollapsed": false,
87 | "unlinkedCollapsed": true
88 | }
89 | }
90 | },
91 | {
92 | "id": "b7ae389c0f519419",
93 | "type": "leaf",
94 | "state": {
95 | "type": "outgoing-link",
96 | "state": {
97 | "file": "ng_basic_update/ng_basic_update.md",
98 | "linksCollapsed": false,
99 | "unlinkedCollapsed": true
100 | }
101 | }
102 | },
103 | {
104 | "id": "c06201c00da8c125",
105 | "type": "leaf",
106 | "state": {
107 | "type": "tag",
108 | "state": {
109 | "sortOrder": "frequency",
110 | "useHierarchy": true
111 | }
112 | }
113 | },
114 | {
115 | "id": "bd0a9a3fabfcd432",
116 | "type": "leaf",
117 | "state": {
118 | "type": "outline",
119 | "state": {
120 | "file": "ng_basic_update/ng_basic_update.md"
121 | }
122 | }
123 | }
124 | ]
125 | }
126 | ],
127 | "direction": "horizontal",
128 | "width": 300,
129 | "collapsed": true
130 | },
131 | "active": "d82287a3c9d718cb",
132 | "lastOpenFiles": []
133 | }
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/ng_basic_update/ng_basic_update.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test.
5 |
6 | Back: Test successful!
7 | END
8 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/ng_delete_sync/ng_delete_sync.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test which should not exist.
5 | Back:
6 | Test successful!
7 |
8 | END
9 |
10 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/question_answer/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "CUSTOM_REGEXPS": {
4 | "Basic": "^Q: ((?:.+\n)*)\n*A: (.+(?:\n(?:^.{1,3}$|^.{4}(?
4 | Q: How do you use this style?
5 | A: Just like this.
6 |
7 |
8 | Q: Can the question
9 | run over multiple lines?
10 | A: Yes, and
11 | So can the answer
12 |
13 |
14 | Q: Does the answer need to be immediately after the question?
15 |
16 |
17 | A: No, and preceding whitespace will be ignored.
18 |
19 |
20 | Q: How is this possible?
21 | A: The 'magic' of regular expressions!
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/remnote_inline/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "CUSTOM_REGEXPS": {
4 | "Basic": "^(.*[^\n:]{1}):{2}([^\n:]{1}.*)",
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": "",
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 | },
43 | "Added Media": [],
44 | "File Hashes": {},
45 | "fields_dict": {
46 | "Basic": [
47 | "Front",
48 | "Back"
49 | ],
50 | "Basic (and reversed card)": [
51 | "Front",
52 | "Back"
53 | ],
54 | "Basic (optional reversed card)": [
55 | "Front",
56 | "Back",
57 | "Add Reverse"
58 | ],
59 | "Basic (type in the answer)": [
60 | "Front",
61 | "Back"
62 | ],
63 | "Cloze": [
64 | "Text",
65 | "Back Extra"
66 | ]
67 | }
68 | }
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/remnote_inline/remnote_inline.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | This is how to use::Remnote single-line style
5 | The script won't see things outside of it.
6 |
7 | You can have::multiple notes in the same file
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/ruled_style/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "settings": {
3 | "CUSTOM_REGEXPS": {
4 | "Basic": "((?:[^\n][\n]?)+\n)-{3,}((?:\n(?:^.{1,3}$|^.{4}(?
4 |
5 | How do you use ruled style?
6 | ---
7 | You need at least three '-' between the front and back of the card.
8 |
9 |
10 |
11 |
12 | Are paragraphs
13 | supported?
14 | ---------
15 | Yes, but you need the front and back
16 | directly before and after the ruler.
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/tag_sync/.obsidian/plugins/obsidian-to-anki-plugin/data.json:
--------------------------------------------------------------------------------
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": "",
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": true
41 | }
42 | },
43 | "Added Media": [],
44 | "File Hashes": {},
45 | "fields_dict": {
46 | "Basic": [
47 | "Front",
48 | "Back"
49 | ],
50 | "Basic (and reversed card)": [
51 | "Front",
52 | "Back"
53 | ],
54 | "Basic (optional reversed card)": [
55 | "Front",
56 | "Back",
57 | "Add Reverse"
58 | ],
59 | "Basic (type in the answer)": [
60 | "Front",
61 | "Back"
62 | ],
63 | "Cloze": [
64 | "Text",
65 | "Back Extra"
66 | ]
67 | }
68 | }
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/tag_sync/tag_sync.file.inline.md:
--------------------------------------------------------------------------------
1 | FILE TAGS: Maths School Physics
2 |
3 |
4 | START
5 | Basic
6 | This is a test with file tags specified inline
7 | Back: Test successful!
8 | END
9 |
10 |
11 |
12 | START
13 | Basic
14 | This is a test 2 with file tags specified inline
15 | And the test is continuing.
16 | Back: Test successful!
17 | END
18 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/tag_sync/tag_sync.file.md:
--------------------------------------------------------------------------------
1 | FILE TAGS
2 | Maths School Physics
3 |
4 |
5 | START
6 | Basic
7 | This is a test with file tags specified in new line
8 | Back: Test successful!
9 | END
10 |
11 |
12 |
13 | START
14 | Basic
15 | This is a test 2 with file tags specified in new line
16 | And the test is continuing.
17 | Back: Test successful!
18 | END
19 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/tag_sync/tag_sync.md:
--------------------------------------------------------------------------------
1 |
2 | START
3 | Basic
4 | This is a test.
5 | Back: Test successful!
6 | Tags: Tag1 Tag2 Tag3
7 | END
8 |
9 |
10 | START
11 | Basic
12 | This is a test. This should not have any tags except default ones.
13 | And the test is continuing.
14 | Back: Test successful!
15 | END
16 |
17 |
18 | START
19 | Basic
20 | This is a test. this should have meow-tag
21 | And the test is continuing. #meow
22 | Back: Test successful!
23 | END
24 |
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/target_deck/target_deck.md:
--------------------------------------------------------------------------------
1 | TARGET DECK
2 | MathematicsInNextLine
3 |
4 |
5 | START
6 | Basic
7 | This is a test with target deck in a seperate line.
8 | Back: Test successful!
9 | END
--------------------------------------------------------------------------------
/tests/defaults/test_vault_suites/target_deck/target_deck.sameline.md:
--------------------------------------------------------------------------------
1 | TARGET DECK: MathematicsInSameLine
2 |
3 |
4 | START
5 | Basic
6 | This is a test with target deck in a same line.
7 | Back: Test successful!
8 | END
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "module": "ESNext",
5 | "types": [
6 | "node",
7 | "@wdio/globals/types",
8 | "expect-webdriverio",
9 | "@wdio/mocha-framework"
10 | ],
11 | "target": "es2022"
12 | }
13 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["esnext", "dom", "dom.iterable"],
4 | "downlevelIteration": true,
5 | "target": "esnext",
6 | "moduleResolution": "node",
7 | "allowSyntheticDefaultImports": true,
8 | },
9 | "exclude": ["node_modules", "tests/**/*.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "3.4.2": "0.9.20",
4 | "3.4.1": "0.9.20",
5 | "3.4.0": "0.9.20",
6 | "3.3.5": "0.9.20",
7 | "3.3.4": "0.9.20",
8 | "3.3.3": "0.9.20",
9 | "3.3.2": "0.9.20",
10 | "3.3.0": "0.9.20",
11 | "3.2.0": "0.9.20",
12 | "3.1.0": "0.9.20",
13 | "3.0.2": "0.9.20",
14 | "3.0.1": "0.9.20",
15 | "3.0.0": "0.9.20"
16 | }
17 |
--------------------------------------------------------------------------------