297 lines
9.0 KiB
Python
297 lines
9.0 KiB
Python
#-*- coding:utf-8 -*-
|
||
#
|
||
# Copyright (C) 2018 sthoo <sth201807@gmail.com>
|
||
#
|
||
# Support: Report an issue at https://github.com/sth2018/FastWordQuery/issues
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published by
|
||
# the Free Software Foundation, either version 3 of the License, or
|
||
# any later version; http://www.gnu.org/copyleft/gpl.html.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
|
||
import os
|
||
import re
|
||
import io
|
||
import shutil
|
||
import unicodedata
|
||
|
||
from collections import defaultdict
|
||
from aqt.qt import *
|
||
from aqt.utils import showInfo
|
||
|
||
from ..constants import Template
|
||
from ..context import config
|
||
from ..service import service_pool, QueryResult, copy_static_file
|
||
from ..service.base import LocalService
|
||
from ..utils import wrap_css
|
||
from ..libs.snowballstemmer import stemmer
|
||
|
||
|
||
__all__ = [
|
||
'InvalidWordException', 'update_note_fields',
|
||
'update_note_field', 'promot_choose_css', 'add_to_tmpl',
|
||
'query_flds', 'inspect_note'
|
||
]
|
||
|
||
|
||
class InvalidWordException(Exception):
|
||
"""Invalid word exception"""
|
||
|
||
|
||
def inspect_note(note):
|
||
"""
|
||
inspect the note, and get necessary input parameters
|
||
return word_ord: field index of the word in current note
|
||
return word: the word
|
||
return maps: dicts map of current note
|
||
"""
|
||
|
||
conf = config.get_maps(note.model()['id'])
|
||
maps_list = {'list': [conf], 'def': 0} if isinstance(conf, list) else conf
|
||
maps = maps_list['list'][maps_list['def']]
|
||
for i, m in enumerate(maps):
|
||
if m.get('word_checked', False):
|
||
word_ord = i
|
||
break
|
||
else:
|
||
# if no field is checked to be the word field, default the
|
||
# first one.
|
||
word_ord = 0
|
||
|
||
def purify_word(word):
|
||
return word.strip() if word else ''
|
||
|
||
word = purify_word(note.fields[word_ord])
|
||
return word_ord, word, maps
|
||
|
||
|
||
def strip_combining(txt):
|
||
"Return txt with all combining characters removed."
|
||
norm = unicodedata.normalize('NFKD', txt)
|
||
return u"".join([c for c in norm if not unicodedata.combining(c)])
|
||
|
||
|
||
def update_note_fields(note, results):
|
||
"""
|
||
Update query result to note fields, return updated fields count.
|
||
"""
|
||
|
||
if not results or not note or len(results) == 0:
|
||
return 0
|
||
count = 0
|
||
for i, q in results.items():
|
||
if isinstance(q, QueryResult) and i < len(note.fields):
|
||
count += update_note_field(note, i, q)
|
||
|
||
return count
|
||
|
||
|
||
def update_note_field(note, fld_index, fld_result):
|
||
"""
|
||
Update single field, if result is valid then return 1, else return 0
|
||
"""
|
||
|
||
result, js, jsfile = fld_result.result, fld_result.js, fld_result.jsfile
|
||
# js process: add to template of the note model
|
||
add_to_tmpl(note, js=js, jsfile=jsfile)
|
||
# if not result:
|
||
# return
|
||
if not config.force_update and not result:
|
||
return 0
|
||
|
||
value = result if result else ''
|
||
if note.fields[fld_index] != value:
|
||
note.fields[fld_index] = value
|
||
return 1
|
||
|
||
return 0
|
||
|
||
|
||
def promot_choose_css(missed_css):
|
||
'''
|
||
Choose missed css file and copy to user folder
|
||
'''
|
||
checked = set()
|
||
for css in missed_css:
|
||
filename = u'_' + css['file']
|
||
if not os.path.exists(filename) and not css['file'] in checked:
|
||
checked.add(css['file'])
|
||
showInfo(
|
||
Template.miss_css.format(
|
||
dict=css['title'],
|
||
css=css['file']
|
||
)
|
||
)
|
||
try:
|
||
filepath = css['dict_path'][:css['dict_path'].rindex(
|
||
os.path.sep)+1]
|
||
filepath = QFileDialog.getOpenFileName(
|
||
directory=filepath,
|
||
caption=u'Choose css file',
|
||
filter=u'CSS (*.css)'
|
||
)
|
||
if filepath:
|
||
shutil.copy(filepath, filename)
|
||
wrap_css(filename)
|
||
|
||
except KeyError:
|
||
pass
|
||
|
||
|
||
def add_to_tmpl(note, **kwargs):
|
||
# templates
|
||
'''
|
||
[{u'name': u'Card 1', u'qfmt': u'{{Front}}\n\n', u'did': None, u'bafmt': u'',
|
||
u'afmt': u'{{FrontSide}}\n\n<hr id=answer>\n\n{{Back}}\n\n{{12}}\n\n{{44}}\n\n', u'ord': 0, u'bqfmt': u''}]
|
||
'''
|
||
# showInfo(str(kwargs))
|
||
afmt = note.model()['tmpls'][0]['afmt']
|
||
if kwargs:
|
||
jsfile, js = kwargs.get('jsfile', None), kwargs.get('js', None)
|
||
if js and js.strip():
|
||
addings = js.strip()
|
||
if addings not in afmt:
|
||
if not addings.startswith(u'<script') and not addings.endswith(u'/script>'):
|
||
addings = u'\n<script type="text/javascript">\n{}\n</script>'.format(addings)
|
||
afmt += addings
|
||
if jsfile:
|
||
#new_jsfile = u'_' + \
|
||
# jsfile if not jsfile.startswith(u'_') else jsfile
|
||
#copy_static_file(jsfile, new_jsfile)
|
||
#addings = u'\r\n<script src="{}"></script>'.format(new_jsfile)
|
||
#afmt += addings
|
||
jsfile = jsfile if isinstance(jsfile, list) else [jsfile]
|
||
addings = u''
|
||
for fn in jsfile:
|
||
try:
|
||
with open(fn, 'r', encoding="utf-8") as f:
|
||
addings += u'\n<script type="text/javascript">\n{}\n</script>'.format(f.read())
|
||
f.close()
|
||
except:
|
||
pass
|
||
if addings not in afmt:
|
||
afmt += addings
|
||
note.model()['tmpls'][0]['afmt'] = afmt
|
||
|
||
|
||
def query_flds(note, fileds=None):
|
||
"""
|
||
Query fields of single note
|
||
"""
|
||
|
||
word_ord, word, maps = inspect_note(note)
|
||
if not word:
|
||
raise InvalidWordException
|
||
|
||
if config.ignore_accents:
|
||
word = strip_combining(word)
|
||
|
||
# progress.update_title(u'Querying [[ %s ]]' % word)
|
||
|
||
services = {}
|
||
tasks = []
|
||
for i, each in enumerate(maps):
|
||
if i == word_ord:
|
||
continue
|
||
if i == len(note.fields):
|
||
break
|
||
#ignore field
|
||
ignore = each.get('ignore', False)
|
||
if ignore:
|
||
continue
|
||
#skip valued
|
||
skip = each.get('skip_valued', False)
|
||
if skip and len(note.fields[i]) != 0:
|
||
continue
|
||
#cloze
|
||
cloze = each.get('cloze_word', False)
|
||
#normal
|
||
dict_unique = each.get('dict_unique', '').strip()
|
||
dict_fld_ord = each.get('dict_fld_ord', -1)
|
||
fld_ord = each.get('fld_ord', -1)
|
||
if dict_unique and dict_fld_ord != -1 and fld_ord != -1:
|
||
if fileds is None or fld_ord in fileds:
|
||
s = services.get(dict_unique, None)
|
||
if s is None:
|
||
s = service_pool.get(dict_unique)
|
||
if s and s.support:
|
||
services[dict_unique] = s
|
||
if s and s.support:
|
||
tasks.append({
|
||
'k': dict_unique,
|
||
'w': word,
|
||
'f': dict_fld_ord,
|
||
'i': fld_ord,
|
||
'cloze': cloze,
|
||
})
|
||
|
||
success_num = 0
|
||
result = defaultdict(QueryResult)
|
||
for task in tasks:
|
||
#try:
|
||
service = services.get(task['k'], None)
|
||
qr = service.active(task['f'], task['w'])
|
||
if qr:
|
||
if task['cloze']:
|
||
qr['result'] = cloze_deletion(qr['result'], word)
|
||
result.update({task['i']: qr})
|
||
success_num += 1
|
||
#except:
|
||
# showInfo(_("NO_QUERY_WORD"))
|
||
# pass
|
||
|
||
missed_css = list()
|
||
for service in services.values():
|
||
if isinstance(service, LocalService):
|
||
for css in service.missed_css:
|
||
missed_css.append({
|
||
'dict_path': service.dict_path,
|
||
'title': service.title,
|
||
'file': css
|
||
})
|
||
service_pool.put(service)
|
||
|
||
return result, -1 if len(tasks) == 0 else success_num, missed_css
|
||
|
||
|
||
def cloze_deletion(text, cloze):
|
||
'''create cloze deletion text'''
|
||
text = text.replace('’', '\'')
|
||
result = text
|
||
offset = 0
|
||
term = _stemmer.stemWord(cloze).lower()
|
||
|
||
terms = re.finditer(r"\b[\w'-]*\b", text)
|
||
tags = re.finditer(r"<[^>]+>", text)
|
||
for m in terms:
|
||
s = m.start()
|
||
e = m.end()
|
||
f = False
|
||
for tag in tags:
|
||
if s >= tag.start() and e <= tag.end():
|
||
f = True
|
||
break
|
||
if f:
|
||
continue
|
||
word = text[s:e]
|
||
if _stemmer.stemWord(word).lower() == term:
|
||
l = len(cloze)
|
||
w = word
|
||
if w[:l].lower() == cloze.lower():
|
||
e = s + l
|
||
w = word[:l]
|
||
result = result[:s+offset] + "{{c1::" + w + "}}" + result[e+offset:]
|
||
offset += 8
|
||
return result
|
||
|
||
_stemmer = stemmer('english')
|