├── .DS_Store
├── Archive.zip
├── LICENSE
├── Obsidianki 4.0.apkg
├── Obsidianki 4.ankiaddon
├── README.md
├── miscellaneous
└── ankiweb.html
└── src
├── README.md
├── __init__.py
├── anki_importer.py
├── files.py
├── manifest.json
├── markdown2
├── markdown2.py
└── markdown2Mathjax.py
├── obsidian_url.py
├── processor.py
└── settings.py
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxxedu/obsidianki4/90305bbf74aa6b625b6b374e87964bf5d5d62a13/.DS_Store
--------------------------------------------------------------------------------
/Archive.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxxedu/obsidianki4/90305bbf74aa6b625b6b374e87964bf5d5d62a13/Archive.zip
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 wxxedu
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 |
--------------------------------------------------------------------------------
/Obsidianki 4.0.apkg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxxedu/obsidianki4/90305bbf74aa6b625b6b374e87964bf5d5d62a13/Obsidianki 4.0.apkg
--------------------------------------------------------------------------------
/Obsidianki 4.ankiaddon:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wxxedu/obsidianki4/90305bbf74aa6b625b6b374e87964bf5d5d62a13/Obsidianki 4.ankiaddon
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # obsidianki 4
2 |
3 | NOTE: The project is now **PAUSED**, and I will not be actively mainting this project until June. I am busy with my exam-preparations for now. Sorry about this issue :-(
4 |
5 |
6 |
7 | > **Please back-up your vault regularly while using this add-on!**
8 | >
9 | > I am a noob in programming, while no occasion of losing notes has happened, I am afraid that it might. As your notes are valuable, please do remember to back up notes.
10 | >
11 | > **Versions**
12 | >
13 | > Theoretically, it now supports Anki 2.1.26 +. I am unaware whether if supports earlier version, and I wasn't able to test it on Anki 2.1.28 as my laptop is a M1 MacBook Air and Anki 2.1.28 does not open on it.
14 | >
15 | > **Expectations**
16 | >
17 | > With this add-on, it is expected that you make all your changes in Obsidian (including the deletion, addition, and moving of the files). If you want to edit a file, you can just click on the link in Anki to link back to Obsidian. Instead of deleting files, you should move unused files into the `.trash` folder that you can turn on in the settings of Obsidian. Obsidianki will automatically remove them for you.
18 |
19 | This is a [Anki](https://github.com/ankitects) add-on that would import your files from [Obsidian](https://obsidian.md) into Anki while preserving the wiki-links. Each file in Obsidianki will be converted to a single note in Anki. It does so by searching through your vault for the file with the name specified and generating an Obsidian url from the path.
20 |
21 | Its github page is [obsidianki4](https://github.com/wxxedu/obsidianki4).
22 |
23 | This add-on also works with [hierarchical tags](https://ankiweb.net/shared/info/594329229) to convert the hierarchical tags in Obsidian in the metadata section (`tags: [tag1/tag1.1/tag1.1.1, tag2/tag2.1/tag2.1.1]`) into anki hierarchical tags. `tag1::tag1.1::tag1.1.1` and `tag2::tag2.1::tag2.1.1`
24 |
25 | ## How to Install
26 |
27 | You can install this Add-on by downloading the `obsidianki 4.ankiaddon` file from the releases section of GitHub and double click on it.
28 |
29 | You can also download from AnkiWeb: [Obsidianki 4 Addon Page](https://ankiweb.net/shared/info/620260832). The code for this add-on is 620260832.
30 |
31 | ## How to Use
32 |
33 | **Before starting to use, you will have to install Obsidianki's template, without which Obsidianki would not work.** To do so, go to Anki's Add-ons folder, open the folder "Obsidianki 4", and find `Obsidianki 4.apkg`. Double click on it to install. You can also download it from GitHub.
34 |
35 | After you've installed the Add-on, you can open Anki, select `Tools` -> `Obsidianki 4`, as shown in the following picture.
36 |
37 | 
38 |
39 | The following menu will pop-up, which will include the default preferences panel. **NOTE THAT THE SETTINGS IN THIS PANEL ARE ALL DEFAULT SETTINGS**, and you **SHOULD NOT** change them regularly, as a change will **AFFECT ALL YOUR NOTES**.
40 |
41 | 
42 |
43 | Copy the path of your Obsidian vault into the first field. Note that you will have to use **forward slashes** `/` instead of backward ones for Obsidianki to function properly.
44 |
45 | After you've set the settings (I will explain in the next section), you can click on "Save and Convert", and it will complete the conversion. However, you won't notice a difference. Why? Because Anki's interface is not refreshed. To refresh the interface, you could click on anything in Anki's main interface, and it should be refreshed.
46 |
47 | ## Default Settings
48 |
49 | Now, let's take a look at the default settings.
50 |
51 | ### Vault Path
52 |
53 | This place shows the path to your vault. Note that in order for the wiki-links in Anki to link back to Obsidian, you will have to use a path that is actually a vault. If you just copy the path of a folder in the vault, the link function will not work.
54 |
55 | Another thing to take especial note of is that you should use **forward slashes** instead of backward ones.
56 |
57 | ### Templates Folder Name
58 |
59 | The name of the folder in the first level that holds your templates. If specified, the contents in this folder would not be imported to Anki.
60 |
61 | ### Trash Folder Name
62 |
63 | The name of the folder in the first level that holds your trash. If specified, the contents in this folder would be **erased** when you run the Obsidianki add-on, and the corresponding cards in anki would also be deleted.
64 |
65 | ### Archive Folder Name
66 |
67 | The name of the folder in the first level that holds your archived file. If specified, corresponding anki cards to the contents in this folder would be deleted in Anki, but the files are still there in Obsidian and would not be deleted.
68 |
69 | ### Mode
70 |
71 | There are four importing cloze modes in Obsidianki.
72 |
73 | #### `word` mode
74 |
75 | It generates a card for every cloze. If you have 10 clozes, it generates 10 cards from `{{c1::Card 1}}` to `{{c10::}}`.
76 |
77 | #### `line` mode
78 |
79 | It generates a card for every line. If you have 10 clozes in the first line, they will be `{{c1::Card 1}}` to `{{c10::Card 10}}`. If you have 2 more clozes in the second line, they will be `{{c2::Card 11}}` to `{{c2::Card 12}}`.
80 |
81 | #### `heading` mode (Recommended)
82 |
83 | It generates a card for the content under every heading, with the exception of list cards and QA cards (I will explain this below). If you have a file as below:
84 |
85 | ```markdown
86 | # Heading 1
87 |
88 | Hello **Obsidianki**.
89 |
90 | This is the best **Anki** add-on for importing Obsidian files into **Anki**.
91 |
92 | ## Heading 2
93 |
94 | This is something **interesting**.
95 |
96 | Q: What is the best add-on for importing Obsidian files into **Anki**?
97 |
98 | A: Obsidianki!
99 |
100 | What are the features of Obsidianki?
101 |
102 | 1. Import files
103 | 2. Preserve wiki links
104 | 3. Convert to Clozes
105 |
106 | ## Heading 3
107 |
108 | This is **Heading 3**.
109 |
110 | ```
111 |
112 | The "Obsidianki", "Anki" under "Heading 1" will be turned into `{{c1::Obsidianki}}` and `{{c1::Anki}}` respectively.
113 |
114 | Theoretically, everything under "Heading 2" should be turned into `{{c2::...}}` cards, right? Not quite, because I have added QA cards and list cards. So, after conversion, the portion under heading to would become:
115 |
116 | ```markdown
117 | ## Heading 2
118 |
119 | This is something **{{c2::interesting}}**.
120 |
121 | Q: What is the best add-on for importing Obsidian files into **Anki**?
122 |
123 | A: {{c3::Obsidianki!}}
124 |
125 | What are the features of Obsidianki?
126 |
127 | 1. {{c4::Import files}}
128 | 2. {{c5::Preserve wiki links}}
129 | 3. {{c6::Convert to Clozes}}
130 | ```
131 |
132 | #### `document` mode
133 |
134 | In the document mode, everything will be converted to `{{c1::...}}`.
135 |
136 | ### Type
137 |
138 | There are two types in Obsidianki 4: `cloze` and `basic`. Nevertheless, these two types are different from Anki's `cloze` and `basic`.
139 |
140 | #### `cloze`
141 |
142 | This type will create visible deletions on the screen. You will be able to see `[...]` on the screen where you applied cloze.
143 |
144 | #### `basic`
145 |
146 | This type will only create one card, and the cloze deletion would not be visible.
147 |
148 | ### Conversions
149 |
150 | #### Bold to Cloze:
151 |
152 | This converts the bold syntax `**bold**` to cloze in Anki, while preserving the format.
153 |
154 | #### Italics to Cloze:
155 |
156 | This converts the italics syntax `*italics*` to cloze in Anki, while preserving the format.
157 |
158 | #### Highlight to Cloze:
159 |
160 | This converts the highlight syntax `==highlight==` to cloze in Anki, while preserving the format.
161 |
162 | #### Image to Cloze:
163 |
164 | This converts the image syntax `![]()` to cloze in Anki, while preserving the image.
165 |
166 | #### Quote to Cloze:
167 |
168 | This converts the quote syntax `> this is a quote` to cloze in Anki, while preserving format.
169 |
170 | **Be aware that currently, this has conflicts with the other syntaxes. If you want to leave this option on, you will have to make sure that you apply no other cloze formatting in the quote.**
171 |
172 | #### QA to Cloze:
173 |
174 | This converts the QA syntax that I created into cloze in Anki.
175 |
176 | ```markdown
177 | Q: Question
178 |
179 | A: Answer
180 | ```
181 |
182 | ```markdown
183 | Q: Question
184 |
185 | A: {{c1::Answer}}
186 | ```
187 |
188 | #### List to Cloze:
189 |
190 | This turns any list into `Cloze`, where each list item is a cloze.
191 |
192 | #### Inline Code to Cloze:
193 |
194 | This converts the inline code syntax to cloze in Anki, while preserving format.
195 |
196 | #### Block Code to Cloze:
197 |
198 | This converts the block code syntax to cloze in Anki, while preserving format.
199 |
200 | ## Individual Settings
201 |
202 | You can also individually specify the settings for each note (file) in the metadata section of your file. The metadata section is the following segment in the very beginning of a document.
203 |
204 | ```markdown
205 | ---
206 | uid: 4511487055494033182
207 | ---
208 | ```
209 |
210 | **By the way, Obsidianki will automatically create a metadata section that contains the file's unique id in the file. If you don't want duplicated notes, do not change the uid.**
211 |
212 | If you want to change the individual importing settings for each file, type it in in the metadata section. You can make this a template in Obsidian:
213 |
214 | ```
215 | ---
216 | mode: heading
217 | type: cloze
218 | bold: True
219 | italics: True
220 | highlight: False
221 | image: True
222 | quote: False
223 | QA: True
224 | list: True
225 | inline code: True
226 | block code: False
227 | ---
228 | ```
229 |
230 | ## Special Note
231 |
232 | ### About the Development
233 |
234 | I will try my best to develop and maintain this add-on. However, as of right now, I am just a high school student who barely knows any programming. All my knowledge of programming come from my AP Computer Science A class LOL.
235 |
236 | I know that my code is pretty bad, so feel free to help me update them. (please do so so that I can learn from you!) I will probably add more comments to my code explaining my thoughts while writing them in the future, just in case you want to know what I did in the code. (I want to do this because I struggled to understand Anki's source code and other Add-ons). While this will not be an add-on writing tutorial and I am by no means good at python, it is my best hopes that sharing my thoughts as a beginner will help other beginners understand better how to write Anki add-ons. This will take some time for me to do, as I need to get back to work and studying, but I am going to spend some time doing so.
237 |
238 | ### Thanks
239 |
240 | I want to thank the creators of Anki and Obsidian for building such beautiful apps. I also want to thank my friend [Anis](https://github.com/qiaozhanrong) for helping me with the code.
241 |
--------------------------------------------------------------------------------
/miscellaneous/ankiweb.html:
--------------------------------------------------------------------------------
1 |
2 | Note: please back up your obsidian vault regularly while using this add-on. As it will write certain information to your vault, I am concerned that it might have the very slight chance of erasing your files.
3 |
4 | This is a Anki add-on that would import your files from Obsidian into Anki while preserving the wiki-links. Each file in Obsidianki will be converted to a single note in Anki. It does so by searching through your vault for the file with the name specified and generating an Obsidian url from the path. Note that this only works with Anki 2.1.38+
5 |
6 | https://github.com/wxxedu/obsidianki4
7 |
8 | This add-on also works with hierarchical tags to convert the hierarchical tags in Obsidian in the metadata section (tags: [tag1/tag1.1/tag1.1.1, tag2/tag2.1/tag2.1.1]
) into anki hierarchical tags. tag1::tag1.1::tag1.1.1
and tag2::tag2.1::tag2.1.1
.
9 |
10 | ## How to Install
11 |
12 | You can install this Add-on by downloading the obsidianki 4.ankiaddon
file from the releases section of GitHub and double click on it.
13 |
14 | You can also use the install code below.
15 |
16 | ## How to Use
17 |
18 | Before starting to use, you will have to install Obsidianki's template, without which Obsidianki would not work. You can also download it from Obsidianki's Github Page.
19 |
20 | After you've installed the Add-on, you can open Anki, select Tools
-> Obsidianki 4
, as shown in the following picture.
21 |
22 |
23 |
24 | The following menu will pop-up, which will include the default preferences panel. NOTE THAT THE SETTINGS IN THIS PANEL ARE ALL DEFAULT SETTINGS, and you SHOULD NOT change them regularly, as a change will AFFECT ALL YOUR NOTES.
25 |
26 |
27 |
28 | Copy the path of your Obsidian vault into the first field. Note that you will have to use forward slashes
/
instead of backward ones for Obsidianki to function properly.
29 |
30 | After you've set the settings (I will explain in the next section), you can click on "Save and Convert", and it will complete the conversion. However, you won't notice a difference. Why? Because Anki's interface is not refreshed. To refresh the interface, you could click on anything in Anki's main interface, and it should be refreshed.
31 |
32 | ## Default Settings
33 |
34 | Now, let's take a look at the default settings.
35 |
36 | ### Vault Path
37 |
38 | This place shows the path to your vault. Note that in order for the wiki-links in Anki to link back to Obsidian, you will have to use a path that is actually a vault. If you just copy the path of a folder in the vault, the link function will not work.
39 |
40 | Another thing to take especial note of is that you should use forward slashes instead of backward ones.
41 |
42 | ### Mode
43 |
44 | There are four importing cloze modes in Obsidianki.
45 |
46 | #### word
mode
47 |
48 | It generates a card for every cloze. If you have 10 clozes, it generates 10 cards from {{c1::Card 1}}
to {{c10::}}
.
49 |
50 | #### line
mode
51 |
52 | It generates a card for every line. If you have 10 clozes in the first line, they will be {{c1::Card 1}}
to {{c10::Card 10}}
. If you have 2 more clozes in the second line, they will be {{c2::Card 11}}
to {{c2::Card 12}}
.
53 |
54 | #### heading
mode (Recommended)
55 |
56 | It generates a card for the content under every heading, with the exception of list cards and QA cards (I will explain this below). If you have a file as below:
57 |
58 |
59 |
60 | The "Obsidianki", "Anki" under "Heading 1" will be turned into
{{c1::Obsidianki}}
and {{c1::Anki}}
respectively.
61 |
62 | Theoretically, everything under "Heading 2" should be turned into {{c2::...}}
cards, right? Not quite, because I have added QA cards and list cards. So, after conversion, the portion under heading to would become:
63 |
64 |
65 |
66 | ####
document
mode
67 |
68 | In the document mode, everything will be converted to {{c1::...}}
.
69 |
70 | ### Type
71 |
72 | There are two types in Obsidianki 4: cloze
and basic
. Nevertheless, these two types are different from Anki's `cloze` and basic
.
73 |
74 | #### cloze
75 |
76 | This type will create visible deletions on the screen. You will be able to see [...]
on the screen where you applied cloze.
77 |
78 | #### basic
79 |
80 | This type will only create one card, and the cloze deletion would not be visible.
81 |
82 | ### Conversions
83 |
84 | #### Bold to Cloze:
85 |
86 | This converts the bold syntax **bold**
to cloze in Anki, while preserving the format.
87 |
88 | #### Italics to Cloze:
89 |
90 | This converts the italics syntax *italics*
to cloze in Anki, while preserving the format.
91 |
92 | #### Highlight to Cloze:
93 |
94 | This converts the highlight syntax ==highlight==
to cloze in Anki, while preserving the format.
95 |
96 | #### Image to Cloze:
97 |
98 | This converts the image syntax ![]()
to cloze in Anki, while preserving the image.
99 |
100 | #### Quote to Cloze:
101 |
102 | This converts the quote syntax > this is a quote
to cloze in Anki, while preserving format.
103 |
104 | Be aware that currently, this has conflicts with the other syntaxes. If you want to leave this option on, you will have to make sure that you apply no other cloze formatting in the quote.
105 |
106 | #### QA to Cloze:
107 |
108 | This converts the QA syntax that I created into cloze in Anki.
109 |
110 |
111 |
112 | #### List to Cloze:
113 |
114 | This turns any list into cloze, where each list item is a cloze.
115 |
116 | #### Inline Code to Cloze:
117 |
118 | This converts the inline code syntax to cloze in Anki, while preserving format.
119 |
120 | #### Block Code to Cloze:
121 |
122 | This converts the block code syntax to cloze in Anki, while preserving format.
123 |
124 | ## Individual Settings
125 |
126 | You can also individually specify the settings for each note (file) in the metadata section of your file. The metadata section is the following segment in the very beginning of a document.
127 |
128 |
129 |
130 | By the way, Obsidianki will automatically create a metadata section that contains the file's unique id in the file. If you don't want duplicated notes, do not change the uid.
131 |
132 | If you want to change the individual importing settings for each file, type it in in the metadata section. You can make this a template in Obsidian:
133 |
134 |
135 |
136 | ## Special Note
137 |
138 | ### About the Development
139 |
140 | I will try my best to develop and maintain this add-on. However, as of right now, I am just a high school student who barely knows any programming. All my knowledge of programming come from my AP Computer Science A class LOL.
141 |
142 | I know that my code is pretty bad, so feel free to help me update them. (please do so so that I can learn from you!)
143 |
144 | ### Thanks
145 |
146 | I want to thank the creators of Anki and Obsidian for building such beautiful apps. I also want to thank my friend Anis for helping me with the code.
--------------------------------------------------------------------------------
/src/README.md:
--------------------------------------------------------------------------------
1 | # obsidianki 4
2 |
3 | > **Please back-up your vault regularly while using this add-on!**
4 | >
5 | > Theoretically, it now supports Anki 2.1.28 +. I am unware whether if supports earlier version, and I wasn't able to test it on Anki 2.1.28 as my laptop is a M1 MacBook Air and Anki 2.1.28 does not open on it.
6 | >
7 | > This add-on also works with [hierarchical tags](https://ankiweb.net/shared/info/594329229) to convert the hierarchical tags in Obsidian in the metadata section (`tags: [tag1/tag1.1/tag1.1.1, tag2/tag2.1/tag2.1.1]`) into anki hierarchical tags. `tag1::tag1.1::tag1.1.1` and `tag2::tag2.1::tag2.1.1`
8 |
9 | This is a [Anki](https://github.com/ankitects) add-on that would import your files from [Obsidian](https://obsidian.md) into Anki while preserving the wiki-links. Each file in Obsidianki will be converted to a single note in Anki. It does so by searching through your vault for the file with the name specified and generating an Obsidian url from the path.
10 |
11 | Its github page is [obsidianki4](https://github.com/wxxedu/obsidianki4).
12 |
13 | ## How to Install
14 |
15 | You can install this Add-on by downloading the `obsidianki 4.ankiaddon` file from the releases section of GitHub and double click on it.
16 |
17 | You can also download from AnkiWeb: [Obsidianki 4 Addon Page](https://ankiweb.net/shared/info/620260832). The code for this add-on is 620260832.
18 |
19 | ## How to Use
20 |
21 | **Before starting to use, you will have to install Obsidianki's template, without which Obsidianki would not work.** To do so, go to Anki's Add-ons folder, open the folder "Obsidianki 4", and find `Obsidianki 4.apkg`. Double click on it to install. You can also download it from GitHub.
22 |
23 | After you've installed the Add-on, you can open Anki, select `Tools` -> `Obsidianki 4`, as shown in the following picture.
24 |
25 | 
26 |
27 | The following menu will pop-up, which will include the default preferences panel. **NOTE THAT THE SETTINGS IN THIS PANEL ARE ALL DEFAULT SETTINGS**, and you **SHOULD NOT** change them regularly, as a change will **AFFECT ALL YOUR NOTES**.
28 |
29 | 
30 |
31 | Copy the path of your Obsidian vault into the first field. Note that you will have to use **forward slashes** `/` instead of backward ones for Obsidianki to function properly.
32 |
33 | After you've set the settings (I will explain in the next section), you can click on "Save and Convert", and it will complete the conversion. However, you won't notice a difference. Why? Because Anki's interface is not refreshed. To refresh the interface, you could click on anything in Anki's main interface, and it should be refreshed.
34 |
35 | ## Default Settings
36 |
37 | Now, let's take a look at the default settings.
38 |
39 | ### Vault Path
40 |
41 | This place shows the path to your vault. Note that in order for the wiki-links in Anki to link back to Obsidian, you will have to use a path that is actually a vault. If you just copy the path of a folder in the vault, the link function will not work.
42 |
43 | Another thing to take especial note of is that you should use **forward slashes** instead of backward ones.
44 |
45 | ### Templates Folder Name
46 |
47 | The name of the folder in the first level that holds your templates. If specified, the contents in this folder would not be imported to Anki.
48 |
49 | ### Trash Folder Name
50 |
51 | The name of the folder in the first level that holds your trash. If specified, the contents in this folder would be **erased** when you run the Obsidianki add-on, and the corresponding cards in anki would also be deleted.
52 |
53 | ### Archive Folder Name
54 |
55 | The name of the folder in the first level that holds your archived file. If specified, corresponding anki cards to the contents in this folder would be deleted in Anki, but the files are still there in Obsidian and would not be deleted.
56 |
57 | ### Mode
58 |
59 | There are four importing cloze modes in Obsidianki.
60 |
61 | #### `word` mode
62 |
63 | It generates a card for every cloze. If you have 10 clozes, it generates 10 cards from `{{c1::Card 1}}` to `{{c10::}}`.
64 |
65 | #### `line` mode
66 |
67 | It generates a card for every line. If you have 10 clozes in the first line, they will be `{{c1::Card 1}}` to `{{c10::Card 10}}`. If you have 2 more clozes in the second line, they will be `{{c2::Card 11}}` to `{{c2::Card 12}}`.
68 |
69 | #### `heading` mode (Recommended)
70 |
71 | It generates a card for the content under every heading, with the exception of list cards and QA cards (I will explain this below). If you have a file as below:
72 |
73 | ```markdown
74 | # Heading 1
75 |
76 | Hello **Obsidianki**.
77 |
78 | This is the best **Anki** add-on for importing Obsidian files into **Anki**.
79 |
80 | ## Heading 2
81 |
82 | This is something **interesting**.
83 |
84 | Q: What is the best add-on for importing Obsidian files into **Anki**?
85 |
86 | A: Obsidianki!
87 |
88 | What are the features of Obsidianki?
89 |
90 | 1. Import files
91 | 2. Preserve wiki links
92 | 3. Convert to Clozes
93 |
94 | ## Heading 3
95 |
96 | This is **Heading 3**.
97 |
98 | ```
99 |
100 | The "Obsidianki", "Anki" under "Heading 1" will be turned into `{{c1::Obsidianki}}` and `{{c1::Anki}}` respectively.
101 |
102 | Theoretically, everything under "Heading 2" should be turned into `{{c2::...}}` cards, right? Not quite, because I have added QA cards and list cards. So, after conversion, the portion under heading to would become:
103 |
104 | ```markdown
105 | ## Heading 2
106 |
107 | This is something **{{c2::interesting}}**.
108 |
109 | Q: What is the best add-on for importing Obsidian files into **Anki**?
110 |
111 | A: {{c3::Obsidianki!}}
112 |
113 | What are the features of Obsidianki?
114 |
115 | 1. {{c4::Import files}}
116 | 2. {{c5::Preserve wiki links}}
117 | 3. {{c6::Convert to Clozes}}
118 | ```
119 |
120 | #### `document` mode
121 |
122 | In the document mode, everything will be converted to `{{c1::...}}`.
123 |
124 | ### Type
125 |
126 | There are two types in Obsidianki 4: `cloze` and `basic`. Nevertheless, these two types are different from Anki's `cloze` and `basic`.
127 |
128 | #### `cloze`
129 |
130 | This type will create visible deletions on the screen. You will be able to see `[...]` on the screen where you applied cloze.
131 |
132 | #### `basic`
133 |
134 | This type will only create one card, and the cloze deletion would not be visible.
135 |
136 | ### Conversions
137 |
138 | #### Bold to Cloze:
139 |
140 | This converts the bold syntax `**bold**` to cloze in Anki, while preserving the format.
141 |
142 | #### Italics to Cloze:
143 |
144 | This converts the italics syntax `*italics*` to cloze in Anki, while preserving the format.
145 |
146 | #### Highlight to Cloze:
147 |
148 | This converts the highlight syntax `==highlight==` to cloze in Anki, while preserving the format.
149 |
150 | #### Image to Cloze:
151 |
152 | This converts the image syntax `![]()` to cloze in Anki, while preserving the image.
153 |
154 | #### Quote to Cloze:
155 |
156 | This converts the quote syntax `> this is a quote` to cloze in Anki, while preserving format.
157 |
158 | **Be aware that currently, this has conflicts with the other syntaxes. If you want to leave this option on, you will have to make sure that you apply no other cloze formatting in the quote.**
159 |
160 | #### QA to Cloze:
161 |
162 | This converts the QA syntax that I created into cloze in Anki.
163 |
164 | ```markdown
165 | Q: Question
166 |
167 | A: Answer
168 | ```
169 |
170 | ```markdown
171 | Q: Question
172 |
173 | A: {{c1::Answer}}
174 | ```
175 |
176 | #### List to Cloze:
177 |
178 | This turns any list into `Cloze`, where each list item is a cloze.
179 |
180 | #### Inline Code to Cloze:
181 |
182 | This converts the inline code syntax to cloze in Anki, while preserving format.
183 |
184 | #### Block Code to Cloze:
185 |
186 | This converts the block code syntax to cloze in Anki, while preserving format.
187 |
188 | ## Individual Settings
189 |
190 | You can also individually specify the settings for each note (file) in the metadata section of your file. The metadata section is the following segment in the very beginning of a document.
191 |
192 | ```markdown
193 | ---
194 | uid: 4511487055494033182
195 | ---
196 | ```
197 |
198 | **By the way, Obsidianki will automatically create a metadata section that contains the file's unique id in the file. If you don't want duplicated notes, do not change the uid.**
199 |
200 | If you want to change the individual importing settings for each file, type it in in the metadata section. You can make this a template in Obsidian:
201 |
202 | ```
203 | ---
204 | mode: heading
205 | type: cloze
206 | bold: True
207 | italics: True
208 | highlight: False
209 | image: True
210 | quote: False
211 | QA: True
212 | list: True
213 | inline code: True
214 | block code: False
215 | ---
216 | ```
217 |
218 | ## Special Note
219 |
220 | ### About the Development
221 |
222 | I will try my best to develop and maintain this add-on. However, as of right now, I am just a high school student who barely knows any programming. All my knowledge of programming come from my AP Computer Science A class LOL.
223 |
224 | I know that my code is pretty bad, so feel free to help me update them. (please do so so that I can learn from you!) I will probably add more comments to my code explaining my thoughts while writing them in the future, just in case you want to know what I did in the code. (I want to do this because I struggled to understand Anki's source code and other Add-ons). While this will not be an add-on writing tutorial and I am by no means good at python, it is my best hopes that sharing my thoughts as a beginner will help other beginners understand better how to write Anki add-ons. This will take some time for me to do, as I need to get back to work and studying, but I am going to spend some time doing so.
225 |
226 | ### Thanks
227 |
228 | I want to thank the creators of Anki and Obsidian for building such beautiful apps. I also want to thank my friend [Anis](https://github.com/qiaozhanrong) for helping me with the code.
229 |
230 |
231 |
232 |
233 |
234 |
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 | from . import files
4 | from . import settings
5 | from . import obsidian_url
6 | from . import anki_importer
7 | import aqt
8 | from aqt import mw
9 | from aqt import AnkiQt, gui_hooks
10 | from aqt.qt import *
11 | from aqt.utils import showInfo
12 | from aqt.utils import tooltip
13 | from PyQt5 import QtWidgets, QtCore
14 |
15 |
16 | def read_files(root_path, relative_path):
17 | files_catalog = []
18 | if relative_path == "":
19 | paths = os.listdir(root_path)
20 | else:
21 | paths = os.listdir(root_path + "/" + relative_path)
22 | for path in paths:
23 | foler_is_ignored = False
24 |
25 | ignore_folder_s = settings.get_settings_by_name("ignore folder")
26 |
27 | if ignore_folder_s == "":
28 | pass
29 | elif ignore_folder_s.find("\n") != -1:
30 | ignore_folders = ignore_folder_s.split("\n")
31 | else:
32 | ignore_folders = [ignore_folder_s]
33 |
34 | for ignore_folder in ignore_folders:
35 | ignore_folder = ignore_folder.lstrip(" ")
36 | ignore_folder = ignore_folder.rstrip(" ")
37 | ignore_folder = "/" + ignore_folder
38 | if relative_path.startswith(ignore_folder) and ignore_folder != "/":
39 | foler_is_ignored = True
40 |
41 | if path.find(".") != -1 and path.split(".")[-1] != "md" and path != ".trash":
42 | pass
43 | elif foler_is_ignored:
44 | pass
45 | elif path.endswith(".md"):
46 | new_path = relative_path + "/" + path
47 | new_file = files.File(root_path, new_path)
48 | files_catalog.append(new_file)
49 | else:
50 | try:
51 | new_path = relative_path + "/" + path
52 | files_catalog = files_catalog + read_files(root_path, new_path)
53 | except NotADirectoryError:
54 | pass
55 | return files_catalog
56 |
57 | def get_bool(status_text):
58 | return status_text == "True" or status_text == "true"
59 |
60 | def get_text(status_bool):
61 | if status_bool:
62 | return "True"
63 | else:
64 | return "False"
65 |
66 | class ObsidiankiSettings(QDialog):
67 | def __init__(self, mw):
68 | super().__init__(mw)
69 |
70 |
71 | layout = QFormLayout(self)
72 |
73 |
74 | self.vault_path = QPlainTextEdit(self)
75 | self.templates_folder = QPlainTextEdit(self)
76 | self.archive_folder = QPlainTextEdit(self)
77 |
78 |
79 | self.mode = QLineEdit(self)
80 | self.type = QLineEdit(self)
81 |
82 |
83 | self.bold = QCheckBox(self)
84 | self.highlight = QCheckBox(self)
85 | self.italics = QCheckBox(self)
86 | self.image = QCheckBox(self)
87 | self.quote = QCheckBox(self)
88 | self.QuestionOrAnswer = QCheckBox(self)
89 | self.list = QCheckBox(self)
90 | self.inline_code = QCheckBox(self)
91 | self.block_code = QCheckBox(self)
92 | self.convert_button = QPushButton("Save and Convert")
93 | self.save_button = QPushButton("Save and Close")
94 |
95 |
96 | layout.addRow(QLabel("Vault Path: "))
97 | layout.addRow(QLabel("(Please use forward slashes for your vault path)"))
98 | layout.addRow(self.vault_path)
99 |
100 | layout.addRow(QLabel("Ignore Folders: "))
101 | layout.addRow(QLabel("(Notes in Anki in this obsidian folder will be ignored)"))
102 | layout.addRow(self.templates_folder)
103 |
104 | layout.addRow(QLabel("Archive Folder Name: "))
105 | layout.addRow(self.archive_folder)
106 | layout.addRow(QLabel("Anki Cards in this folder will be deleted"))
107 |
108 |
109 | layout.addRow(QLabel("Mode: "), self.mode)
110 | layout.addRow(QLabel("Mode: choose from word/line/heading/document"))
111 | layout.addRow(QLabel("Type: "), self.type)
112 | layout.addRow(QLabel("Type: choose from cloze/basic"))
113 |
114 |
115 | layout.addRow(QLabel("Bold to Cloze: "), self.bold)
116 | layout.addRow(QLabel("Italics to Cloze: "), self.italics)
117 | layout.addRow(QLabel("Highlight to Cloze: "), self.highlight)
118 | layout.addRow(QLabel("Image to Cloze: "), self.image)
119 | layout.addRow(QLabel("Quote to Cloze: "), self.quote)
120 | layout.addRow(QLabel("QA to Cloze"), self.QuestionOrAnswer)
121 | layout.addRow(QLabel("List to Cloze"), self.list)
122 | layout.addRow(QLabel("Inline Code to Cloze"), self.inline_code)
123 | layout.addRow(QLabel("Block Code to Cloze"), self.block_code)
124 |
125 |
126 | layout.addRow(self.save_button, self.convert_button)
127 |
128 |
129 | self.vault_path.setPlainText(settings.get_settings_by_name("vault path"))
130 | self.templates_folder.setPlainText(settings.get_settings_by_name("ignore folder"))
131 | self.archive_folder.setPlainText(settings.get_settings_by_name("archive folder"))
132 |
133 |
134 | self.mode.setText(settings.get_settings_by_name("mode"))
135 | self.type.setText(settings.get_settings_by_name("type"))
136 |
137 |
138 | self.bold.setChecked(get_bool(settings.get_settings_by_name("bold")))
139 | self.italics.setChecked(get_bool(settings.get_settings_by_name("italics")))
140 | self.highlight.setChecked(get_bool(settings.get_settings_by_name("highlight")))
141 | self.image.setChecked(get_bool(settings.get_settings_by_name("image")))
142 | self.quote.setChecked(get_bool(settings.get_settings_by_name("quote")))
143 | self.QuestionOrAnswer.setChecked(get_bool(settings.get_settings_by_name("QA")))
144 | self.list.setChecked(get_bool(settings.get_settings_by_name("list")))
145 | self.inline_code.setChecked(get_bool(settings.get_settings_by_name("inline code")))
146 | self.block_code.setChecked(get_bool(settings.get_settings_by_name("block code")))
147 |
148 |
149 | self.convert_button.setDefault(True)
150 | self.convert_button.clicked.connect(self.onOk)
151 | self.save_button.clicked.connect(self.onSave)
152 | self.show()
153 |
154 | def onOk(self):
155 | newSettings = {}
156 | newSettings["vault path"] = self.vault_path.toPlainText()
157 | newSettings["ignore folder"] = self.templates_folder.toPlainText()
158 | # newSettings["trash folder"] = self.trash_folder.text()
159 | newSettings["archive folder"] = self.archive_folder.toPlainText()
160 | newSettings["mode"] = self.mode.text()
161 | newSettings["type"] = self.type.text()
162 | newSettings["bold"] = get_text(self.bold.isChecked())
163 | newSettings["highlight"] = get_text(self.highlight.isChecked())
164 | newSettings["italics"] = get_text(self.italics.isChecked())
165 | newSettings["image"] = get_text(self.image.isChecked())
166 | newSettings["quote"] = get_text(self.quote.isChecked())
167 | newSettings["QA"] = get_text(self.QuestionOrAnswer.isChecked())
168 | newSettings["list"] = get_text(self.list.isChecked())
169 | newSettings["inline code"] = get_text(self.inline_code.isChecked())
170 | newSettings["block code"] = get_text(self.block_code.isChecked())
171 | settings.save_settings(newSettings)
172 |
173 | ###############################################################################################################################
174 | ###############################################################################################################################
175 | ###############################################################################################################################
176 |
177 | if self.vault_path.toPlainText().find("\n") != -1:
178 | vault_paths = self.vault_path.toPlainText().split("\n")
179 | else:
180 | vault_paths = [self.vault_path.toPlainText()]
181 |
182 | my_files_catalog = []
183 |
184 | for a_vault_path in vault_paths:
185 | if a_vault_path != "":
186 | my_files_catalog = my_files_catalog + read_files(a_vault_path, "")
187 |
188 | length_of_files = len(my_files_catalog)
189 | for i in range(0, length_of_files):
190 | my_files_catalog[i].set_file_content(obsidian_url.process_obsidian_file(my_files_catalog[i].file_content, my_files_catalog))
191 |
192 | anki_importer.importer(my_files_catalog)
193 |
194 | ###############################################################################################################################
195 | ###############################################################################################################################
196 | ###############################################################################################################################
197 |
198 | mw.update()
199 | mw.reset(True)
200 |
201 | self.close()
202 |
203 | def onSave(self):
204 | newSettings = {}
205 | newSettings["vault path"] = self.vault_path.toPlainText()
206 | newSettings["ignore folder"] = self.templates_folder.toPlainText()
207 | newSettings["archive folder"] = self.archive_folder.toPlainText()
208 |
209 | newSettings["mode"] = self.mode.text()
210 | newSettings["type"] = self.type.text()
211 | newSettings["bold"] = get_text(self.bold.isChecked())
212 | newSettings["highlight"] = get_text(self.highlight.isChecked())
213 | newSettings["italics"] = get_text(self.italics.isChecked())
214 | newSettings["image"] = get_text(self.image.isChecked())
215 | newSettings["quote"] = get_text(self.quote.isChecked())
216 | newSettings["QA"] = get_text(self.QuestionOrAnswer.isChecked())
217 | newSettings["list"] = get_text(self.list.isChecked())
218 | newSettings["inline code"] = get_text(self.inline_code.isChecked())
219 | newSettings["block code"] = get_text(self.block_code.isChecked())
220 | settings.save_settings(newSettings)
221 | self.close()
222 |
223 | action = QAction("Obsidianki 4", aqt.mw)
224 | action.triggered.connect(lambda: ObsidiankiSettings(aqt.mw))
225 |
226 | aqt.mw.form.menuTools.addAction(action)
--------------------------------------------------------------------------------
/src/anki_importer.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 | import shutil
4 | from . import settings
5 | from aqt import mw
6 | from anki.cards import Card
7 | from anki.notes import Note
8 | from anki.collection import Collection
9 | from aqt.utils import showInfo
10 |
11 | def importer(my_files_catalog):
12 | for file in my_files_catalog:
13 | importer_to_anki(file)
14 | empty_trash()
15 | delete_empty_decks()
16 |
17 | def importer_to_anki(file):
18 |
19 | archive_folder_input = settings.get_settings_by_name("archive folder")
20 | if archive_folder_input == "":
21 | pass
22 | elif archive_folder_input.find("\n") != -1:
23 | archive_folders = archive_folder_input.split("\n")
24 | else:
25 | archive_folders = [archive_folder_input]
26 |
27 | is_in_archive_folder = False
28 |
29 | for archive_folder in archive_folders:
30 | archive_folder = archive_folder.lstrip(" ")
31 | archive_folder = archive_folder.rstrip(" ")
32 | archive_folder = "/" + archive_folder
33 | if file.get_file_relative_path().startswith(archive_folder) and archive_folder != "" and archive_folder != "\n":
34 | is_in_archive_folder = True
35 |
36 | if file.get_file_relative_path().startswith("/.trash"):
37 | uid = file.get_file_uid()
38 | note_list = mw.col.find_notes(uid)
39 | if len(note_list) > 0:
40 | for single_note_id in note_list:
41 | single_note = mw.col.getNote(single_note_id)
42 | try:
43 | if single_note["UID"] == uid:
44 | mw.col.remNotes([single_note_id])
45 | except KeyError:
46 | pass
47 | elif is_in_archive_folder: # or file.get_file_root_folder() == settings.get_settings_by_name("ignore folder")
48 | uid = file.get_file_uid()
49 | note_list = mw.col.find_notes(uid)
50 | if len(note_list) > 0:
51 | for single_note_id in note_list:
52 | single_note = mw.col.getNote(single_note_id)
53 | if single_note["UID"] == uid:
54 | mw.col.remNotes([single_note_id])
55 | else:
56 | deck_id = mw.col.decks.id(file.get_deck_name())
57 | mw.col.decks.select(deck_id)
58 | card_model = mw.col.models.byName("Obsidianki4")
59 | uid = file.get_file_uid()
60 | note_list = mw.col.find_notes(uid)
61 | found_exisiting_file = False
62 | if len(note_list) > 0:
63 | for single_note_id in note_list:
64 | single_note = mw.col.getNote(single_note_id)
65 | if single_note.model() == card_model:
66 | if single_note["UID"] == uid:
67 | if file.get_cloze_or_basic():
68 | single_note["Cloze"] = file.get_file_content()
69 | single_note["Text"] = ""
70 | else:
71 | single_note["Cloze"] = "{{c1::}}"
72 | single_note["Text"] = file.get_file_content()
73 |
74 | back_extra = "Source: " + file.get_file_name_with_url()
75 | single_note["Back Extra"] = back_extra
76 |
77 | single_note.tags = []
78 | for tag in file.get_tags():
79 | single_note.tags.append(tag)
80 | try:
81 | card_ids = mw.col.card_ids_of_note(single_note_id)
82 | mw.col.set_deck(card_ids, deck_id)
83 | except AttributeError:
84 | card_ids = mw.col.find_cards(uid)
85 | mw.col.decks.setDeck(card_ids, deck_id)
86 | single_note.flush()
87 | found_exisiting_file = True
88 | if not found_exisiting_file:
89 | try:
90 | deck = mw.col.decks.get(deck_id)
91 | deck["mid"] = card_model["id"]
92 | mw.col.decks.save(deck)
93 | note_object = mw.col.newNote(deck_id)
94 | if file.get_cloze_or_basic():
95 | note_object["Cloze"] = file.get_file_content()
96 | note_object["Text"] = ""
97 | else:
98 | note_object["Cloze"] = "{{c1::}}"
99 | note_object["Text"] = file.get_file_content()
100 | note_object["UID"] = uid
101 | back_extra = "Source: " + file.get_file_name_with_url()
102 | note_object["Back Extra"] = back_extra
103 | for tag in file.get_tags():
104 | note_object.tags.append(tag)
105 |
106 | mw.col.add_note(note_object, deck_id)
107 | except TypeError:
108 | pass
109 |
110 | def delete_empty_decks():
111 | names_and_ids = mw.col.decks.all_names_and_ids()
112 | for name_and_id in names_and_ids:
113 | # I could not find what type this object is, so the only way for me to do it now is to use the string.
114 | name_and_id_segments = str(name_and_id).split("\n")
115 | deck_id= int(name_and_id_segments[0].split(": ")[1])
116 |
117 | if deck_has_cards(deck_id):
118 | mw.col.decks.rem(deck_id, True, True)
119 |
120 | def empty_trash():
121 | path_s = settings.get_settings_by_name("vault path")
122 |
123 | if path_s == "":
124 | pass
125 | elif path_s.find("\n"):
126 | paths = path_s.split("\n")
127 | else:
128 | paths = [paths]
129 |
130 | for path in paths:
131 | path = path.lstrip(" ")
132 | path = path.rstrip(" ")
133 | # TODO: Add this to settings
134 | if path != "":
135 | trash_can_path = path + "/" + ".trash"
136 | try:
137 | trash_directories = os.listdir(trash_can_path)
138 | for trash_directory in trash_directories:
139 | trash_directory_path = trash_can_path + "/" + trash_directory
140 | try:
141 | shutil.rmtree(trash_directory_path)
142 | except NotADirectoryError:
143 | os.remove(trash_directory_path)
144 | except NotADirectoryError:
145 | pass
146 |
147 | def deck_has_cards(deck_id):
148 | if deck_id != 1:
149 | try:
150 | if mw.col.decks.card_count(deck_id, True) == 0:
151 | return True
152 | except AttributeError:
153 | cids = mw.col.decks.cids(deck_id, True)
154 | if len(cids) == 0:
155 | return True
156 | return False
--------------------------------------------------------------------------------
/src/files.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | from aqt import mw
3 | from aqt.utils import showInfo
4 | from . import processor
5 |
6 | def gen_obsidian_url(vault_name, file_url):
7 | vault_url = "obsidian://open?vault=" + my_encode(vault_name)
8 | file_url = "&file=" + my_encode(file_url)
9 | return vault_url + file_url
10 |
11 |
12 | def my_encode(string:str):
13 | string = str(string.encode("utf-8"))
14 | string = string.replace("\\x", "%")
15 | string = string.replace(" ", "%20")
16 | string = string.replace("/", "%2F")
17 | string = string.lstrip("\'b")
18 | string = string.rstrip("\'")
19 | string = capitalize_unicode(string)
20 | return string
21 |
22 |
23 | def capitalize_unicode(string):
24 | new = []
25 | position = -5
26 | for index in range(0, len(string)):
27 | if string[index] == "%":
28 | position = index
29 | new.append(string[index])
30 | elif index == position + 1 or index == position + 2:
31 | new.append(string[index].capitalize())
32 | else:
33 | new.append(string[index])
34 | return "".join(new)
35 |
36 |
37 | class File:
38 | # important parameters
39 | file_name = ""
40 | vault_path = ""
41 | relative_path = ""
42 | full_path = ""
43 | uid = ""
44 | file_content = ""
45 | cloze_or_basic = True
46 | obsidian_url = ""
47 | metadata = {}
48 |
49 | def __init__(self, vault_path, relative_path):
50 | self.vault_path = vault_path
51 | self.relative_path = relative_path
52 | self.full_path = self.vault_path + "/" + self.relative_path
53 | tmp = processor.read_file(self.full_path)
54 | self.uid = tmp[0]
55 | self.file_content = tmp[1]
56 | self.cloze_or_basic = tmp[2]
57 | self.metadata = tmp[3]
58 | self.obsidian_url = self.generate_obsidian_url()
59 | self.file_name = self.generate_file_name()
60 |
61 |
62 | def get_deck_name(self):
63 | root_name = self.vault_path.split("/")[-1]
64 | sublevel_name_segments = self.relative_path.split("/")[:-1]
65 | sublevel_name = "::".join(sublevel_name_segments)
66 | deck_name = root_name + sublevel_name
67 | return deck_name
68 |
69 | def get_file_root_folder(self):
70 | tmp = self.relative_path.lstrip("/")
71 | root_folder = tmp.split("/")[0]
72 | # TODO: Delete this test code
73 | return root_folder
74 |
75 | def get_file_full_path(self):
76 | return self.full_path
77 |
78 | def get_file_relative_path(self):
79 | return self.relative_path
80 |
81 |
82 | def generate_obsidian_url(self):
83 | vault_name = self.vault_path.split("/")[-1]
84 | file_url_segments = self.relative_path.split(".")[:-1]
85 | file_url = ".".join(file_url_segments)
86 | return gen_obsidian_url(vault_name, file_url)
87 |
88 |
89 | def get_obsidian_url(self):
90 | return self.obsidian_url
91 |
92 |
93 | def generate_file_name(self):
94 | file_name = self.relative_path.split("/")[-1]
95 | file_name_segments = file_name.split(".")[:-1]
96 | file_name = ".".join(file_name_segments)
97 | return file_name
98 |
99 |
100 | def get_file_name(self):
101 | return self.file_name
102 |
103 |
104 | def get_file_name_with_url(self):
105 | url = self.get_obsidian_url()
106 | name = self.get_file_name()
107 | name_with_url = "" + name + ""
108 | return name_with_url
109 |
110 |
111 | def get_file_uid(self):
112 | return self.uid
113 |
114 |
115 | def get_cloze_or_basic(self):
116 | return self.cloze_or_basic
117 |
118 |
119 | def set_file_content(self, file_content):
120 | self.file_content = file_content
121 |
122 |
123 | def get_file_content(self):
124 | return self.file_content
125 |
126 |
127 | def get_tags(self):
128 | tag_line = "[]"
129 | try:
130 | tag_line = self.metadata["tags"]
131 | except:
132 | pass
133 | tag_line = tag_line.lstrip("[")
134 | tag_line = tag_line.rstrip("]")
135 | if tag_line.find("/"):
136 | tag_line = tag_line.replace("/", "::")
137 | tags = tag_line.split(",")
138 | for i in range(0, len(tags)):
139 | tags[i] = tags[i].lstrip(" ")
140 | tags[i] = tags[i].rstrip(" ")
141 | tags[i] = tags[i].replace(" ", "_")
142 | return tags
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Obsidianki 4",
3 | "package": "Obsidianki 4"
4 | }
--------------------------------------------------------------------------------
/src/markdown2/markdown2.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright (c) 2012 Trent Mick.
3 | # Copyright (c) 2007-2008 ActiveState Corp.
4 | # License: MIT (http://www.opensource.org/licenses/mit-license.php)
5 |
6 | r"""A fast and complete Python implementation of Markdown.
7 |
8 | [from http://daringfireball.net/projects/markdown/]
9 | > Markdown is a text-to-HTML filter; it translates an easy-to-read /
10 | > easy-to-write structured text format into HTML. Markdown's text
11 | > format is most similar to that of plain text email, and supports
12 | > features such as headers, *emphasis*, code blocks, blockquotes, and
13 | > links.
14 | >
15 | > Markdown's syntax is designed not as a generic markup language, but
16 | > specifically to serve as a front-end to (X)HTML. You can use span-level
17 | > HTML tags anywhere in a Markdown document, and you can use block level
18 | > HTML tags (like
tags.
1831 | """
1832 | yield 0, ""
1833 | for tup in inner:
1834 | yield tup
1835 | yield 0, "
"
1836 |
1837 | def wrap(self, source, outfile):
1838 | """Return the source with a code, pre, and div."""
1839 | return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
1840 |
1841 | formatter_opts.setdefault("cssclass", "codehilite")
1842 | formatter = HtmlCodeFormatter(**formatter_opts)
1843 | return pygments.highlight(codeblock, lexer, formatter)
1844 |
1845 | def _code_block_sub(self, match, is_fenced_code_block=False):
1846 | lexer_name = None
1847 | if is_fenced_code_block:
1848 | lexer_name = match.group(1)
1849 | if lexer_name:
1850 | formatter_opts = self.extras['fenced-code-blocks'] or {}
1851 | codeblock = match.group(2)
1852 | codeblock = codeblock[:-1] # drop one trailing newline
1853 | else:
1854 | codeblock = match.group(1)
1855 | codeblock = self._outdent(codeblock)
1856 | codeblock = self._detab(codeblock)
1857 | codeblock = codeblock.lstrip('\n') # trim leading newlines
1858 | codeblock = codeblock.rstrip() # trim trailing whitespace
1859 |
1860 | # Note: "code-color" extra is DEPRECATED.
1861 | if "code-color" in self.extras and codeblock.startswith(":::"):
1862 | lexer_name, rest = codeblock.split('\n', 1)
1863 | lexer_name = lexer_name[3:].strip()
1864 | codeblock = rest.lstrip("\n") # Remove lexer declaration line.
1865 | formatter_opts = self.extras['code-color'] or {}
1866 |
1867 | # Use pygments only if not using the highlightjs-lang extra
1868 | if lexer_name and "highlightjs-lang" not in self.extras:
1869 | def unhash_code(codeblock):
1870 | for key, sanitized in list(self.html_spans.items()):
1871 | codeblock = codeblock.replace(key, sanitized)
1872 | replacements = [
1873 | ("&", "&"),
1874 | ("<", "<"),
1875 | (">", ">")
1876 | ]
1877 | for old, new in replacements:
1878 | codeblock = codeblock.replace(old, new)
1879 | return codeblock
1880 | lexer = self._get_pygments_lexer(lexer_name)
1881 | if lexer:
1882 | codeblock = unhash_code( codeblock )
1883 | colored = self._color_with_pygments(codeblock, lexer,
1884 | **formatter_opts)
1885 | return "\n\n%s\n\n" % colored
1886 |
1887 | codeblock = self._encode_code(codeblock)
1888 | pre_class_str = self._html_class_str_from_tag("pre")
1889 |
1890 | if "highlightjs-lang" in self.extras and lexer_name:
1891 | code_class_str = ' class="%s language-%s"' % (lexer_name, lexer_name)
1892 | else:
1893 | code_class_str = self._html_class_str_from_tag("code")
1894 |
1895 | return "\n\n%s\n
\n\n" % (
1896 | pre_class_str, code_class_str, codeblock)
1897 |
1898 | def _html_class_str_from_tag(self, tag):
1899 | """Get the appropriate ' class="..."' string (note the leading
1900 | space), if any, for the given tag.
1901 | """
1902 | if "html-classes" not in self.extras:
1903 | return ""
1904 | try:
1905 | html_classes_from_tag = self.extras["html-classes"]
1906 | except TypeError:
1907 | return ""
1908 | else:
1909 | if tag in html_classes_from_tag:
1910 | return ' class="%s"' % html_classes_from_tag[tag]
1911 | return ""
1912 |
1913 | def _do_code_blocks(self, text):
1914 | """Process Markdown `` blocks."""
1915 | code_block_re = re.compile(r'''
1916 | (?:\n\n|\A\n?)
1917 | ( # $1 = the code block -- one or more lines, starting with a space/tab
1918 | (?:
1919 | (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces
1920 | .*\n+
1921 | )+
1922 | )
1923 | ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
1924 | # Lookahead to make sure this block isn't already in a code block.
1925 | # Needed when syntax highlighting is being used.
1926 | (?![^<]*\
)
1927 | ''' % (self.tab_width, self.tab_width),
1928 | re.M | re.X)
1929 | return code_block_re.sub(self._code_block_sub, text)
1930 |
1931 | _fenced_code_block_re = re.compile(r'''
1932 | (?:\n+|\A\n?)
1933 | ^```\s*?([\w+-]+)?\s*?\n # opening fence, $1 = optional lang
1934 | (.*?) # $2 = code block content
1935 | ^```[ \t]*\n # closing fence
1936 | ''', re.M | re.X | re.S)
1937 |
1938 | def _fenced_code_block_sub(self, match):
1939 | return self._code_block_sub(match, is_fenced_code_block=True)
1940 |
1941 | def _do_fenced_code_blocks(self, text):
1942 | """Process ```-fenced unindented code blocks ('fenced-code-blocks' extra)."""
1943 | return self._fenced_code_block_re.sub(self._fenced_code_block_sub, text)
1944 |
1945 | # Rules for a code span:
1946 | # - backslash escapes are not interpreted in a code span
1947 | # - to include one or or a run of more backticks the delimiters must
1948 | # be a longer run of backticks
1949 | # - cannot start or end a code span with a backtick; pad with a
1950 | # space and that space will be removed in the emitted HTML
1951 | # See `test/tm-cases/escapes.text` for a number of edge-case
1952 | # examples.
1953 | _code_span_re = re.compile(r'''
1954 | (?%s
" % c
1967 |
1968 | def _do_code_spans(self, text):
1969 | # * Backtick quotes are used for
spans.
1970 | #
1971 | # * You can use multiple backticks as the delimiters if you want to
1972 | # include literal backticks in the code span. So, this input:
1973 | #
1974 | # Just type ``foo `bar` baz`` at the prompt.
1975 | #
1976 | # Will translate to:
1977 | #
1978 | # Just type foo `bar` baz
at the prompt.
`bar`
...
1991 | return self._code_span_re.sub(self._code_span_sub, text)
1992 |
1993 | def _encode_code(self, text):
1994 | """Encode/escape certain characters inside Markdown code runs.
1995 | The point is that in code, these characters are literals,
1996 | and lose their special Markdown meanings.
1997 | """
1998 | replacements = [
1999 | # Encode all ampersands; HTML entities are not
2000 | # entities within a Markdown code span.
2001 | ('&', '&'),
2002 | # Do the angle bracket song and dance:
2003 | ('<', '<'),
2004 | ('>', '>'),
2005 | ]
2006 | for before, after in replacements:
2007 | text = text.replace(before, after)
2008 | hashed = _hash_text(text)
2009 | self._escape_table[text] = hashed
2010 | return hashed
2011 |
2012 | _strike_re = re.compile(r"~~(?=\S)(.+?)(?<=\S)~~", re.S)
2013 | def _do_strike(self, text):
2014 | text = self._strike_re.sub(r".+?)', re.S) 2104 | def _dedent_two_spaces_sub(self, match): 2105 | return re.sub(r'(?m)^ ', '', match.group(1)) 2106 | 2107 | def _block_quote_sub(self, match): 2108 | bq = match.group(1) 2109 | is_spoiler = 'spoiler' in self.extras and self._bq_all_lines_spoilers.match(bq) 2110 | # trim one level of quoting 2111 | if is_spoiler: 2112 | bq = self._bq_one_level_re_spoiler.sub('', bq) 2113 | else: 2114 | bq = self._bq_one_level_re.sub('', bq) 2115 | # trim whitespace-only lines 2116 | bq = self._ws_only_line_re.sub('', bq) 2117 | bq = self._run_block_gamut(bq) # recurse 2118 | 2119 | bq = re.sub('(?m)^', ' ', bq) 2120 | # These leading spaces screw with
content, so we need to fix that: 2121 | bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq) 2122 | 2123 | if is_spoiler: 2124 | return '\n%s\n\n\n' % bq 2125 | else: 2126 | return '\n%s\n\n\n' % bq 2127 | 2128 | def _do_block_quotes(self, text): 2129 | if '>' not in text: 2130 | return text 2131 | if 'spoiler' in self.extras: 2132 | return self._block_quote_re_spoiler.sub(self._block_quote_sub, text) 2133 | else: 2134 | return self._block_quote_re.sub(self._block_quote_sub, text) 2135 | 2136 | def _form_paragraphs(self, text): 2137 | # Strip leading and trailing lines: 2138 | text = text.strip('\n') 2139 | 2140 | # Wraptags. 2141 | grafs = [] 2142 | for i, graf in enumerate(re.split(r"\n{2,}", text)): 2143 | if graf in self.html_blocks: 2144 | # Unhashify HTML blocks 2145 | grafs.append(self.html_blocks[graf]) 2146 | else: 2147 | cuddled_list = None 2148 | if "cuddled-lists" in self.extras: 2149 | # Need to put back trailing '\n' for `_list_item_re` 2150 | # match at the end of the paragraph. 2151 | li = self._list_item_re.search(graf + '\n') 2152 | # Two of the same list marker in this paragraph: a likely 2153 | # candidate for a list cuddled to preceding paragraph 2154 | # text (issue 33). Note the `[-1]` is a quick way to 2155 | # consider numeric bullets (e.g. "1." and "2.") to be 2156 | # equal. 2157 | if (li and len(li.group(2)) <= 3 2158 | and ( 2159 | (li.group("next_marker") and li.group("marker")[-1] == li.group("next_marker")[-1]) 2160 | or 2161 | li.group("next_marker") is None 2162 | ) 2163 | ): 2164 | start = li.start() 2165 | cuddled_list = self._do_lists(graf[start:]).rstrip("\n") 2166 | assert cuddled_list.startswith("
tags. 2170 | graf = self._run_span_gamut(graf) 2171 | grafs.append("
" % self._html_class_str_from_tag('p') + graf.lstrip(" \t") + "
") 2172 | 2173 | if cuddled_list: 2174 | grafs.append(cuddled_list) 2175 | 2176 | return "\n\n".join(grafs) 2177 | 2178 | def _add_footnotes(self, text): 2179 | if self.footnotes: 2180 | footer = [ 2181 | '%s
" % backlink) 2215 | footer.append('(.*)
\n") 16 | md=markdown(placeholder) 17 | mdp=mdstrip.match(md) 18 | if mdp and mdp.group(1)==placeholder: 19 | return True 20 | return False 21 | 22 | def mathdown(text): 23 | """Convenience function which runs the basic markdown and mathjax processing sequentially.""" 24 | tmp=sanitizeInput(text) 25 | return reconstructMath(markdown(tmp[0]),tmp[1]) 26 | 27 | def sanitizeInput(string,inline_delims=["$","$"],equation_delims=["$$","$$"],placeholder="™™™"): 28 | """Given a string that will be passed to markdown, the content of the different math blocks is stripped out and replaced by a placeholder which MUST be ignored by markdown. A list is returned containing the text with placeholders and a list of the stripped out equations. Note that any pre-existing instances of the placeholder are "replaced" with themselves and a corresponding dummy entry is placed in the returned codeblock. The sanitized string can then be passed safetly through markdown and then reconstructed with reconstructMath. 29 | 30 | There are potential four delimiters that can be specified. The left and right delimiters for inline and equation mode math. These can potentially be anything that isn't already used by markdown and is compatible with mathjax (see documentation for both). 31 | """ 32 | #Check placeholder is valid. 33 | if not markdown_safe(placeholder): 34 | raise ValueError("Placeholder %s altered by markdown processing." % placeholder) 35 | #really what we want is a reverse markdown function, but as that's too much work, this will do 36 | inline_left=re.compile("(?=0):tmp] 79 | #Set the new post 80 | post=tmp 81 | #Back to start! 82 | continue 83 | elif startmatches[1] is None and startmatches[2] is None: 84 | #No more blocks, add in the rest of string and be done with it... 85 | sanitizedString = sanitizedString + string[post*(post>=0):] 86 | return (sanitizedString, codeblocks) 87 | elif startmatches[1] is None: 88 | inBlock=2 89 | elif startmatches[2] is None: 90 | inBlock=1 91 | else: 92 | inBlock = (startpoints[1] < startpoints[2]) + (startpoints[1] > startpoints[2])*2 93 | if not inBlock: 94 | inBlock = break_tie(startmatches[1],startmatches[2]) 95 | #Magic to ensure minimum index is 0 96 | sanitizedString = sanitizedString+string[(post*(post>=0)):startpoints[inBlock]] 97 | post = startmatches[inBlock].end() 98 | #Now find the matching end... 99 | while terminator"):
12 | isInCode = True
13 | elif lines[i].startswith("
(?!{{c¡::", file_content)
133 | file_content = re.sub(r"
(?!)", "}}", file_content)
134 | if cloze_settings["QA"] == "True" or cloze_settings["QA"] == "true":
135 | tmp = file_content.split("\n")
136 | for i in range(0, len(tmp)):
137 | if tmp[i].startswith("A: ") and tmp[i].endswith("
"): 138 | # TODO: add a security check to make sure that these two things are in the same line. 139 | tmp[i] = tmp[i].replace("{{¡::", "") 140 | tmp[i] = tmp[i].replace("}}", "") 141 | tmp[i] = tmp[i].replace("A: ", "
A: {{c¡::", 1) 142 | tmp[i] = tmp[i].replace("
", "}}", 1) 143 | elif tmp[i].startswith("答:") and tmp[i].endswith("
"): 144 | tmp[i] = tmp[i].replace("{{¡::", "") 145 | tmp[i] = tmp[i].replace("}}", "") 146 | tmp[i] = tmp[i].replace("答:", "
答:{{c¡::", 1) 147 | tmp[i] = tmp[i].replace("
", "}}") 148 | 149 | # ================================================================== 150 | # | You Can Disable this code if you Enabled strict line spacing. | 151 | # ================================================================== 152 | elif tmp[i].startswith("A: ") and tmp[i].endswith(""): 153 | tmp[i] = tmp[i].replace("{{¡::", "") 154 | tmp[i] = tmp[i].replace("}}", "") 155 | tmp[i] = tmp[i].replace("A: ", "A: {{c¡::", 1) 156 | tmp[i] = tmp[i].replace("", "}}", 1) 157 | elif tmp[i].startswith("答:") and tmp[i].endswith(""): 158 | tmp[i] = tmp[i].replace("{{¡::", "") 159 | tmp[i] = tmp[i].replace("}}", "") 160 | tmp[i] = tmp[i].replace("答:", "答: {{c¡::", 1) 161 | tmp[i] = tmp[i].replace("", "}}", 1) 162 | file_content = "\n".join(tmp) 163 | if cloze_settings["list"] == "True" or cloze_settings["list"] == "true": 164 | tmp = file_content.split("\n") 165 | for i in range(0, len(tmp)): 166 | if tmp[i].find("{{c¡::") != -1: 167 | pass 168 | else: 169 | tmp[i] = tmp[i].replace("", "") 178 | if cloze_settings["block code"] == "True" or cloze_settings["block code"] == "true": 179 | # =================================================== 180 | # | TODO: use REGEX to replace the proper ones here | 181 | # =================================================== 182 | file_content = file_content.replace("{{c¡::") 177 | file_content = file_content.replace("", "}}
", "{{c¡::")
183 | file_content = file_content.replace("
", "}}
A: ") or tmp[i].startswith("
答:") or tmp[i].startswith("A: ") or tmp[i].startswith("答:"): 221 | increase_num = get_cloze_number(tmp) + 1 222 | tmp[i] = tmp[i].replace("¡", str(increase_num)) 223 | cloze_num = increase_num + 1 224 | elif tmp[i].startswith("
"):
265 | isInCode = True
266 | elif lines[i].startswith("