py/gaphrase_srs_anki.py
changeset 1146 50007cd95972
child 1148 263e9e066981
equal deleted inserted replaced
1145:79b55cca9f44 1146:50007cd95972
       
     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