├── .gitignore ├── LICENSE ├── README.md ├── ankdown ├── __init__.py ├── ankdown.py └── highlight.css ├── requirements.txt ├── setup.cfg └── setup.py /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # Friggin Apple 104 | .DS_Store 105 | 106 | # PyCharm 107 | .idea/ 108 | 109 | # VSCode 110 | .vscode 111 | *.code-workspace -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ben Weinstein-Raun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ankdown 2 | 3 | A simple way to write Anki decks in Markdown. 4 | 5 | ## What This Is 6 | 7 | [Anki](https://apps.ankiweb.net) is awesome, in many ways. 8 | However, its card editor is... a little bit uncomfortable. 9 | I really wanted to write Anki cards in Markdown. So I made 10 | a tool to convert Markdown (+ standard MathJAX math notation) 11 | into Anki decks that can be easily imported. This way, it's 12 | possible to use any fancy markdown (and MathJAX) tools to build 13 | your decks. 14 | 15 | ## How to use it 16 | 17 | **NOTE** This program requires _Python 3_, along with the 18 | packages in requirements.txt 19 | 20 | ### Installing 21 | 22 | Ankdown can be installed by doing `pip3 install --user ankdown`. 23 | 24 | ### Writing Cards 25 | 26 | Cards are written in the following format: 27 | 28 | ```markdown 29 | Expected Value of \(f(x)\) 30 | 31 | % 32 | 33 | \[\mathbb{E}[f(x)] = \sum_x p(x)f(x)\] 34 | 35 | % 36 | 37 | math, probability 38 | 39 | --- 40 | 41 | Variance of \(f(x)\) 42 | 43 | % 44 | 45 | \[\text{Var}(f(x)) = \mathbb{E}[(f(x) - \mathbb{E}[f(x)])^2]\] 46 | 47 | ``` 48 | 49 | Each of the solitary `%` signs is a field separator: the first 50 | field is the front of the card, the second field is 51 | the back of the card, and subsequent fields can contain whatever 52 | you want them to (all fields after the second are optional). 53 | 54 | `---` markers represent a card boundary. 55 | 56 | The tool needs these separators to be alone on their own lines, 57 | and most markdown editors will work better if you separate them from 58 | other text with empty lines, so that they're treated as their own 59 | paragraphs by the editor. 60 | 61 | ### Running Ankdown 62 | 63 | #### Method A: manually 64 | 65 | To compile your cards, put them in markdown files with `.md` extensions, 66 | inside of a directory that has the name of the deck you'd like to put 67 | the cards into. Then, run `ankdown -r [directory] -p [package filename]`. 68 | The package filename should end in `.apkg`. 69 | 70 | You can then import the package using the Anki import tool. To do this, 71 | go to `File > Import`, and then in the drop-down menu select "Packaged 72 | Anki Deck/Collection (*.apkg)". 73 | 74 | #### Method B: via the add-on 75 | 76 | Once you've installed ankdown, it can be a hassle to run it on all 77 | of your decks over and over again. There is an [`ankdown` 78 | Anki add-on](https://ankiweb.net/shared/info/109255569) that you 79 | can use to make this process simpler: If you put all of your decks 80 | in one megadirectory (mine is in `~/Flashcards`), you can re-import 81 | your decks in one swell foop by going to `Tools > Reload Markdown 82 | Decks` (or using the operating-system-dependent keybinding). 83 | 84 | 85 | ## Gotchas 86 | 87 | Ankdown has an unusually large number of known issues; my preferred method 88 | of discussing them is via github ticket. 89 | 90 | ### Multiple Decks 91 | 92 | Ankdown uses Genanki as a backend, which doesn't (as of this writing) handle 93 | multiple decks in a single package very well. If you point ankdown at a 94 | directory with multiple decks in subdirectories, it will do its best, and 95 | your cards will all be added to the package, but they won't be assigned 96 | to the correct decks. The ankdown plugin solves this problem by running 97 | the executable on each deck individually, and then importing all the 98 | resulting packages. 99 | 100 | ### Intentional feature removals 101 | 102 | There used to be other ways to run ankdown, but they were slowly making 103 | the code worse and worse as I tried to keep them all operational. If there's 104 | a particular method of operating ankdown that you used and miss, let me know 105 | in a github issue. 106 | 107 | ### Math separators 108 | 109 | Unfortunately, `$` and `$$` as math separators were not chosen by the anki 110 | developers for the desktop client's MathJax display, and so in order for math 111 | to work in both web and desktop, it became much simpler to use `\(\)` and 112 | `\[\]`. These separators should be configurable in most markdown editors 113 | (e.g. I use the VSCode Markdown+Math plugin). Older decks that were built 114 | for ankdown need to be modified to use the new separators. 115 | 116 | ### Media references 117 | 118 | Ankdown should work with media references that result in `src=""` appearing 119 | somewhere in the generated html (mainly images). If you need it to work with 120 | other media types (like sounds), let me know in a github issue and I may make 121 | time to fix this. 122 | 123 | ### Updating Cards 124 | 125 | When you want to modify a card, just run your deck through the above 126 | process after changing the markdown file. Anki should notice, and update 127 | the card. This is done by giving the cards in your deck unique IDs based on 128 | their filename and index in the file. 129 | 130 | This is the most robust solution I could come up with, but it has some downsides: 131 | 132 | 1. It's not possible to automatically remove cards from your anki decks, since 133 | the anki package importer never deletes cards. 134 | 2. If you delete a card from a markdown file, ankdown will give all of its 135 | successors off-by-one ID numbers, and so if they were different in important 136 | ways (like how much you needed to study them), anki will get confused. 137 | The best way to deal with this is to give each card its own markdown file. 138 | 139 | ### General code quality 140 | 141 | Lastly, the catch-all disclaimer: this is, as they say, alpha-quality software. 142 | I wrote this program (and the add-on) to work for me; it's pretty likely that 143 | you'll hit bugs in proportion to how different your desires are from mine. That 144 | said, I want it to be useful for other people as well; please submit github 145 | tickets if you do run into problems! 146 | 147 | -------------------------------------------------------------------------------- /ankdown/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benwr/ankdown/92d547546e390f6df7ebe2242bbf67b1e025b537/ankdown/__init__.py -------------------------------------------------------------------------------- /ankdown/ankdown.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Ankdown: Convert Markdown files into anki decks. 3 | 4 | This is a hacky script that I wrote because I wanted to use 5 | aesthetically pleasing editing tools to make anki cards, instead of 6 | the (somewhat annoying, imo) card editor in the anki desktop app. 7 | 8 | The math support is via MathJax, which is more full-featured (and 9 | much prettier) than Anki's builtin LaTeX support. 10 | 11 | The markdown inputs should look like this: 12 | 13 | ``` 14 | First Card Front  15 | 16 | % 17 | 18 | First Card Back: \\(\\text{TeX inline math}\\) 19 | 20 | % 21 | 22 | first, card, tags 23 | 24 | --- 25 | 26 | Second Card Front: 27 | 28 | \\[\\text{TeX Math environment}\\] 29 | 30 | % 31 | 32 | Second Card Back (note that tags are optional) 33 | ``` 34 | 35 | Ankdown can be configured via yaml. A possible configuration file might look like this: 36 | 37 | ```yaml 38 | recur_dir: ~/ankdown_cards 39 | pkg_arg: ~/ankdown_cards.apkg 40 | card_model_name: CustomModelName 41 | card_model_css: ".card {font-family: 'Crimson Pro', 'Crimson Text', 'Cardo', 'Times', 'serif'; text-align: left; color: black; background-color: white;}" 42 | dollar: True 43 | ``` 44 | 45 | A configuration can also be passed as a string: `"{dollar: True, card_model_name: CustomModelName, card_model_css: \".card {text-align: left;}\"}"` 46 | 47 | Usage: 48 | ankdown.py [-r DIR] [-p PACKAGENAME] [--highlight] [--config CONFIG_STRING] [--configFile CONFIG_FILE_PATH] 49 | 50 | Options: 51 | -h --help Show this help message 52 | --version Show version 53 | 54 | -r DIR Recursively visit DIR, accumulating cards from `.md` files. 55 | 56 | -p PACKAGE Instead of a .txt file, produce a .apkg file. recommended. 57 | 58 | --highlight Enable syntax highlighting for code 59 | 60 | --config CONFIG_STRING ankdown configuration as YAML string 61 | 62 | --configFile CONFIG_FILE_PATH path to ankdown configuration as YAML file 63 | """ 64 | 65 | 66 | import hashlib 67 | import os 68 | import re 69 | import tempfile 70 | import textwrap 71 | 72 | from shutil import copyfile 73 | 74 | import misaka 75 | import genanki 76 | import yaml 77 | 78 | from docopt import docopt 79 | 80 | import houdini as h 81 | from pygments import highlight 82 | from pygments.formatters import HtmlFormatter, ClassNotFound 83 | from pygments.lexers import get_lexer_by_name 84 | 85 | 86 | class HighlighterRenderer(misaka.HtmlRenderer): 87 | def blockcode(self, text, lang): 88 | try: 89 | lexer = get_lexer_by_name(lang, stripall=True) 90 | except ClassNotFound: 91 | lexer = None 92 | 93 | if lexer: 94 | formatter = HtmlFormatter() 95 | return highlight(text, lexer, formatter) 96 | # default 97 | return '\n
{}
\n'.format(
98 | h.escape_html(text.strip()))
99 |
100 |
101 | renderer = HighlighterRenderer()
102 | highlight_markdown = misaka.Markdown(renderer, extensions=("fenced-code", "math"))
103 |
104 |
105 | VERSION = "0.7.1"
106 |
107 | # Anki 2.1 has mathjax built in, but ankidroid and other clients don't.
108 | CARD_MATHJAX_CONTENT = textwrap.dedent("""\
109 |
120 |
133 | """)
134 |
135 | CONFIG = {
136 | 'pkg_arg': 'AnkdownPkg.apkg',
137 | 'recur_dir': '.',
138 | 'dollar': False,
139 | 'highlight': False,
140 | 'card_model_name': 'Ankdown Model 2',
141 | 'card_model_css': """
142 | .card {
143 | font-family: 'Crimson Pro', 'Crimson Text', 'Cardo', 'Times', 'serif';
144 | text-align: center;
145 | color: black;
146 | background-color: white;
147 | }
148 | """,
149 | 'card_model_fields': [
150 | {"name": "Question"},
151 | {"name": "Answer"},
152 | {"name": "Tags"},
153 | ],
154 | 'card_model_templates': [
155 | {
156 | "name": "Ankdown Card",
157 | "qfmt": "{{{{Question}}}}\n{0}".format(CARD_MATHJAX_CONTENT),
158 | "afmt": "{{{{Question}}}}