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'

{word} {label}

\n' # # If there are variants, then add them in an unordered list. # CSS will make it pretty # if "vrs" in self.current.keys(): html += "\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'\\{how}\\') else: tmp.append(f"\\{how}\\") html += '' html += ''.join(tmp) html += "\n" # # If there are inflections, create a header for that. # if "ins" in self.current.keys(): html += '

' html += ", ".join([ins["if"] for ins in self.current["ins"]]) html += "

\n" # # Start creating the definition section # html += "\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