diff -r f92cd37ad600 -r a3a7c8342b1c py/gadialog_srs_anki.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/py/gadialog_srs_anki.py Mon Feb 10 00:54:18 2020 +0200 @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +"""Anki card writer from gaphrase format""" + +import re +import hashlib + +import os +import io +import sys +import tempfile +import shutil + +from gadict_util import ParseException + +import anki +from anki.exporting import AnkiPackageExporter + +FINAME = None +FONAME = None +FDELNAME = None + +ARG_NAME_RE = re.compile("-name=(.+)") +ARG_DELFILE_RE = re.compile("-delfile=(.+)") + +look_for_files = False +for idx in range(1, len(sys.argv)): + arg = sys.argv[idx] + if arg == "--": + look_for_files = True + continue + if not look_for_files: + m = ARG_NAME_RE.match(arg) + if m: + NAME = m.group(1) + continue + m = ARG_DELFILE_RE.match(arg) + if m: + FDELNAME = m.group(1) + continue + if arg.startswith("-"): + raise Exception("Unsupported option format: '{:s}'".format(arg)) + if not FINAME: + FINAME = arg + continue + if not FONAME: + FONAME = arg + continue + raise Exception("Unnecessary argument: '{:s}'".format(arg)) + +if not FINAME: + raise Exception("Input file name is not passed...") +if FONAME is None: + raise Exception("Output file name is not passed...") +if not NAME: + NAME, _ = os.path.splitext(os.path.basename(FINAME)) + +# if FDELNAME: +# FDEL = io.open(FDELNAME, mode='r', buffering=1, encoding="utf-8") +# else: +# FDEL = None + +################################################################ + +class Parser: + + COMMENT_RE = re.compile("^; ") + NUM_RE = re.compile(u"^# ([1-9][0-9]*)$") + PHRASE_START_RE = re.compile(u"^- (.*)") + + def __init__(self): + pass + + def readline(self): + while True: + self.line = self.stream.readline() + self.eof = len(self.line) == 0 + if self.eof: + break + self.lineno += 1 + if self.COMMENT_RE.search(self.line): + continue + self.line = self.line.strip(' \n\t') + if len(self.line) > 0: + break + + def parse(self, stream): + self.lineno = 0 + self.stream = stream + self.dom = dict() + self.eof = False + try: + self.parse_prelude() + while not self.eof: + self.parse_article() + except ParseException as ex: + if sys.version_info.major == 2: + import traceback + traceback.print_exc() + raise ParseException(ex.msg, self.lineno, self.line) + return self.dom + + def parse_prelude(self): + while True: + self.readline() + if self.eof: + return + m = self.NUM_RE.match(self.line) + if m: + self.num = m.group(1) + break + + def parse_article(self): + """Assume we are at ``# NUM`` line.""" + num = self.num + phrase_buf = [] + phrases = [] + while True: + self.readline() + if self.eof: + if len(phrase_buf) > 0: + phrases.append(" ".join(phrase_buf)) + break + m = self.NUM_RE.match(self.line) + if m: + if len(phrase_buf) > 0: + phrases.append(" ".join(phrase_buf)) + self.num = m.group(1) + break + m = self.PHRASE_START_RE.match(self.line) + if m: + if len(phrase_buf) > 0: + phrases.append(" ".join(phrase_buf)) + phrase_buf = [m.group(1)] + else: + phrase_buf.append(self.line) + if len(phrases) == 0: + raise ParseException("""There are no any phrases...""") + if num in self.dom: + raise ParseException("""Conflicting key: {}...""".format(num)) + self.dom[num] = phrases + +FIN = io.open(FINAME, mode='r', buffering=1, encoding="utf-8") + +PARSER = Parser() +try: + DOM = PARSER.parse(FIN) +finally: + FIN.close() + +################################################################ + +MODEL_CSS = """ +.card { + font-family: arial; + font-size: 20px; + text-align: left; + color: black; + background-color: white; +} +.line { + margin-bottom: 0.5em; +} +.odd { + color: #004000; +} +.even { + color: #000080; +} +""" + +class AnkiDbBuilder: + + def __init__(self, tmpdir, name): + self.tmpdir = tmpdir + self.name = name + + self.collection = collection = anki.Collection(os.path.join(self.tmpdir, 'collection.anki2')) + + deck_id = collection.decks.id(self.name) + + # It is essential to keep model['id'] unchanged between upgrades!! + model_id = int(hashlib.sha1(self.name.encode('utf-8')).hexdigest(), 16) % (2**63) + + ################################################################ + # One face card model. + model = collection.models.new(self.name + "_front") + model['did'] = deck_id + model['css'] = MODEL_CSS + + collection.models.addField(model, collection.models.newField('Front')) + + tmpl = collection.models.newTemplate('Front') + tmpl['qfmt'] = '
{{Front}}
' + tmpl['afmt'] = '{{FrontSide}}' + collection.models.addTemplate(model, tmpl) + + model['id'] = model_id + collection.models.update(model) + collection.models.save(model) + self.model = model + + def guid(self, num): + h = hashlib.md5(":".join((self.name, str(num))).encode('utf-8')) + return h.hexdigest() + + def add_note(self, num, front): + note = anki.notes.Note(self.collection, self.model) + note['Front'] = front + note.guid = self.guid(num) + self.collection.addNote(note) + + def export(self, fname): + export = AnkiPackageExporter(self.collection) + export.exportInto(fname) + + def close(self): + self.collection.close() + +def write_lines(buf, lines): + odd = True + for line in lines: + if odd: + buf.append("
") + else: + buf.append("
") + buf.append("- ") + buf.append(line) + buf.append("
") + odd = not odd + +# Looks like anki libs change working directory to media directory of current deck +# Therefore absolute path should be stored before creating temporary deck +FONAME = os.path.abspath(FONAME) +TMPDIR = tempfile.mkdtemp(dir=os.path.dirname(FONAME)) + +try: + BUILDER = AnkiDbBuilder(TMPDIR, NAME) + + for num, lines in DOM.items(): + buf = [] + write_lines(buf, lines) + front = "".join(buf) + BUILDER.add_note(num, front) + BUILDER.export(FONAME) +finally: + BUILDER.close() + shutil.rmtree(TMPDIR, ignore_errors=True) +