448 lines
15 KiB
Python
448 lines
15 KiB
Python
import importlib
|
|
import pkgutil
|
|
import json
|
|
import re
|
|
from typing import Any, Dict, cast
|
|
|
|
from PyQt6.QtCore import (
|
|
Qt,
|
|
pyqtSlot,
|
|
)
|
|
from PyQt6.QtGui import (
|
|
QColor,
|
|
QFont,
|
|
QFontDatabase,
|
|
)
|
|
from PyQt6.QtNetwork import QNetworkAccessManager
|
|
from PyQt6.QtSql import QSqlQuery
|
|
from PyQt6.QtWidgets import QScrollArea
|
|
|
|
from lib import query_error
|
|
from lib.sounds import SoundOff
|
|
from lib.definition import Definition, Line, Fragment
|
|
|
|
import plugins
|
|
def find_plugins(ns_pkg):
|
|
return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + '.')
|
|
|
|
discovered_plugins = {
|
|
# finder, name, ispkg
|
|
importlib.import_module(name).registration['source']: importlib.import_module(name) for _, name, _ in find_plugins(plugins)
|
|
}
|
|
|
|
API = "https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
|
|
|
|
|
|
class Word:
|
|
"""All processing of a dictionary word."""
|
|
|
|
_words: dict[str, Any] = {}
|
|
_resources: Dict[str, Any] = {}
|
|
_nam = QNetworkAccessManager()
|
|
def __init__(self, word: str) -> None:
|
|
Word.set_resources()
|
|
#
|
|
# Have we already retrieved this word?
|
|
#
|
|
try:
|
|
self.current = json.loads(Word._words[word])
|
|
return
|
|
except KeyError:
|
|
pass
|
|
query = QSqlQuery()
|
|
query.prepare("SELECT * FROM words " "WHERE word = :word")
|
|
query.bindValue(":word", word)
|
|
if not query.exec():
|
|
query_error(query)
|
|
if query.next():
|
|
Word._words[word] = {
|
|
"word": word,
|
|
"source": query.value("source"),
|
|
"definition": json.loads(query.value("definition")),
|
|
}
|
|
self.current = Word._words[word]
|
|
return
|
|
#
|
|
# The code should look at our settings to see if we have an API
|
|
# key for MW to decide on the source to use.
|
|
#
|
|
source = "mw"
|
|
|
|
self._words[word] = discovered_plugins[source].fetch(word)
|
|
self.current = Word._words[word]
|
|
query.prepare(
|
|
"INSERT INTO words "
|
|
"(word, source, definition) "
|
|
"VALUES (:word, :source, :definition)"
|
|
)
|
|
query.bindValue(":word", self.current["word"])
|
|
query.bindValue(":source", self.current["source"])
|
|
query.bindValue(":definition", json.dumps(self.current["definition"]))
|
|
if not query.exec():
|
|
query_error(query)
|
|
return
|
|
|
|
@classmethod
|
|
def set_resources(cls) -> None:
|
|
if len(cls._resources.keys()) > 0:
|
|
return
|
|
#
|
|
# Colors we used
|
|
#
|
|
headerFont = QFontDatabase.font("OpenDyslexic", None, 10)
|
|
headerFont.setPixelSize(48)
|
|
labelFont = QFont(headerFont)
|
|
labelFont.setPixelSize(30)
|
|
boldFont = QFont(headerFont)
|
|
boldFont.setPixelSize(20)
|
|
textFont = QFont(boldFont)
|
|
italicFont = QFont(boldFont)
|
|
capsFont = QFont(boldFont)
|
|
smallCapsFont = QFont(boldFont)
|
|
|
|
headerFont.setWeight(QFont.Weight.Bold)
|
|
boldFont.setBold(True)
|
|
italicFont.setItalic(True)
|
|
capsFont.setCapitalization(QFont.Capitalization.AllUppercase)
|
|
smallCapsFont.setCapitalization(QFont.Capitalization.SmallCaps)
|
|
|
|
phonicFont = QFontDatabase.font("Gentium", None, 10)
|
|
phonicFont.setPixelSize(20)
|
|
|
|
cls._resources = {
|
|
"colors": {
|
|
"base": QColor(Qt.GlobalColor.white),
|
|
"link": QColor("#4a7d95"),
|
|
"subdued": QColor(Qt.GlobalColor.gray),
|
|
},
|
|
"fonts": {
|
|
"header": headerFont,
|
|
"label": labelFont,
|
|
"phonic": phonicFont,
|
|
"bold": boldFont,
|
|
"italic": italicFont,
|
|
"text": textFont,
|
|
"caps": capsFont,
|
|
"smallCaps": smallCapsFont,
|
|
},
|
|
}
|
|
@pyqtSlot()
|
|
def playSound(self) -> None:
|
|
url = discovered_plugins[self.current['source']].getFirstSound(self.current['definition'])
|
|
if url.isValid():
|
|
snd = SoundOff()
|
|
snd.playSound(url)
|
|
return
|
|
|
|
def getWord(self) -> str:
|
|
return cast(str, self.current["word"])
|
|
|
|
def get_html(self) -> str | None:
|
|
src = self.current['source']
|
|
try:
|
|
return discovered_plugins[src].getHtml(self.current)
|
|
except KeyError:
|
|
raise Exception(f"Unknown source: {src}")
|
|
|
|
def get_def(self) -> list[Line]:
|
|
if len(self._lines) > 0:
|
|
return self._lines
|
|
src = self.current['source']
|
|
try:
|
|
return discovered_plugins[src].getDef(self.current)
|
|
except KeyError:
|
|
raise Exception(f"Unknown source: {self.current['source']}")
|
|
|
|
def mw_def(self) -> list[Line]:
|
|
lines: list[Line] = []
|
|
# print(json.dumps(self.current,indent=2))
|
|
for entry in self.current["definition"]:
|
|
lines += self.mw_def_entry(entry)
|
|
self._lines = lines
|
|
return lines
|
|
|
|
def mw_seq(self, seq: list[Any]) -> list[Line]:
|
|
lines: list[Line] = []
|
|
outer = " "
|
|
inner = " "
|
|
for value in seq:
|
|
if value[0] == 'pseq':
|
|
continue
|
|
print(value[0])
|
|
print(value[1])
|
|
sense = value[1]
|
|
#
|
|
# The optional 'sn' field tells us what sort of labeling to do
|
|
#
|
|
sn = sense.get("sn", "")
|
|
sns = sn.split(" ")
|
|
if len(sns) == 2:
|
|
outer = sns[0]
|
|
inner = sns[1]
|
|
elif len(sns) == 1:
|
|
if inner == " ":
|
|
outer = sns[0]
|
|
else:
|
|
inner = sns[0]
|
|
try:
|
|
text = ", ".join(sense["sls"])
|
|
line = Line()
|
|
frag = Fragment(
|
|
f"{outer} {inner} ",
|
|
self._resources["fonts"]["bold"],
|
|
color=self._resources["colors"]["base"],
|
|
)
|
|
outer = " "
|
|
line.addFragment(frag)
|
|
frag = Fragment(
|
|
text,
|
|
self._resources["fonts"]["italic"],
|
|
color=self._resources["colors"]["base"],
|
|
)
|
|
frag.setLeft(30)
|
|
line.addFragment(frag)
|
|
except KeyError:
|
|
pass
|
|
try:
|
|
for dt in sense["dt"]:
|
|
if dt[0] == "text":
|
|
line = Line()
|
|
frag = Fragment(
|
|
f"{outer} {inner} ",
|
|
self._resources["fonts"]["bold"],
|
|
color=self._resources["colors"]["base"],
|
|
)
|
|
outer = " "
|
|
frag.setLeft(10)
|
|
line.addFragment(frag)
|
|
frag = Fragment(
|
|
dt[1],
|
|
self._resources["fonts"]["text"],
|
|
color=self._resources["colors"]["base"],
|
|
)
|
|
frag.setLeft(30)
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
elif dt[0] == "vis":
|
|
for vis in dt[1]:
|
|
line = Line()
|
|
frag = Fragment(
|
|
f" ",
|
|
self._resources["fonts"]["bold"],
|
|
)
|
|
frag.setLeft(45)
|
|
line.addFragment(frag)
|
|
line.addFragment(
|
|
Fragment(
|
|
vis["t"],
|
|
self._resources["fonts"]["text"],
|
|
color=QColor("#aaa"),
|
|
)
|
|
)
|
|
lines.append(line)
|
|
elif dt[0] == "uns":
|
|
for uns in dt[1]:
|
|
for seg in uns:
|
|
if seg[0] == "text":
|
|
try:
|
|
line = lines.pop()
|
|
except IndexError:
|
|
line = Line()
|
|
frag = Fragment(
|
|
"\u27F6 " + seg[1],
|
|
self._resources["fonts"]["text"],
|
|
color=self._resources["colors"]["base"],
|
|
)
|
|
frag.setLeft(30)
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
elif dt[0] == 'ca':
|
|
continue
|
|
else:
|
|
raise Exception(f"Unknown key {dt[0]} in {sense['dt']}")
|
|
except KeyError:
|
|
pass
|
|
return lines
|
|
|
|
def mw_def_entry(self, entry: dict[str, Any]) -> list[Line]:
|
|
#
|
|
# Easy reference to colors
|
|
#
|
|
base = self._resources["colors"]["base"]
|
|
blue = self._resources["colors"]["blue"]
|
|
|
|
lines: list[Line] = []
|
|
line = Line()
|
|
hw = re.sub(r"\*", "", entry["hwi"]["hw"])
|
|
frag = Fragment(hw, self._resources["fonts"]["header"], color=base)
|
|
line.addFragment(frag)
|
|
frag = Fragment(
|
|
" " + entry["fl"], self._resources["fonts"]["label"], color=blue
|
|
)
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
|
|
if "vrs" in entry.keys():
|
|
line = Line()
|
|
space = ""
|
|
for vrs in entry["vrs"]:
|
|
frag = Fragment(
|
|
space + vrs["va"],
|
|
self._resources["fonts"]["label"],
|
|
color=base,
|
|
)
|
|
space = " "
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
if "prs" in entry["hwi"]:
|
|
line = Line()
|
|
frag = Fragment(
|
|
entry["hwi"]["hw"] + " ",
|
|
self._resources["fonts"]["phonic"],
|
|
color=base,
|
|
)
|
|
line.addFragment(frag)
|
|
for prs in entry["hwi"]["prs"]:
|
|
audio = self.mw_sound_url(prs)
|
|
if audio is None:
|
|
audio = ""
|
|
frag = Fragment(
|
|
prs["mw"], self._resources["fonts"]["phonic"], color=blue
|
|
)
|
|
frag.setAudio(audio)
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
if "ins" in entry.keys():
|
|
line = Line()
|
|
space = ""
|
|
for ins in entry["ins"]:
|
|
try:
|
|
frag = Fragment(
|
|
ins["il"], self._resources["fonts"]["text"], color=base
|
|
)
|
|
line.addFragment(frag)
|
|
space = " "
|
|
except KeyError:
|
|
pass
|
|
frag = Fragment(
|
|
space + ins["if"],
|
|
self._resources["fonts"]["bold"],
|
|
color=base,
|
|
)
|
|
line.addFragment(frag)
|
|
space = "; "
|
|
lines.append(line)
|
|
if "lbs" in entry.keys():
|
|
line = Line()
|
|
frag = Fragment(
|
|
"; ".join(entry["lbs"]),
|
|
self._resources["fonts"]["bold"],
|
|
color=base,
|
|
)
|
|
line.addFragment(frag)
|
|
lines.append(line)
|
|
for value in entry["def"]: # has multiple 'sseg' or 'vd' init
|
|
for k, v in value.items():
|
|
if k == "sseq": # has multiple 'senses'
|
|
for seq in v:
|
|
r = self.mw_seq(seq)
|
|
lines += r
|
|
elif k == "vd":
|
|
line = Line()
|
|
line.addFragment(
|
|
Fragment(
|
|
v, self._resources["fonts"]["italic"], color=blue
|
|
)
|
|
)
|
|
lines.append(line)
|
|
return lines
|
|
|
|
def mw_html(self) -> str:
|
|
#
|
|
# Create the header, base word and its label
|
|
#
|
|
word = self.current["hwi"]["hw"]
|
|
label = self.current["fl"]
|
|
html = f'<h1 class="def-word">{word} <span class="def-label">{label}</span></h1>\n'
|
|
|
|
#
|
|
# If there are variants, then add them in an unordered list.
|
|
# CSS will make it pretty
|
|
#
|
|
if "vrs" in self.current.keys():
|
|
html += "<ul class=\"def-vrs'>\n"
|
|
html += "<li>"
|
|
html += "</li>\n<li>".join(
|
|
[vrs["va"] for vrs in self.current["vrs"]]
|
|
)
|
|
html += "</li>\n</ul>\n"
|
|
|
|
#
|
|
# If there is a pronunciation section, create it
|
|
#
|
|
if "prs" in self.current["hwi"].keys():
|
|
tmp = []
|
|
for prs in self.current["hwi"]["prs"]:
|
|
url = self.mw_sound_url(prs)
|
|
how = prs["mw"]
|
|
if url:
|
|
tmp.append(f'<a href="{url}">\\{how}\\</a>')
|
|
else:
|
|
tmp.append(f"\\{how}\\")
|
|
html += '<span class="def-phonetic">'
|
|
html += '</span><span="def-phonetic">'.join(tmp)
|
|
html += "</span>\n"
|
|
|
|
#
|
|
# If there are inflections, create a header for that.
|
|
#
|
|
if "ins" in self.current.keys():
|
|
html += '<h2 class="def-word">'
|
|
html += ", ".join([ins["if"] for ins in self.current["ins"]])
|
|
html += "</h2>\n"
|
|
|
|
#
|
|
# Start creating the definition section
|
|
#
|
|
html += "<ul class='def-outer'>\n"
|
|
for meaning in self.current["def"]:
|
|
html += f"<li>{meaning['vd']}\n"
|
|
html += '<ul class="def-inner">\n'
|
|
label = ""
|
|
for sseq in meaning["sseq"]:
|
|
for sense in sseq:
|
|
label = sense[1]["sn"]
|
|
sls = ""
|
|
if "sls" in sense[1].keys():
|
|
sls = ", ".join(sense[1]["sls"])
|
|
sls = f'<span class="def-sls">{sls}</span> '
|
|
for dt in sense[1]["dt"]:
|
|
if dt[0] == "text":
|
|
html += f'<li class="def-text"><span class="def-sn">{label}</span>{sls}{dt[1]}</li>\n'
|
|
elif dt[0] == "vis":
|
|
for vis in dt[1]:
|
|
html += (
|
|
f"<li class=\"def-vis\">{vis['t']}</li>\n"
|
|
)
|
|
else:
|
|
print(f"Do something with {dt[0]}")
|
|
html += "</ul>\n"
|
|
html += "</ul>\n"
|
|
return html
|
|
|
|
def apidictionary_html(self) -> str:
|
|
html = ""
|
|
return html
|
|
|
|
|
|
|
|
|
|
class DefinitionArea(QScrollArea):
|
|
def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None:
|
|
super(DefinitionArea, self).__init__(*args, *kwargs)
|
|
d = Definition(w)
|
|
self.setWidget(d)
|
|
self.setWidgetResizable(True)
|
|
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
|
return
|