diff --git a/lib/words.py b/lib/words.py index 8c0f614..b85fc94 100644 --- a/lib/words.py +++ b/lib/words.py @@ -1,10 +1,10 @@ import copy import json import re -from typing import Any, Optional, Self +from typing import Any, Optional import requests -from PyQt6.QtCore import QPoint, QRect, QSize, QUrl, Qt, pyqtSignal +from PyQt6.QtCore import QPoint, QRect, QUrl, Qt, pyqtSignal from PyQt6.QtGui import ( QBrush, QColor, @@ -19,231 +19,227 @@ from PyQt6.QtGui import ( QTransform, ) from PyQt6.QtSql import QSqlQuery -from PyQt6.QtWidgets import QScrollArea, QScrollBar, QSizePolicy, QWidget +from PyQt6.QtWidgets import QScrollArea, QWidget from lib import query_error +class Fragment: + """A structure to hold typed values of a fragment.""" + + _type: str # Function of this fragment. Think text, span, button + _text: str # The simple utf-8 text + _content: list['Fragment'] + _audio: QUrl # Optional audio URL + _font: QFont # The font, with all options, to use for this fragment + _align: QTextOption # Alignment information + _rect: QRect # The rect that contains _text + _padding: list[int] # space to add around the text + _border: list[int] # Size of the border (all the same) + _margin: list[int] # Space outside of the border + _color: QColor # the pen color + _wref: str # a word used as a 'href' + _position: QPoint # where to drawText + _borderRect: QRect # where to drawRect + _radius: int # Radius for rounded rects + _asis: bool = False + _left: int = 0 + + + TYPES = [ 'text', 'span', 'button' ] + def __init__(self, + text:str, + font:QFont, + t:str = 'text', + audio:str = '', + color: Optional[QColor] = None, + asis: bool = False, + ) -> None: + if t not in self.TYPES: + raise Exception(f"Unknown fragment type{t}") + self._type = t + self._text = text + self._font = font + self._audio = QUrl(audio) + self._align = QTextOption( + Qt.AlignmentFlag.AlignLeft + | Qt.AlignmentFlag.AlignBaseline + ) + self._padding = [0, 0, 0, 0] + self._border = [0, 0, 0, 0] + self._margin = [0, 0, 0, 0] + self._wref = '' + self._position = QPoint() + self._borderRect = QRect() + if color: + self._color = color + else: + self._color = QColor() + self._asis = asis + return + def __str__(self) -> str: + return self._text + # + # Setters + # + def setType(self, t:str) -> None: + if t not in self.TYPES: + raise Exception(f"Unknown fragment type{t}") + self._type = t + return + def setText(self, text:str) -> None: + self._text = text + return + def setFont(self, font:QFont) -> None: + self._font = font + return + def setAudio(self, audio:str) -> None: + self._audio = QUrl(audio) + return + def setAlign(self, align:QTextOption) -> None: + self._align = align + return + def setRect(self,rect:QRect) -> None: + self._rect = rect + return + def setPadding(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None: + if top > -1 or right > -1 or bottom > -1 or left > -1: + if top >= 0: + self._padding[0] = top + if right >= 0: + self._padding[1] = right + if bottom >= 0: + self._padding[2] = bottom + if left >= 0: + self._padding[3] = left + return + if len(args) == 4: + self._padding = [args[0], args[1], args[2], args[3]] + elif len(args) == 3: + self._padding = [args[0], args[1], args[2], args[1]] + elif len(args) == 2: + self._padding = [args[0], args[1], args[0], args[1]] + elif len(args) == 1: + self._padding = [args[0], args[0], args[0], args[0]] + else: + raise Exception("argument error") + return + def setBorder(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None: + if top > -1 or right > -1 or bottom > -1 or left > -1: + if top >= 0: + self._border[0] = top + if right >= 0: + self._border[1] = right + if bottom >= 0: + self._border[2] = bottom + if left >= 0: + self._border[3] = left + return + if len(args) == 4: + self._border = [args[0], args[1], args[2], args[3]] + elif len(args) == 3: + self._border = [args[0], args[1], args[2], args[1]] + elif len(args) == 2: + self._border = [args[0], args[1], args[0], args[1]] + elif len(args) == 1: + self._border = [args[0], args[0], args[0], args[0]] + else: + raise Exception("argument error") + return + def setMargin(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None: + if top > -1 or right > -1 or bottom > -1 or left > -1: + if top >= 0: + self._margin[0] = top + if right >= 0: + self._margin[1] = right + if bottom >= 0: + self._margin[2] = bottom + if left >= 0: + self._margin[3] = left + return + if len(args) == 4: + self._margin = [args[0], args[1], args[2], args[3]] + elif len(args) == 3: + self._margin = [args[0], args[1], args[2], args[1]] + elif len(args) == 2: + self._margin = [args[0], args[1], args[0], args[1]] + elif len(args) == 1: + self._margin = [args[0], args[0], args[0], args[0]] + else: + raise Exception("argument error") + return + def setWRef(self, ref:str) -> None: + self._wref = ref + return + def setPosition(self, pnt:QPoint) -> None: + self._position = pnt + return + def setBorderRect(self, rect:QRect) -> None: + self._borderRect = rect + return + def setColor(self,color:QColor) -> None: + self._color = color + return + def setLeft(self, left:int) -> None: + self._left = left + return + # + # Getters + # + def wRef(self) -> str: + return self._wref + def type(self) -> str: + return self._type + def text(self) -> str: + return self._text + def font(self) -> QFont: + return self._font + def audio(self) -> str: + return self._audio.url() + def align(self) -> QTextOption: + return self._align + def rect(self) -> QRect: + return self._rect + def padding(self) -> list[int]: + return self._padding + def border(self) -> list[int]: + return self._border + def margin(self) -> list[int]: + return self._margin + def position(self) -> QPoint: + return self._position + def borderRect(self) -> QRect: + return self._borderRect + def color(self) -> QColor: + return self._color + def asis(self) -> bool: + return self._asis + def left(self) -> int: + return self._left + + API = "https://api.dictionaryapi.dev/api/v2/entries/en/{word}" MWAPI = "https://www.dictionaryapi.com/api/v3/references/collegiate/json/{word}?key=51d9df34-ee13-489e-8656-478c215e846c" class Word: - _instance = None + """All processing of a dictionary word.""" + + _words: dict[str, Any] = {} - _current: dict[str, Any] = {} - _currentWord: str - class Fragment: - """A structure to hold typed values of a fragment.""" - - _type: str # Function of this fragment. Think text, span, button - _text: str # The simple utf-8 text - _content: list['Word.Fragment'] - _audio: QUrl # Optional audio URL - _font: QFont # The font, with all options, to use for this fragment - _align: QTextOption # Alignment information - _rect: QRect # The rect that contains _text - _padding: list[int] # space to add around the text - _border: list[int] # Size of the border (all the same) - _margin: list[int] # Space outside of the border - _color: QColor # the pen color - _wref: str # a word used as a 'href' - _position: QPoint # where to drawText - _borderRect: QRect # where to drawRect - _radius: int # Radius for rounded rects - _asis: bool = False - _left: int = 0 - - - TYPES = [ 'text', 'span', 'button' ] - def __init__(self, - text:str, - font:QFont, - t:str = 'text', - audio:str = '', - color: Optional[QColor] = None, - asis: bool = False, - ) -> None: - if t not in self.TYPES: - raise Exception(f"Unknown fragment type{t}") - self._type = t - self._text = text - self._font = font - self._audio = QUrl(audio) - self._align = QTextOption( - Qt.AlignmentFlag.AlignLeft - | Qt.AlignmentFlag.AlignBaseline - ) - self._padding = [0, 0, 0, 0] - self._border = [0, 0, 0, 0] - self._margin = [0, 0, 0, 0] - self._wref = '' - self._position = QPoint() - self._borderRect = QRect() - if color: - self._color = color - else: - self._color = QColor() - self._asis = asis - return - def __str__(self) -> str: - return self._text - # - # Setters - # - def setType(self, t:str) -> None: - if t not in self.TYPES: - raise Exception(f"Unknown fragment type{t}") - self._type = t - return - def setText(self, text:str) -> None: - self._text = text - return - def setFont(self, font:QFont) -> None: - self._font = font - return - def setAudio(self, audio:str) -> None: - self._audio = QUrl(audio) - return - def setAlign(self, align:QTextOption) -> None: - self._align = align - return - def setRect(self,rect:QRect) -> None: - self._rect = rect - return - def setPadding(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None: - if top > -1 or right > -1 or bottom > -1 or left > -1: - if top >= 0: - self._padding[0] = top - if right >= 0: - self._padding[1] = right - if bottom >= 0: - self._padding[2] = bottom - if left >= 0: - self._padding[3] = left - return - if len(args) == 4: - self._padding = [args[0], args[1], args[2], args[3]] - elif len(args) == 3: - self._padding = [args[0], args[1], args[2], args[1]] - elif len(args) == 2: - self._padding = [args[0], args[1], args[0], args[1]] - elif len(args) == 1: - self._padding = [args[0], args[0], args[0], args[0]] - else: - raise Exception("argument error") - return - def setBorder(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None: - if top > -1 or right > -1 or bottom > -1 or left > -1: - if top >= 0: - self._border[0] = top - if right >= 0: - self._border[1] = right - if bottom >= 0: - self._border[2] = bottom - if left >= 0: - self._border[3] = left - return - if len(args) == 4: - self._border = [args[0], args[1], args[2], args[3]] - elif len(args) == 3: - self._border = [args[0], args[1], args[2], args[1]] - elif len(args) == 2: - self._border = [args[0], args[1], args[0], args[1]] - elif len(args) == 1: - self._border = [args[0], args[0], args[0], args[0]] - else: - raise Exception("argument error") - return - def setMargin(self, top:int = -1, right:int = -1, bottom:int = -1, left:int = -1, *args:int) -> None: - if top > -1 or right > -1 or bottom > -1 or left > -1: - if top >= 0: - self._margin[0] = top - if right >= 0: - self._margin[1] = right - if bottom >= 0: - self._margin[2] = bottom - if left >= 0: - self._margin[3] = left - return - if len(args) == 4: - self._margin = [args[0], args[1], args[2], args[3]] - elif len(args) == 3: - self._margin = [args[0], args[1], args[2], args[1]] - elif len(args) == 2: - self._margin = [args[0], args[1], args[0], args[1]] - elif len(args) == 1: - self._margin = [args[0], args[0], args[0], args[0]] - else: - raise Exception("argument error") - return - def setWRef(self, ref:str) -> None: - self._wref = ref - return - def setPosition(self, pnt:QPoint) -> None: - self._position = pnt - return - def setBorderRect(self, rect:QRect) -> None: - self._borderRect = rect - return - def setColor(self,color:QColor) -> None: - self._color = color - return - def setLeft(self, left:int) -> None: - self._left = left - return - # - # Getters - # - def wRef(self) -> str: - return self._wref - def type(self) -> str: - return self._type - def text(self) -> str: - return self._text - def font(self) -> QFont: - return self._font - def audio(self) -> str: - return self._audio.url() - def align(self) -> QTextOption: - return self._align - def rect(self) -> QRect: - return self._rect - def padding(self) -> list[int]: - return self._padding - def border(self) -> list[int]: - return self._border - def margin(self) -> list[int]: - return self._margin - def position(self) -> QPoint: - return self._position - def borderRect(self) -> QRect: - return self._borderRect - def color(self) -> QColor: - return self._color - def asis(self) -> bool: - return self._asis - def left(self) -> int: - return self._left - class Line: - _maxHeight: int - _leading: int - _baseLine: int - _fragments: list['Word.Fragment'] - def __init__(self) -> None: self._maxHeight = -1 self._baseLine = -1 self._leading = -1 - self._fragments = [] + self._fragments:list[Fragment] = [] return def __repr__(self) -> str: return '|'.join([x.text() for x in self._fragments])+f'|{self._maxHeight}' - def parseText(self, frag: 'Word.Fragment') -> list['Word.Fragment']: + def parseText(self, frag: Fragment) -> list[Fragment]: org = frag.text() if frag.asis(): return [frag] @@ -259,7 +255,7 @@ class Word: script = QFont(frag.font()) script.setPixelSize(int(script.pixelSize()/4)) - results: list['Word.Fragment'] = [] + results: list[Fragment] = [] while True: text = frag.text() start = text.find('{') @@ -292,7 +288,7 @@ class Word: newFrag = copy.copy(frag) oldFont = QFont(frag.font()) if token == 'bc': - results.append(Word.Fragment(': ', bold, color=QColor('#fff'))) + results.append(Fragment(': ', bold, color=QColor('#fff'))) continue if token in ['b', 'inf', 'it', 'sc', 'sup', 'phrase', 'parahw', 'gloss', 'qword', 'wi', 'dx', 'dx_def', 'dx_ety', 'ma']: @@ -372,7 +368,7 @@ class Word: raise Exception(f"Unable to locate a known token {token} in {org}") - def addFragment(self, frag: 'Word.Fragment',) -> None: + def addFragment(self, frag: Fragment,) -> None: SPEAKER = "\U0001F508" if frag.audio(): @@ -459,7 +455,7 @@ class Word: x += width return - def getLine(self) -> list['Word.Fragment']: + def getLine(self) -> list[Fragment]: return self._fragments def getLeading(self) -> int: @@ -467,26 +463,13 @@ class Word: _lines: list[Line] = [] - def __new__(cls: type[Self], _: str) -> Self: # flycheck: ignore - if cls._instance: - return cls._instance - cls._instance = super(Word, cls).__new__(cls) - return cls._instance - def __init__(self, word: str) -> None: - # - # reset the current definition - # - try: - if word != self._current['word']: - self._lines = [] - except KeyError: - pass + self.resources = {} # # Have we already retrieved this word? # try: - self._current = json.loads(self._words[word]) + self.current = json.loads(Word._words[word]) return except KeyError: pass @@ -496,17 +479,17 @@ class Word: if not query.exec(): query_error(query) if query.next(): - self._words[word] = { + Word._words[word] = { 'word': word, 'source': query.value('source'), 'definition': json.loads(query.value("definition")), } - self._current = self._words[word] + self.current = Word._words[word] return source = 'mw' response = requests.get(MWAPI.format(word=word)) if response.status_code != 200: - self._current = {} + self.current = {} return data = json.loads(response.content.decode("utf-8")) print(data) @@ -515,35 +498,34 @@ class Word: 'source': source, 'definition': data, } - self._current = self._words[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'])) + 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 - def getCurrent(self) -> str: - return self._current['word'] + def getWord(self) -> str: + return self.current['word'] def get_html(self) -> str | None: - if self._current['source'] == 'mw': + if self.current['source'] == 'mw': return self.mw_html() - elif self._current['source'] == 'apidictionary': + elif self.current['source'] == 'apidictionary': return self.apidictionary_html() else: - raise Exception(f"Unknown source: {self._current['source']}") + raise Exception(f"Unknown source: {self.current['source']}") - _resources:dict[str,Any] = {} def get_def(self) -> list[Line] | None: if len(self._lines) > 0: return self._lines - if len(self._resources.keys()) < 1: + if len(self.resources.keys()) < 1: # # Colors we used # @@ -563,7 +545,7 @@ class Word: phonicFont = QFontDatabase.font("Gentium", None, 10) phonicFont.setPixelSize(20) - self._resources = { + self.resources = { 'colors': { 'base':QColor(Qt.GlobalColor.white), 'blue': QColor("#4a7d95"), @@ -577,22 +559,22 @@ class Word: 'text': textFont, } } - if self._current['source'] == 'mw': + if self.current['source'] == 'mw': return self.mw_def() - elif self._current['source'] == 'apidictionary': + elif self.current['source'] == 'apidictionary': return None else: - raise Exception(f"Unknown source: {self._current['source']}") + raise Exception(f"Unknown source: {self.current['source']}") def mw_def(self) -> list[Line]: lines: list[Word.Line] = [] - for entry in self._current['definition']: + for entry in self.current['definition']: line = Word.Line() meta = json.dumps(entry['meta']) line.addFragment( - Word.Fragment( + Fragment( meta, - self._resources['fonts']['text'],asis=True + self.resources['fonts']['text'],asis=True ) ) lines.append(line) @@ -623,18 +605,18 @@ class Word: for dt in sense['dt']: if dt[0] == 'text': line = Word.Line() - frag = Word.Fragment( + frag = Fragment( f"{outer} {inner} ", - self._resources['fonts']['bold'], - color = self._resources['colors']['base'] + self.resources['fonts']['bold'], + color = self.resources['colors']['base'] ) outer = ' ' frag.setLeft(10) line.addFragment(frag) - frag = Word.Fragment( + frag = Fragment( dt[1], - self._resources['fonts']['text'], - color = self._resources['colors']['base'] + self.resources['fonts']['text'], + color = self.resources['colors']['base'] ) frag.setLeft(30) line.addFragment(frag) @@ -642,14 +624,14 @@ class Word: elif dt[0] == 'vis': for vis in dt[1]: line = Word.Line() - frag =Word.Fragment(f" ", - self._resources['fonts']['bold'], + frag =Fragment(f" ", + self.resources['fonts']['bold'], ) frag.setLeft(45) line.addFragment(frag) line.addFragment( - Word.Fragment(vis['t'], - self._resources['fonts']['text'], + Fragment(vis['t'], + self.resources['fonts']['text'], color = QColor('#aaa') ) ) @@ -659,15 +641,15 @@ class Word: # # Easy reference to colors # - base = self._resources['colors']['base'] - blue = self._resources['colors']['blue'] + base = self.resources['colors']['base'] + blue = self.resources['colors']['blue'] lines: list[Word.Line] = [] line = Word.Line() hw = re.sub(r'\*', '', entry['hwi']['hw']) - frag = Word.Fragment(hw, self._resources['fonts']['header'], color=base) + frag = Fragment(hw, self.resources['fonts']['header'], color=base) line.addFragment(frag) - frag = Word.Fragment(' '+entry["fl"], self._resources['fonts']['label'], color=blue) + frag = Fragment(' '+entry["fl"], self.resources['fonts']['label'], color=blue) line.addFragment(frag) lines.append(line) @@ -675,19 +657,19 @@ class Word: line = self.Line() space = '' for vrs in entry["vrs"]: - frag = Word.Fragment(space + vrs["va"], self._resources['fonts']['label'], color=base) + frag = Fragment(space + vrs["va"], self.resources['fonts']['label'], color=base) space = ' ' line.addFragment(frag) lines.append(line) if "prs" in entry["hwi"].keys(): line = self.Line() - frag = Word.Fragment(entry['hwi']['hw'] + ' ', self._resources['fonts']['phonic'], color=base) + frag = Fragment(entry['hwi']['hw'] + ' ', self.resources['fonts']['phonic'], color=base) line.addFragment(frag) for prs in entry["hwi"]["prs"]: audio = self.sound_url(prs) if audio is None: audio = "" - frag = Word.Fragment(prs['mw'], self._resources['fonts']['phonic'], color=blue) + frag = Fragment(prs['mw'], self.resources['fonts']['phonic'], color=blue) frag.setAudio(audio) frag.setPadding(0,10,3,12) frag.setBorder(1) @@ -699,18 +681,18 @@ class Word: space = '' for ins in entry['ins']: try: - frag = Word.Fragment(ins['il'], self._resources['fonts']['text'], color=base) + frag = Fragment(ins['il'], self.resources['fonts']['text'], color=base) line.addFragment(frag) space = ' ' except KeyError: pass - frag = Word.Fragment(space + ins['if'], self._resources['fonts']['bold'], color=base) + frag = Fragment(space + ins['if'], self.resources['fonts']['bold'], color=base) line.addFragment(frag) space = '; ' lines.append(line) if 'lbs' in entry.keys(): line = self.Line() - frag = Word.Fragment('; '.join(entry['lbs']), self._resources['fonts']['bold'], color=base) + 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 @@ -721,9 +703,9 @@ class Word: lines += r elif k == 'vd': line = self.Line() - line.addFragment(Word.Fragment( + line.addFragment(Fragment( v, - self._resources['fonts']['italic'], + self.resources['fonts']['italic'], color=blue )) lines.append(line) @@ -749,28 +731,28 @@ class Word: # # Create the header, base word and its label # - word = self._current["hwi"]["hw"] - label = self._current["fl"] + 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(): + if "vrs" in self.current.keys(): html += "\n" # # If there is a pronunciation section, create it # - if "prs" in self._current["hwi"].keys(): + if "prs" in self.current["hwi"].keys(): tmp = [] - for prs in self._current["hwi"]["prs"]: + for prs in self.current["hwi"]["prs"]: url = self.sound_url(prs) how = prs["mw"] if url: @@ -784,16 +766,16 @@ class Word: # # If there are inflections, create a header for that. # - if "ins" in self._current.keys(): + if "ins" in self.current.keys(): html += '

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

\n" # # Start creating the definition section # html += "