Move Fragment out of Word

This commit is contained in:
Christopher T. Johnson
2024-03-27 16:20:24 -04:00
parent 3dcbd5f78d
commit c46ee65662

View File

@@ -1,10 +1,10 @@
import copy import copy
import json import json
import re import re
from typing import Any, Optional, Self from typing import Any, Optional
import requests 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 ( from PyQt6.QtGui import (
QBrush, QBrush,
QColor, QColor,
@@ -19,26 +19,16 @@ from PyQt6.QtGui import (
QTransform, QTransform,
) )
from PyQt6.QtSql import QSqlQuery from PyQt6.QtSql import QSqlQuery
from PyQt6.QtWidgets import QScrollArea, QScrollBar, QSizePolicy, QWidget from PyQt6.QtWidgets import QScrollArea, QWidget
from lib import query_error from lib import query_error
API = "https://api.dictionaryapi.dev/api/v2/entries/en/{word}" class Fragment:
MWAPI = "https://www.dictionaryapi.com/api/v3/references/collegiate/json/{word}?key=51d9df34-ee13-489e-8656-478c215e846c"
class Word:
_instance = None
_words: dict[str, Any] = {}
_current: dict[str, Any] = {}
_currentWord: str
class Fragment:
"""A structure to hold typed values of a fragment.""" """A structure to hold typed values of a fragment."""
_type: str # Function of this fragment. Think text, span, button _type: str # Function of this fragment. Think text, span, button
_text: str # The simple utf-8 text _text: str # The simple utf-8 text
_content: list['Word.Fragment'] _content: list['Fragment']
_audio: QUrl # Optional audio URL _audio: QUrl # Optional audio URL
_font: QFont # The font, with all options, to use for this fragment _font: QFont # The font, with all options, to use for this fragment
_align: QTextOption # Alignment information _align: QTextOption # Alignment information
@@ -226,24 +216,30 @@ class Word:
def left(self) -> int: def left(self) -> int:
return self._left return self._left
class Line:
_maxHeight: int API = "https://api.dictionaryapi.dev/api/v2/entries/en/{word}"
_leading: int MWAPI = "https://www.dictionaryapi.com/api/v3/references/collegiate/json/{word}?key=51d9df34-ee13-489e-8656-478c215e846c"
_baseLine: int
_fragments: list['Word.Fragment']
class Word:
"""All processing of a dictionary word."""
_words: dict[str, Any] = {}
class Line:
def __init__(self) -> None: def __init__(self) -> None:
self._maxHeight = -1 self._maxHeight = -1
self._baseLine = -1 self._baseLine = -1
self._leading = -1 self._leading = -1
self._fragments = [] self._fragments:list[Fragment] = []
return return
def __repr__(self) -> str: def __repr__(self) -> str:
return '|'.join([x.text() for x in self._fragments])+f'|{self._maxHeight}' 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() org = frag.text()
if frag.asis(): if frag.asis():
return [frag] return [frag]
@@ -259,7 +255,7 @@ class Word:
script = QFont(frag.font()) script = QFont(frag.font())
script.setPixelSize(int(script.pixelSize()/4)) script.setPixelSize(int(script.pixelSize()/4))
results: list['Word.Fragment'] = [] results: list[Fragment] = []
while True: while True:
text = frag.text() text = frag.text()
start = text.find('{') start = text.find('{')
@@ -292,7 +288,7 @@ class Word:
newFrag = copy.copy(frag) newFrag = copy.copy(frag)
oldFont = QFont(frag.font()) oldFont = QFont(frag.font())
if token == 'bc': if token == 'bc':
results.append(Word.Fragment(': ', bold, color=QColor('#fff'))) results.append(Fragment(': ', bold, color=QColor('#fff')))
continue continue
if token in ['b', 'inf', 'it', 'sc', 'sup', 'phrase', 'parahw', 'gloss', if token in ['b', 'inf', 'it', 'sc', 'sup', 'phrase', 'parahw', 'gloss',
'qword', 'wi', 'dx', 'dx_def', 'dx_ety', 'ma']: '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}") 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" SPEAKER = "\U0001F508"
if frag.audio(): if frag.audio():
@@ -459,7 +455,7 @@ class Word:
x += width x += width
return return
def getLine(self) -> list['Word.Fragment']: def getLine(self) -> list[Fragment]:
return self._fragments return self._fragments
def getLeading(self) -> int: def getLeading(self) -> int:
@@ -467,26 +463,13 @@ class Word:
_lines: list[Line] = [] _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: def __init__(self, word: str) -> None:
# self.resources = {}
# reset the current definition
#
try:
if word != self._current['word']:
self._lines = []
except KeyError:
pass
# #
# Have we already retrieved this word? # Have we already retrieved this word?
# #
try: try:
self._current = json.loads(self._words[word]) self.current = json.loads(Word._words[word])
return return
except KeyError: except KeyError:
pass pass
@@ -496,17 +479,17 @@ class Word:
if not query.exec(): if not query.exec():
query_error(query) query_error(query)
if query.next(): if query.next():
self._words[word] = { Word._words[word] = {
'word': word, 'word': word,
'source': query.value('source'), 'source': query.value('source'),
'definition': json.loads(query.value("definition")), 'definition': json.loads(query.value("definition")),
} }
self._current = self._words[word] self.current = Word._words[word]
return return
source = 'mw' source = 'mw'
response = requests.get(MWAPI.format(word=word)) response = requests.get(MWAPI.format(word=word))
if response.status_code != 200: if response.status_code != 200:
self._current = {} self.current = {}
return return
data = json.loads(response.content.decode("utf-8")) data = json.loads(response.content.decode("utf-8"))
print(data) print(data)
@@ -515,35 +498,34 @@ class Word:
'source': source, 'source': source,
'definition': data, 'definition': data,
} }
self._current = self._words[word] self.current = Word._words[word]
query.prepare( query.prepare(
"INSERT INTO words " "INSERT INTO words "
"(word, source, definition) " "(word, source, definition) "
"VALUES (:word, :source, :definition)" "VALUES (:word, :source, :definition)"
) )
query.bindValue(":word", self._current['word']) query.bindValue(":word", self.current['word'])
query.bindValue(":source", self._current['source']) query.bindValue(":source", self.current['source'])
query.bindValue(":definition", json.dumps(self._current['definition'])) query.bindValue(":definition", json.dumps(self.current['definition']))
if not query.exec(): if not query.exec():
query_error(query) query_error(query)
return return
def getCurrent(self) -> str: def getWord(self) -> str:
return self._current['word'] return self.current['word']
def get_html(self) -> str | None: def get_html(self) -> str | None:
if self._current['source'] == 'mw': if self.current['source'] == 'mw':
return self.mw_html() return self.mw_html()
elif self._current['source'] == 'apidictionary': elif self.current['source'] == 'apidictionary':
return self.apidictionary_html() return self.apidictionary_html()
else: 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: def get_def(self) -> list[Line] | None:
if len(self._lines) > 0: if len(self._lines) > 0:
return self._lines return self._lines
if len(self._resources.keys()) < 1: if len(self.resources.keys()) < 1:
# #
# Colors we used # Colors we used
# #
@@ -563,7 +545,7 @@ class Word:
phonicFont = QFontDatabase.font("Gentium", None, 10) phonicFont = QFontDatabase.font("Gentium", None, 10)
phonicFont.setPixelSize(20) phonicFont.setPixelSize(20)
self._resources = { self.resources = {
'colors': { 'colors': {
'base':QColor(Qt.GlobalColor.white), 'base':QColor(Qt.GlobalColor.white),
'blue': QColor("#4a7d95"), 'blue': QColor("#4a7d95"),
@@ -577,22 +559,22 @@ class Word:
'text': textFont, 'text': textFont,
} }
} }
if self._current['source'] == 'mw': if self.current['source'] == 'mw':
return self.mw_def() return self.mw_def()
elif self._current['source'] == 'apidictionary': elif self.current['source'] == 'apidictionary':
return None return None
else: else:
raise Exception(f"Unknown source: {self._current['source']}") raise Exception(f"Unknown source: {self.current['source']}")
def mw_def(self) -> list[Line]: def mw_def(self) -> list[Line]:
lines: list[Word.Line] = [] lines: list[Word.Line] = []
for entry in self._current['definition']: for entry in self.current['definition']:
line = Word.Line() line = Word.Line()
meta = json.dumps(entry['meta']) meta = json.dumps(entry['meta'])
line.addFragment( line.addFragment(
Word.Fragment( Fragment(
meta, meta,
self._resources['fonts']['text'],asis=True self.resources['fonts']['text'],asis=True
) )
) )
lines.append(line) lines.append(line)
@@ -623,18 +605,18 @@ class Word:
for dt in sense['dt']: for dt in sense['dt']:
if dt[0] == 'text': if dt[0] == 'text':
line = Word.Line() line = Word.Line()
frag = Word.Fragment( frag = Fragment(
f"{outer} {inner} ", f"{outer} {inner} ",
self._resources['fonts']['bold'], self.resources['fonts']['bold'],
color = self._resources['colors']['base'] color = self.resources['colors']['base']
) )
outer = ' ' outer = ' '
frag.setLeft(10) frag.setLeft(10)
line.addFragment(frag) line.addFragment(frag)
frag = Word.Fragment( frag = Fragment(
dt[1], dt[1],
self._resources['fonts']['text'], self.resources['fonts']['text'],
color = self._resources['colors']['base'] color = self.resources['colors']['base']
) )
frag.setLeft(30) frag.setLeft(30)
line.addFragment(frag) line.addFragment(frag)
@@ -642,14 +624,14 @@ class Word:
elif dt[0] == 'vis': elif dt[0] == 'vis':
for vis in dt[1]: for vis in dt[1]:
line = Word.Line() line = Word.Line()
frag =Word.Fragment(f" ", frag =Fragment(f" ",
self._resources['fonts']['bold'], self.resources['fonts']['bold'],
) )
frag.setLeft(45) frag.setLeft(45)
line.addFragment(frag) line.addFragment(frag)
line.addFragment( line.addFragment(
Word.Fragment(vis['t'], Fragment(vis['t'],
self._resources['fonts']['text'], self.resources['fonts']['text'],
color = QColor('#aaa') color = QColor('#aaa')
) )
) )
@@ -659,15 +641,15 @@ class Word:
# #
# Easy reference to colors # Easy reference to colors
# #
base = self._resources['colors']['base'] base = self.resources['colors']['base']
blue = self._resources['colors']['blue'] blue = self.resources['colors']['blue']
lines: list[Word.Line] = [] lines: list[Word.Line] = []
line = Word.Line() line = Word.Line()
hw = re.sub(r'\*', '', entry['hwi']['hw']) 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) 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) line.addFragment(frag)
lines.append(line) lines.append(line)
@@ -675,19 +657,19 @@ class Word:
line = self.Line() line = self.Line()
space = '' space = ''
for vrs in entry["vrs"]: 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 = ' ' space = ' '
line.addFragment(frag) line.addFragment(frag)
lines.append(line) lines.append(line)
if "prs" in entry["hwi"].keys(): if "prs" in entry["hwi"].keys():
line = self.Line() 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) line.addFragment(frag)
for prs in entry["hwi"]["prs"]: for prs in entry["hwi"]["prs"]:
audio = self.sound_url(prs) audio = self.sound_url(prs)
if audio is None: if audio is None:
audio = "" 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.setAudio(audio)
frag.setPadding(0,10,3,12) frag.setPadding(0,10,3,12)
frag.setBorder(1) frag.setBorder(1)
@@ -699,18 +681,18 @@ class Word:
space = '' space = ''
for ins in entry['ins']: for ins in entry['ins']:
try: 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) line.addFragment(frag)
space = ' ' space = ' '
except KeyError: except KeyError:
pass 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) line.addFragment(frag)
space = '; ' space = '; '
lines.append(line) lines.append(line)
if 'lbs' in entry.keys(): if 'lbs' in entry.keys():
line = self.Line() 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) line.addFragment(frag)
lines.append(line) lines.append(line)
for value in entry['def']: # has multiple 'sseg' or 'vd' init for value in entry['def']: # has multiple 'sseg' or 'vd' init
@@ -721,9 +703,9 @@ class Word:
lines += r lines += r
elif k == 'vd': elif k == 'vd':
line = self.Line() line = self.Line()
line.addFragment(Word.Fragment( line.addFragment(Fragment(
v, v,
self._resources['fonts']['italic'], self.resources['fonts']['italic'],
color=blue color=blue
)) ))
lines.append(line) lines.append(line)
@@ -749,28 +731,28 @@ class Word:
# #
# Create the header, base word and its label # Create the header, base word and its label
# #
word = self._current["hwi"]["hw"] word = self.current["hwi"]["hw"]
label = self._current["fl"] label = self.current["fl"]
html = f'<h1 class="def-word">{word} <span class="def-label">{label}</span></h1>\n' 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. # If there are variants, then add them in an unordered list.
# CSS will make it pretty # CSS will make it pretty
# #
if "vrs" in self._current.keys(): if "vrs" in self.current.keys():
html += "<ul class=\"def-vrs'>\n" html += "<ul class=\"def-vrs'>\n"
html += "<li>" html += "<li>"
html += "</li>\n<li>".join( html += "</li>\n<li>".join(
[vrs["va"] for vrs in self._current["vrs"]] [vrs["va"] for vrs in self.current["vrs"]]
) )
html += "</li>\n</ul>\n" html += "</li>\n</ul>\n"
# #
# If there is a pronunciation section, create it # If there is a pronunciation section, create it
# #
if "prs" in self._current["hwi"].keys(): if "prs" in self.current["hwi"].keys():
tmp = [] tmp = []
for prs in self._current["hwi"]["prs"]: for prs in self.current["hwi"]["prs"]:
url = self.sound_url(prs) url = self.sound_url(prs)
how = prs["mw"] how = prs["mw"]
if url: if url:
@@ -784,16 +766,16 @@ class Word:
# #
# If there are inflections, create a header for that. # If there are inflections, create a header for that.
# #
if "ins" in self._current.keys(): if "ins" in self.current.keys():
html += '<h2 class="def-word">' html += '<h2 class="def-word">'
html += ", ".join([ins["if"] for ins in self._current["ins"]]) html += ", ".join([ins["if"] for ins in self.current["ins"]])
html += "</h2>\n" html += "</h2>\n"
# #
# Start creating the definition section # Start creating the definition section
# #
html += "<ul class='def-outer'>\n" html += "<ul class='def-outer'>\n"
for meaning in self._current["def"]: for meaning in self.current["def"]:
html += f"<li>{meaning['vd']}\n" html += f"<li>{meaning['vd']}\n"
html += '<ul class="def-inner">\n' html += '<ul class="def-inner">\n'
label = "" label = ""
@@ -825,19 +807,14 @@ class Word:
class Definition(QWidget): class Definition(QWidget):
pronounce = pyqtSignal(str) pronounce = pyqtSignal(str)
_word: str
_lines: list[Word.Line]
_buttons: list[QRect]
def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None: def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None:
super(Definition, self).__init__(*args, **kwargs) super(Definition, self).__init__(*args, **kwargs)
self._word = w.getCurrent() self._word:str = w.getWord()
lines = w.get_def() lines = w.get_def()
assert lines is not None assert lines is not None
self._lines = lines self._lines = lines
self._buttons = [] self._buttons:list[Fragment] = []
assert self._lines is not None
#self.setFixedWidth(600)
base = 0 base = 0
for line in self._lines: for line in self._lines:
line.finalizeLine(self.width()) line.finalizeLine(self.width())
@@ -859,7 +836,8 @@ class Definition(QWidget):
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None: def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
if not event: if not event:
return super().mousePressEvent(event) return super().mousePressEvent(event)
for rect in self._buttons: for frag in self._buttons:
rect = frag.borderRect()
if rect.contains(event.pos()): if rect.contains(event.pos()):
self._downRect = rect self._downRect = rect
return return