├── .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 | ![Remnote 1](Images/Remnote_1.png) 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 | ![Header 1](Images/Header_1.png) 88 | * Question answer style. 89 |
 90 |   Q: How do you use this style?
 91 |   A: Just like this.
 92 |   
93 | ![Question 1](Images/Question_1.png) 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 | ![Neuracache 1](Images/Neuracache_1.png) 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 | ![Ruled 1](Images/Ruled_1.png) 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 | ![Table 2](Images/Table_2.png) 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 | ![Cloze 1](Images/Cloze_1.png) 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 | Buy Me a Coffee at ko-fi.com 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 | 4 | 5 | 6 | /squashfs-root/obsidian --no-sandbox --disable-dev-shm-usage --disable-gpu --disable-software-rasterizer 7 | 8 | 9 | 10 | 11 | /usr/bin/xterm 12 | 13 | 14 | 15 | 16 | anki 17 | 18 | 19 | 20 | 21 | 22 | 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 | '' + embed.displayText + '' 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 ![alt-text](http://meow.in/meow.png). 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 | --------------------------------------------------------------------------------