|
1 # -*- coding: utf-8 -*- |
|
2 """Anki card writer from gaphrase format""" |
|
3 |
|
4 import re |
|
5 import hashlib |
|
6 |
|
7 import os |
|
8 import io |
|
9 import sys |
|
10 import tempfile |
|
11 import shutil |
|
12 |
|
13 import anki |
|
14 from anki.exporting import AnkiPackageExporter |
|
15 |
|
16 FINAME = None |
|
17 FONAME = None |
|
18 FDELNAME = None |
|
19 |
|
20 ARG_NAME_RE = re.compile("-name=(.+)") |
|
21 ARG_DELFILE_RE = re.compile("-delfile=(.+)") |
|
22 |
|
23 look_for_files = False |
|
24 for idx in range(1, len(sys.argv)): |
|
25 arg = sys.argv[idx] |
|
26 if arg == "--": |
|
27 look_for_files = True |
|
28 continue |
|
29 if not look_for_files: |
|
30 m = ARG_NAME_RE.match(arg) |
|
31 if m: |
|
32 NAME = m.group(1) |
|
33 continue |
|
34 m = ARG_DELFILE_RE.match(arg) |
|
35 if m: |
|
36 FDELNAME = m.group(1) |
|
37 continue |
|
38 if arg.startswith("-"): |
|
39 raise Exception("Unsupported option format: '{:s}'".format(arg)) |
|
40 if not FINAME: |
|
41 FINAME = arg |
|
42 continue |
|
43 if not FONAME: |
|
44 FONAME = arg |
|
45 continue |
|
46 raise Exception("Unnecessary argument: '{:s}'".format(arg)) |
|
47 |
|
48 if not FINAME: |
|
49 raise Exception("Input file name is not passed...") |
|
50 if FONAME is None: |
|
51 raise Exception("Output file name is not passed...") |
|
52 if not NAME: |
|
53 NAME, _ = os.path.splitext(os.path.basename(FINAME)) |
|
54 |
|
55 # if FDELNAME: |
|
56 # FDEL = io.open(FDELNAME, mode='r', buffering=1, encoding="utf-8") |
|
57 # else: |
|
58 # FDEL = None |
|
59 |
|
60 ################################################################ |
|
61 |
|
62 class Parser: |
|
63 |
|
64 COMMENT_RE = re.compile("^; ") |
|
65 NUM_RE = re.compile(u"^# ([1-9][0-9]*)$") |
|
66 PHRASE_START_RE = re.compile(u"^- (.*)") |
|
67 |
|
68 def __init__(self): |
|
69 pass |
|
70 |
|
71 def readline(self): |
|
72 while True: |
|
73 self.line = self.stream.readline() |
|
74 self.eof = len(self.line) == 0 |
|
75 if self.eof: |
|
76 break |
|
77 self.lineno += 1 |
|
78 if self.COMMENT_RE.search(self.line): |
|
79 continue |
|
80 self.line = self.line.strip(' \n\t') |
|
81 if len(self.line) > 0: |
|
82 break |
|
83 |
|
84 def parse(self, stream): |
|
85 self.lineno = 0 |
|
86 self.stream = stream |
|
87 self.dom = dict() |
|
88 self.eof = False |
|
89 try: |
|
90 self.parse_prelude() |
|
91 while not self.eof: |
|
92 self.parse_article() |
|
93 except ParseException as ex: |
|
94 if sys.version_info.major == 2: |
|
95 import traceback |
|
96 traceback.print_exc() |
|
97 raise ParseException(ex.msg, self.lineno, self.line) |
|
98 return self.dom |
|
99 |
|
100 def parse_prelude(self): |
|
101 while True: |
|
102 self.readline() |
|
103 if self.eof: |
|
104 return |
|
105 m = self.NUM_RE.match(self.line) |
|
106 if m: |
|
107 self.num = m.group(1) |
|
108 break |
|
109 |
|
110 def parse_article(self): |
|
111 """Assume we are at ``# NUM`` line.""" |
|
112 num = self.num |
|
113 phrase_buf = [] |
|
114 phrases = [] |
|
115 while True: |
|
116 self.readline() |
|
117 if self.eof: |
|
118 if len(phrase_buf) > 0: |
|
119 phrases.append(" ".join(phrase_buf)) |
|
120 break |
|
121 m = self.NUM_RE.match(self.line) |
|
122 if m: |
|
123 if len(phrase_buf) > 0: |
|
124 phrases.append(" ".join(phrase_buf)) |
|
125 self.num = m.group(1) |
|
126 break |
|
127 m = self.PHRASE_START_RE.match(self.line) |
|
128 if m: |
|
129 if len(phrase_buf) > 0: |
|
130 phrases.append(" ".join(phrase_buf)) |
|
131 phrase_buf = [m.group(1)] |
|
132 else: |
|
133 phrase_buf.append(self.line) |
|
134 if len(phrases) == 0: |
|
135 raise ParseException("""There are no any phrases...""") |
|
136 self.dom[num] = phrases |
|
137 |
|
138 FIN = io.open(FINAME, mode='r', buffering=1, encoding="utf-8") |
|
139 |
|
140 PARSER = Parser() |
|
141 try: |
|
142 DOM = PARSER.parse(FIN) |
|
143 finally: |
|
144 FIN.close() |
|
145 |
|
146 ################################################################ |
|
147 |
|
148 MODEL_CSS = """ |
|
149 .card { |
|
150 font-family: arial; |
|
151 font-size: 20px; |
|
152 text-align: left; |
|
153 color: black; |
|
154 background-color: white; |
|
155 } |
|
156 .line { |
|
157 margin-bottom: 0.5em; |
|
158 } |
|
159 .odd { |
|
160 color: #004000; |
|
161 } |
|
162 .even { |
|
163 color: #000080; |
|
164 } |
|
165 """ |
|
166 |
|
167 class AnkiDbBuilder: |
|
168 |
|
169 def __init__(self, tmpdir, name): |
|
170 self.tmpdir = tmpdir |
|
171 self.name = name |
|
172 |
|
173 self.collection = collection = anki.Collection(os.path.join(self.tmpdir, 'collection.anki2')) |
|
174 |
|
175 deck_id = collection.decks.id(self.name) |
|
176 |
|
177 # It is essential to keep model['id'] unchanged between upgrades!! |
|
178 model_id = int(hashlib.sha1(self.name.encode('utf-8')).hexdigest(), 16) % (2**63) |
|
179 |
|
180 ################################################################ |
|
181 # One face card model. |
|
182 model = collection.models.new(self.name + "_front") |
|
183 model['did'] = deck_id |
|
184 model['css'] = MODEL_CSS |
|
185 |
|
186 collection.models.addField(model, collection.models.newField('Front')) |
|
187 |
|
188 tmpl = collection.models.newTemplate('Front') |
|
189 tmpl['qfmt'] = '<div class="front">{{Front}}</div>' |
|
190 tmpl['afmt'] = '{{FrontSide}}' |
|
191 collection.models.addTemplate(model, tmpl) |
|
192 |
|
193 model['id'] = model_id |
|
194 collection.models.update(model) |
|
195 collection.models.save(model) |
|
196 self.model = model |
|
197 |
|
198 def guid(self, num): |
|
199 h = hashlib.md5(":".join((self.name, str(num))).encode('utf-8')) |
|
200 return h.hexdigest() |
|
201 |
|
202 def add_note(self, num, front): |
|
203 note = anki.notes.Note(self.collection, self.model) |
|
204 note['Front'] = front |
|
205 note.guid = self.guid(num) |
|
206 self.collection.addNote(note) |
|
207 |
|
208 def export(self, fname): |
|
209 export = AnkiPackageExporter(self.collection) |
|
210 export.exportInto(fname) |
|
211 |
|
212 def close(self): |
|
213 self.collection.close() |
|
214 |
|
215 def write_lines(buf, lines): |
|
216 odd = True |
|
217 for line in lines: |
|
218 if odd: |
|
219 buf.append("<div class='line odd'>") |
|
220 else: |
|
221 buf.append("<div class='line even'>") |
|
222 buf.append("- ") |
|
223 buf.append(line) |
|
224 buf.append("</div>") |
|
225 odd = not odd |
|
226 |
|
227 # Looks like anki libs change working directory to media directory of current deck |
|
228 # Therefore absolute path should be stored before creating temporary deck |
|
229 FONAME = os.path.abspath(FONAME) |
|
230 TMPDIR = tempfile.mkdtemp(dir=os.path.dirname(FONAME)) |
|
231 |
|
232 try: |
|
233 BUILDER = AnkiDbBuilder(TMPDIR, NAME) |
|
234 |
|
235 for num, lines in DOM.items(): |
|
236 buf = [] |
|
237 write_lines(buf, lines) |
|
238 front = "".join(buf) |
|
239 BUILDER.add_note(num, front) |
|
240 BUILDER.export(FONAME) |
|
241 finally: |
|
242 BUILDER.close() |
|
243 shutil.rmtree(TMPDIR, ignore_errors=True) |
|
244 |