Files
esl-reader/lib/words.py
Christopher T. Johnson ad5904f3ae checkpoint
2024-04-09 11:45:56 -04:00

394 lines
13 KiB
Python

import importlib
import pkgutil
import json
import re
from typing import Any, TypedDict, cast
from PyQt6.QtCore import (
QUrl,
Qt,
pyqtSlot,
)
from PyQt6.QtGui import (
QColor,
)
from PyQt6.QtSql import QSqlQuery
from PyQt6.QtWidgets import QScrollArea
from lib.utils import query_error, Resources
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 WordType(TypedDict):
word: str
source: str
definition: str
class Word:
"""All processing of a dictionary word."""
_words: dict[str, WordType] = {}
def __init__(self, word: str) -> None:
#
# Have we already retrieved this word?
#
try:
self.current = 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
@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]:
src = self.current['source']
try:
lines = discovered_plugins[src].getDef(self.current["definition"])
return lines
except KeyError:
raise Exception(f"Unknown source: {self.current['source']}")
def mw_seq(self, seq: list[Any]) -> list[Line]:
r=Resources()
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} ",
r.boldFont,
color=r.baseColor
)
outer = " "
line.addFragment(frag)
frag = Fragment(
text,
r.italicFont,
color=r.baseColor
)
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} ",
r.boldFont,
color=r.baseColor
)
outer = " "
frag.setLeft(10)
line.addFragment(frag)
frag = Fragment(
dt[1],
r.textFont,
color=r.baseColor
)
frag.setLeft(30)
line.addFragment(frag)
lines.append(line)
elif dt[0] == "vis":
for vis in dt[1]:
line = Line()
frag = Fragment(
f" ",
r.boldFont
)
frag.setLeft(45)
line.addFragment(frag)
line.addFragment(
Fragment(
vis["t"],
r.textFont,
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],
r.textFont,
color=r.baseColor
)
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]:
r = Resources()
#
# Easy reference to colors
#
lines: list[Line] = []
line = Line()
hw = re.sub(r"\*", "", entry["hwi"]["hw"])
frag = Fragment(hw, r.headerFont, color=r.baseColor)
line.addFragment(frag)
frag = Fragment(
" " + entry["fl"], r.labelFont, color=r.linkColor
)
line.addFragment(frag)
lines.append(line)
if "vrs" in entry.keys():
line = Line()
space = ""
for vrs in entry["vrs"]:
frag = Fragment(
space + vrs["va"],
r.labelFont,
color=r.baseColor
)
space = " "
line.addFragment(frag)
lines.append(line)
if "prs" in entry["hwi"]:
line = Line()
frag = Fragment(
entry["hwi"]["hw"] + " ",
r.phonicFont,
color=r.baseColor,
)
line.addFragment(frag)
for prs in entry["hwi"]["prs"]:
audio = None
if audio is None:
audio = ""
frag = Fragment(
prs["mw"], r.phonicFont, color=r.linkColor
)
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"], r.textFont, color=r.baseColor
)
line.addFragment(frag)
space = " "
except KeyError:
pass
frag = Fragment(
space + ins["if"],
r.boldFont,
color=r.baseColor
)
line.addFragment(frag)
space = "; "
lines.append(line)
if "lbs" in entry.keys():
line = Line()
frag = Fragment(
"; ".join(entry["lbs"]),
r.boldFont,
color=r.baseColor
)
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:
rr = self.mw_seq(seq)
lines += rr
elif k == "vd":
line = Line()
line.addFragment(
Fragment(
v, r.italicFont, color=r.linkColor
)
)
lines.append(line)
return lines
def mw_html(self) -> str:
#
# Create the header, base word and its label
#
word = self.current['definition']["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 = QUrl()
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