├── ankdown ├── __init__.py ├── highlight.css └── ankdown.py ├── setup.cfg ├── requirements.txt ├── LICENSE ├── setup.py ├── .gitignore └── README.md /ankdown/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cached-property==1.3.0 2 | cffi==1.10.0 3 | docopt==0.6.2 4 | frozendict==1.2 5 | genanki==0.6.3 6 | misaka==2.1.0 7 | pycparser==2.18 8 | pystache==0.5.4 9 | PyYAML==3.13 10 | Pygments==2.4.0 11 | houdini.py==0.1.0 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | 3 | from codecs import open as codec_open 4 | from setuptools import setup, find_packages 5 | 6 | HERE = path.abspath(path.dirname(__file__)) 7 | 8 | with codec_open(path.join(HERE, 'README.md'), encoding='utf-8') as f: 9 | LONG_DESCRIPTION = f.read() 10 | 11 | setup( 12 | name="ankdown", 13 | version="0.7.1", 14 | description="A utility for converting Markdown into Anki cards", 15 | long_description=LONG_DESCRIPTION, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/benwr/ankdown", 18 | 19 | author="Ben Weinstein-Raun", 20 | author_email="b@w-r.me", 21 | license="MIT", 22 | classifiers=[ 23 | "Development Status :: 3 - Alpha", 24 | 25 | "Intended Audience :: Developers", 26 | "Topic :: Education :: Computer Aided Instruction (CAI)", 27 | "License :: OSI Approved :: MIT License", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.0", 30 | "Programming Language :: Python :: 3.1", 31 | "Programming Language :: Python :: 3.2", 32 | "Programming Language :: Python :: 3.3", 33 | "Programming Language :: Python :: 3.4", 34 | "Programming Language :: Python :: 3.5", 35 | "Programming Language :: Python :: 3.6", 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3 :: Only", 38 | ], 39 | keywords="anki spaced-repetition markdown math latex", 40 | packages=find_packages(), 41 | package_data={'':['*.css']}, 42 | install_requires=['genanki>=0.6.3', 'misaka>=2.1.0', 'docopt>=0.6.2', 'houdini.py>=0.1.0', 'Pygments>=2.4.0'], 43 | entry_points={ 44 | "console_scripts": [ 45 | "ankdown=ankdown.ankdown:main" 46 | ] 47 | } 48 | ) 49 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /ankdown/highlight.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #f8f8f8; text-align: left } 3 | .highlight .c { color: #408080; font-style: italic } /* Comment */ 4 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 5 | .highlight .k { color: #008000; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #666666 } /* Operator */ 7 | .highlight .ch { color: #408080; font-style: italic } /* Comment.Hashbang */ 8 | .highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 9 | .highlight .cp { color: #BC7A00 } /* Comment.Preproc */ 10 | .highlight .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */ 11 | .highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ 12 | .highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ 13 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 14 | .highlight .ge { font-style: italic } /* Generic.Emph */ 15 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 16 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 17 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 18 | .highlight .go { color: #888888 } /* Generic.Output */ 19 | .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 20 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 21 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 22 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 23 | .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 24 | .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 25 | .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 26 | .highlight .kp { color: #008000 } /* Keyword.Pseudo */ 27 | .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 28 | .highlight .kt { color: #B00040 } /* Keyword.Type */ 29 | .highlight .m { color: #666666 } /* Literal.Number */ 30 | .highlight .s { color: #BA2121 } /* Literal.String */ 31 | .highlight .na { color: #7D9029 } /* Name.Attribute */ 32 | .highlight .nb { color: #008000 } /* Name.Builtin */ 33 | .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 34 | .highlight .no { color: #880000 } /* Name.Constant */ 35 | .highlight .nd { color: #AA22FF } /* Name.Decorator */ 36 | .highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ 37 | .highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 38 | .highlight .nf { color: #0000FF } /* Name.Function */ 39 | .highlight .nl { color: #A0A000 } /* Name.Label */ 40 | .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 41 | .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ 42 | .highlight .nv { color: #19177C } /* Name.Variable */ 43 | .highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 44 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 45 | .highlight .mb { color: #666666 } /* Literal.Number.Bin */ 46 | .highlight .mf { color: #666666 } /* Literal.Number.Float */ 47 | .highlight .mh { color: #666666 } /* Literal.Number.Hex */ 48 | .highlight .mi { color: #666666 } /* Literal.Number.Integer */ 49 | .highlight .mo { color: #666666 } /* Literal.Number.Oct */ 50 | .highlight .sa { color: #BA2121 } /* Literal.String.Affix */ 51 | .highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ 52 | .highlight .sc { color: #BA2121 } /* Literal.String.Char */ 53 | .highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ 54 | .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 55 | .highlight .s2 { color: #BA2121 } /* Literal.String.Double */ 56 | .highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 57 | .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ 58 | .highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 59 | .highlight .sx { color: #008000 } /* Literal.String.Other */ 60 | .highlight .sr { color: #BB6688 } /* Literal.String.Regex */ 61 | .highlight .s1 { color: #BA2121 } /* Literal.String.Single */ 62 | .highlight .ss { color: #19177C } /* Literal.String.Symbol */ 63 | .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ 64 | .highlight .fm { color: #0000FF } /* Name.Function.Magic */ 65 | .highlight .vc { color: #19177C } /* Name.Variable.Class */ 66 | .highlight .vg { color: #19177C } /* Name.Variable.Global */ 67 | .highlight .vi { color: #19177C } /* Name.Variable.Instance */ 68 | .highlight .vm { color: #19177C } /* Name.Variable.Magic */ 69 | .highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ 70 | -------------------------------------------------------------------------------- /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/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}}}}