Checkpoint. Not working
This commit is contained in:
@@ -4,4 +4,5 @@ from .books import Book
|
|||||||
from .person import PersonDialog
|
from .person import PersonDialog
|
||||||
from .read import ReadDialog
|
from .read import ReadDialog
|
||||||
from .session import SessionDialog
|
from .session import SessionDialog
|
||||||
from .words import Definition, DefinitionArea, Word
|
from .words import DefinitionArea, Word
|
||||||
|
from .definition import Fragment, Line, Definition
|
||||||
|
|||||||
832
lib/words.py
832
lib/words.py
@@ -1,624 +1,46 @@
|
|||||||
import copy
|
import importlib
|
||||||
|
import pkgutil
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, Optional, cast
|
from typing import Any, Dict, cast
|
||||||
|
|
||||||
from PyQt6.QtCore import (
|
from PyQt6.QtCore import (
|
||||||
QByteArray,
|
|
||||||
QEventLoop,
|
|
||||||
QMargins,
|
|
||||||
QPoint,
|
|
||||||
QRect,
|
|
||||||
QSize,
|
|
||||||
Qt,
|
Qt,
|
||||||
QThread,
|
|
||||||
QUrl,
|
|
||||||
pyqtSignal,
|
|
||||||
pyqtSlot,
|
pyqtSlot,
|
||||||
)
|
)
|
||||||
from PyQt6.QtGui import (
|
from PyQt6.QtGui import (
|
||||||
QBrush,
|
|
||||||
QColor,
|
QColor,
|
||||||
QFont,
|
QFont,
|
||||||
QFontDatabase,
|
QFontDatabase,
|
||||||
QFontMetrics,
|
|
||||||
QMouseEvent,
|
|
||||||
QPainter,
|
|
||||||
QPaintEvent,
|
|
||||||
QResizeEvent,
|
|
||||||
QTextOption,
|
|
||||||
QTransform,
|
|
||||||
)
|
)
|
||||||
from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest
|
from PyQt6.QtNetwork import QNetworkAccessManager
|
||||||
from PyQt6.QtSql import QSqlQuery
|
from PyQt6.QtSql import QSqlQuery
|
||||||
from PyQt6.QtWidgets import QScrollArea, QWidget
|
from PyQt6.QtWidgets import QScrollArea
|
||||||
|
|
||||||
from lib import query_error
|
from lib import query_error
|
||||||
from lib.sounds import SoundOff
|
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__ + '.')
|
||||||
|
|
||||||
class Fragment:
|
discovered_plugins = {
|
||||||
"""A fragment of text to be displayed"""
|
# finder, name, ispkg
|
||||||
|
importlib.import_module(name).registration['source']: importlib.import_module(name) for _, name, _ in find_plugins(plugins)
|
||||||
def __init__(
|
}
|
||||||
self,
|
|
||||||
text: str,
|
|
||||||
font: QFont,
|
|
||||||
audio: str = "",
|
|
||||||
color: Optional[QColor] = None,
|
|
||||||
asis: bool = False,
|
|
||||||
) -> None:
|
|
||||||
self._text = text
|
|
||||||
self._font = font
|
|
||||||
self._audio: QUrl = QUrl(audio)
|
|
||||||
self._align = QTextOption(
|
|
||||||
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline
|
|
||||||
)
|
|
||||||
self._padding = QMargins()
|
|
||||||
self._border = QMargins()
|
|
||||||
self._margin = QMargins()
|
|
||||||
self._wref = ""
|
|
||||||
self._position = QPoint()
|
|
||||||
self._rect = QRect()
|
|
||||||
self._borderRect = QRect()
|
|
||||||
self._clickRect = QRect()
|
|
||||||
if color:
|
|
||||||
self._color = color
|
|
||||||
else:
|
|
||||||
self._color = QColor()
|
|
||||||
self._asis = asis
|
|
||||||
self._left = 0
|
|
||||||
self._target = "word"
|
|
||||||
return
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.__repr__()
|
|
||||||
|
|
||||||
def size(self, width: int) -> QSize:
|
|
||||||
rect = QRect(self._position, QSize(width - self._position.x(), 2000))
|
|
||||||
flags = (
|
|
||||||
Qt.AlignmentFlag.AlignLeft
|
|
||||||
| Qt.AlignmentFlag.AlignBaseline
|
|
||||||
| Qt.TextFlag.TextWordWrap
|
|
||||||
)
|
|
||||||
fm = QFontMetrics(self._font)
|
|
||||||
bounding = fm.boundingRect(rect, flags, self._text)
|
|
||||||
size = bounding.size()
|
|
||||||
size = size.grownBy(self._padding)
|
|
||||||
size = size.grownBy(self._border)
|
|
||||||
size = size.grownBy(self._margin)
|
|
||||||
return size
|
|
||||||
|
|
||||||
def height(self, width: int) -> int:
|
|
||||||
return self.size(width).height()
|
|
||||||
|
|
||||||
def width(self, width: int) -> int:
|
|
||||||
return self.size(width).width()
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"({self._position.x()}, {self._position.y()}): {self._text}"
|
|
||||||
|
|
||||||
def repaintEvent(self, painter: QPainter) -> int:
|
|
||||||
painter.save()
|
|
||||||
painter.setFont(self._font)
|
|
||||||
painter.setPen(self._color)
|
|
||||||
rect = QRect()
|
|
||||||
rect.setLeft(self._position.x())
|
|
||||||
rect.setTop(
|
|
||||||
self._position.y()
|
|
||||||
+ painter.fontMetrics().descent()
|
|
||||||
- painter.fontMetrics().height()
|
|
||||||
)
|
|
||||||
rect.setWidth(painter.viewport().width() - self._position.x())
|
|
||||||
rect.setHeight(2000)
|
|
||||||
flags = (
|
|
||||||
Qt.AlignmentFlag.AlignLeft
|
|
||||||
| Qt.AlignmentFlag.AlignBaseline
|
|
||||||
| Qt.TextFlag.TextWordWrap
|
|
||||||
)
|
|
||||||
bounding = painter.boundingRect(rect, flags, self._text)
|
|
||||||
size = bounding.size()
|
|
||||||
|
|
||||||
painter.setPen(QColor("#f00"))
|
|
||||||
if self._audio.isValid():
|
|
||||||
radius = self._borderRect.height() / 2
|
|
||||||
painter.drawRoundedRect(self._borderRect, radius, radius)
|
|
||||||
if self._wref:
|
|
||||||
start = bounding.bottomLeft()
|
|
||||||
end = bounding.bottomRight()
|
|
||||||
painter.drawLine(start, end)
|
|
||||||
painter.setPen(self._color)
|
|
||||||
painter.drawText(rect, flags, self._text)
|
|
||||||
painter.restore()
|
|
||||||
size = size.grownBy(self._margin)
|
|
||||||
size = size.grownBy(self._border)
|
|
||||||
size = size.grownBy(self._padding)
|
|
||||||
return size.height()
|
|
||||||
|
|
||||||
#
|
|
||||||
# Setters
|
|
||||||
#
|
|
||||||
def setText(self, text: str) -> None:
|
|
||||||
self._text = text
|
|
||||||
return
|
|
||||||
|
|
||||||
def setTarget(self, target: str) -> None:
|
|
||||||
self._target = target
|
|
||||||
return
|
|
||||||
|
|
||||||
def setFont(self, font: QFont) -> None:
|
|
||||||
self._font = font
|
|
||||||
return
|
|
||||||
|
|
||||||
def setAudio(self, audio: str | QUrl) -> None:
|
|
||||||
if type(audio) is str:
|
|
||||||
self._audio = QUrl(audio)
|
|
||||||
else:
|
|
||||||
self._audio = cast(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, *args: int, **kwargs: int) -> None:
|
|
||||||
top = kwargs.get("top", -1)
|
|
||||||
right = kwargs.get("right", -1)
|
|
||||||
bottom = kwargs.get("bottom", -1)
|
|
||||||
left = kwargs.get("left", -1)
|
|
||||||
if top > -1 or right > -1 or bottom > -1 or left > -1:
|
|
||||||
if top >= 0:
|
|
||||||
self._padding.setTop(top)
|
|
||||||
if right >= 0:
|
|
||||||
self._padding.setRight(right)
|
|
||||||
if bottom >= 0:
|
|
||||||
self._padding.setBottom(bottom)
|
|
||||||
if left >= 0:
|
|
||||||
self._padding.setLeft(left)
|
|
||||||
return
|
|
||||||
if len(args) == 4:
|
|
||||||
(top, right, bottom, left) = [args[0], args[1], args[2], args[3]]
|
|
||||||
elif len(args) == 3:
|
|
||||||
(top, right, bottom, left) = [args[0], args[1], args[2], args[1]]
|
|
||||||
elif len(args) == 2:
|
|
||||||
(top, right, bottom, left) = [args[0], args[1], args[0], args[1]]
|
|
||||||
elif len(args) == 1:
|
|
||||||
(top, right, bottom, left) = [args[0], args[0], args[0], args[0]]
|
|
||||||
else:
|
|
||||||
raise Exception("argument error")
|
|
||||||
self._padding = QMargins(left, top, right, bottom)
|
|
||||||
return
|
|
||||||
|
|
||||||
def setBorder(self, *args: int, **kwargs: int) -> None:
|
|
||||||
top = kwargs.get("top", -1)
|
|
||||||
right = kwargs.get("right", -1)
|
|
||||||
bottom = kwargs.get("bottom", -1)
|
|
||||||
left = kwargs.get("left", -1)
|
|
||||||
if top > -1 or right > -1 or bottom > -1 or left > -1:
|
|
||||||
if top >= 0:
|
|
||||||
self._border.setTop(top)
|
|
||||||
if right >= 0:
|
|
||||||
self._border.setRight(right)
|
|
||||||
if bottom >= 0:
|
|
||||||
self._border.setBottom(bottom)
|
|
||||||
if left >= 0:
|
|
||||||
self._border.setLeft(left)
|
|
||||||
return
|
|
||||||
if len(args) == 4:
|
|
||||||
(top, right, bottom, left) = [args[0], args[1], args[2], args[3]]
|
|
||||||
elif len(args) == 3:
|
|
||||||
(top, right, bottom, left) = [args[0], args[1], args[2], args[1]]
|
|
||||||
elif len(args) == 2:
|
|
||||||
(top, right, bottom, left) = [args[0], args[1], args[0], args[1]]
|
|
||||||
elif len(args) == 1:
|
|
||||||
(top, right, bottom, left) = [args[0], args[0], args[0], args[0]]
|
|
||||||
else:
|
|
||||||
raise Exception("argument error")
|
|
||||||
self._border = QMargins(left, top, right, bottom)
|
|
||||||
return
|
|
||||||
|
|
||||||
def setMargin(self, *args: int, **kwargs: int) -> None:
|
|
||||||
top = kwargs.get("top", -1)
|
|
||||||
right = kwargs.get("right", -1)
|
|
||||||
bottom = kwargs.get("bottom", -1)
|
|
||||||
left = kwargs.get("left", -1)
|
|
||||||
|
|
||||||
if top > -1 or right > -1 or bottom > -1 or left > -1:
|
|
||||||
if top >= 0:
|
|
||||||
self._margin.setTop(top)
|
|
||||||
if right >= 0:
|
|
||||||
self._margin.setRight(right)
|
|
||||||
if bottom >= 0:
|
|
||||||
self._margin.setBottom(bottom)
|
|
||||||
if left >= 0:
|
|
||||||
self._margin.setLeft(left)
|
|
||||||
return
|
|
||||||
if len(args) == 4:
|
|
||||||
(top, right, bottom, left) = [args[0], args[1], args[2], args[3]]
|
|
||||||
elif len(args) == 3:
|
|
||||||
(top, right, bottom, left) = [args[0], args[1], args[2], args[1]]
|
|
||||||
elif len(args) == 2:
|
|
||||||
(top, right, bottom, left) = [args[0], args[1], args[0], args[1]]
|
|
||||||
elif len(args) == 1:
|
|
||||||
(top, right, bottom, left) = [args[0], args[0], args[0], args[0]]
|
|
||||||
else:
|
|
||||||
raise Exception("argument error")
|
|
||||||
self._margin = QMargins(left, top, right, bottom)
|
|
||||||
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 setClickRect(self, rect: QRect) -> None:
|
|
||||||
self._clickRect = 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 text(self) -> str:
|
|
||||||
return self._text
|
|
||||||
|
|
||||||
def font(self) -> QFont:
|
|
||||||
return self._font
|
|
||||||
|
|
||||||
def audio(self) -> QUrl:
|
|
||||||
return self._audio
|
|
||||||
|
|
||||||
def align(self) -> QTextOption:
|
|
||||||
return self._align
|
|
||||||
|
|
||||||
def rect(self) -> QRect:
|
|
||||||
return self._rect
|
|
||||||
|
|
||||||
def padding(self) -> QMargins:
|
|
||||||
return self._padding
|
|
||||||
|
|
||||||
def border(self) -> QMargins:
|
|
||||||
return self._border
|
|
||||||
|
|
||||||
def margin(self) -> QMargins:
|
|
||||||
return self._margin
|
|
||||||
|
|
||||||
def position(self) -> QPoint:
|
|
||||||
return self._position
|
|
||||||
|
|
||||||
def borderRect(self) -> QRect:
|
|
||||||
return self._borderRect
|
|
||||||
|
|
||||||
def clickRect(self) -> QRect:
|
|
||||||
return self._clickRect
|
|
||||||
|
|
||||||
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}"
|
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:
|
class Word:
|
||||||
"""All processing of a dictionary word."""
|
"""All processing of a dictionary word."""
|
||||||
|
|
||||||
_words: dict[str, Any] = {}
|
_words: dict[str, Any] = {}
|
||||||
|
|
||||||
class Line:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._maxHeight = -1
|
|
||||||
self._baseLine = -1
|
|
||||||
self._leading = -1
|
|
||||||
self._fragments: list[Fragment] = []
|
|
||||||
return
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return (
|
|
||||||
"|".join([x.text() for x in self._fragments])
|
|
||||||
+ f"|{self._maxHeight}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def repaintEvent(self, painter: QPainter) -> int:
|
|
||||||
#
|
|
||||||
# we do not have an event field because we are not a true widget
|
|
||||||
#
|
|
||||||
lineSpacing = 0
|
|
||||||
for frag in self._fragments:
|
|
||||||
ls = frag.repaintEvent(painter)
|
|
||||||
if ls > lineSpacing:
|
|
||||||
lineSpacing = ls
|
|
||||||
return lineSpacing
|
|
||||||
|
|
||||||
def parseText(self, frag: Fragment) -> list[Fragment]:
|
|
||||||
org = frag.text()
|
|
||||||
if frag.asis():
|
|
||||||
return [frag]
|
|
||||||
#
|
|
||||||
# Needed Fonts
|
|
||||||
# We can't use _resources because that might not be the font
|
|
||||||
# for this piece of text
|
|
||||||
#
|
|
||||||
bold = QFont(frag.font())
|
|
||||||
bold.setWeight(QFont.Weight.Bold)
|
|
||||||
italic = QFont(frag.font())
|
|
||||||
italic.setItalic(True)
|
|
||||||
smallCaps = QFont(frag.font())
|
|
||||||
smallCaps.setCapitalization(QFont.Capitalization.SmallCaps)
|
|
||||||
script = QFont(frag.font())
|
|
||||||
script.setPixelSize(int(script.pixelSize() / 4))
|
|
||||||
caps = QFont(frag.font())
|
|
||||||
caps.setCapitalization(QFont.Capitalization.AllUppercase)
|
|
||||||
|
|
||||||
results: list[Fragment] = []
|
|
||||||
while True:
|
|
||||||
text = frag.text()
|
|
||||||
start = text.find("{")
|
|
||||||
if start < 0:
|
|
||||||
results.append(frag)
|
|
||||||
return results
|
|
||||||
if start > 0:
|
|
||||||
newFrag = copy.copy(frag)
|
|
||||||
newFrag.setText(text[:start])
|
|
||||||
results.append(newFrag)
|
|
||||||
frag.setText(text[start:])
|
|
||||||
continue
|
|
||||||
#
|
|
||||||
# Start == 0
|
|
||||||
#
|
|
||||||
|
|
||||||
#
|
|
||||||
# If the token is an end-token, return now.
|
|
||||||
#
|
|
||||||
if text.startswith("{/"):
|
|
||||||
results.append(frag)
|
|
||||||
return results
|
|
||||||
|
|
||||||
#
|
|
||||||
# extract this token
|
|
||||||
#
|
|
||||||
end = text.find("}")
|
|
||||||
token = text[1:end]
|
|
||||||
frag.setText(text[end + 1 :])
|
|
||||||
newFrag = copy.copy(frag)
|
|
||||||
oldFont = QFont(frag.font())
|
|
||||||
if token == "bc":
|
|
||||||
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",
|
|
||||||
]:
|
|
||||||
if token == "b":
|
|
||||||
frag.setFont(bold)
|
|
||||||
elif token in ["it", "qword", "wi"]:
|
|
||||||
frag.setFont(italic)
|
|
||||||
elif token == "sc":
|
|
||||||
frag.setFont(smallCaps)
|
|
||||||
elif token in ["inf", "sup"]:
|
|
||||||
frag.setFont(script)
|
|
||||||
elif token == "phrase":
|
|
||||||
font = QFont(bold)
|
|
||||||
font.setItalic(True)
|
|
||||||
frag.setFont(font)
|
|
||||||
elif token == "parahw":
|
|
||||||
font = QFont(smallCaps)
|
|
||||||
font.setWeight(QFont.Weight.Bold)
|
|
||||||
frag.setFont(font)
|
|
||||||
elif token == "gloss":
|
|
||||||
frag.setText("[" + frag.text())
|
|
||||||
elif token in ["dx", "dx_ety"]:
|
|
||||||
frag.setText("\u2014" + frag.text())
|
|
||||||
elif token == "ma":
|
|
||||||
frag.setText("\u2014 more at " + frag.text())
|
|
||||||
elif token == "dx_def":
|
|
||||||
frag.setText("(" + frag.text())
|
|
||||||
else:
|
|
||||||
raise Exception(f"Unknown block marker: {token}")
|
|
||||||
results += self.parseText(frag)
|
|
||||||
frag = results.pop()
|
|
||||||
frag.setFont(oldFont)
|
|
||||||
text = frag.text()
|
|
||||||
if not text.startswith("{/" + token + "}"):
|
|
||||||
raise Exception(
|
|
||||||
f"No matching close for {token} in {org}"
|
|
||||||
)
|
|
||||||
if token == "gloss":
|
|
||||||
results[-1].setText(results[-1].text() + "]")
|
|
||||||
elif token == "dx_def":
|
|
||||||
results[-1].setText(results[-1].text() + ")")
|
|
||||||
end = text.find("}")
|
|
||||||
text = text[end + 1 :]
|
|
||||||
frag.setText(text)
|
|
||||||
continue
|
|
||||||
#
|
|
||||||
# These are codes that include all information within the token
|
|
||||||
#
|
|
||||||
fields = token.split("|")
|
|
||||||
token = fields[0]
|
|
||||||
if token in [
|
|
||||||
"a_link",
|
|
||||||
"d_link",
|
|
||||||
"dxt",
|
|
||||||
"et_link",
|
|
||||||
"i_link",
|
|
||||||
"mat",
|
|
||||||
"sx",
|
|
||||||
]:
|
|
||||||
wref = ""
|
|
||||||
htext = fields[1]
|
|
||||||
oldFont = QFont(frag.font())
|
|
||||||
target = "word"
|
|
||||||
if token == "a_link":
|
|
||||||
wref = fields[1]
|
|
||||||
elif token in ["d_link", "et_link", "mat", "sx", "i_link"]:
|
|
||||||
if fields[2] == "":
|
|
||||||
wref = fields[1]
|
|
||||||
else:
|
|
||||||
wref = fields[2]
|
|
||||||
if token == "i_link":
|
|
||||||
frag.setFont(italic)
|
|
||||||
elif token == "sx":
|
|
||||||
frag.setFont(caps)
|
|
||||||
elif token == "dxt":
|
|
||||||
if fields[3] == "illustration":
|
|
||||||
wref = fields[2]
|
|
||||||
target = "article"
|
|
||||||
elif fields[3] == "table":
|
|
||||||
wref = fields[2]
|
|
||||||
target = "table"
|
|
||||||
elif fields[3] != "":
|
|
||||||
wref = fields[3]
|
|
||||||
target = "sense"
|
|
||||||
else:
|
|
||||||
wref = fields[1]
|
|
||||||
target = "word"
|
|
||||||
elif token == "a_link":
|
|
||||||
target = "word"
|
|
||||||
wref = fields[1]
|
|
||||||
else:
|
|
||||||
raise Exception(f"Unknown code: {token} in {org}")
|
|
||||||
newFrag = copy.copy(frag)
|
|
||||||
newFrag.setText(htext)
|
|
||||||
newFrag.setWRef(wref)
|
|
||||||
newFrag.setTarget(target)
|
|
||||||
results.append(newFrag)
|
|
||||||
frag.setFont(oldFont)
|
|
||||||
text = frag.text()
|
|
||||||
continue
|
|
||||||
raise Exception(
|
|
||||||
f"Unable to locate a known token {token} in {org}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def addFragment(
|
|
||||||
self,
|
|
||||||
frag: Fragment,
|
|
||||||
) -> None:
|
|
||||||
SPEAKER = "\U0001F508"
|
|
||||||
|
|
||||||
if frag.audio().isValid():
|
|
||||||
frag.setText(frag.text() + " " + SPEAKER)
|
|
||||||
|
|
||||||
text = frag.text()
|
|
||||||
text = re.sub(r"\*", "\u2022", text)
|
|
||||||
text = re.sub(r"\{ldquo\}", "\u201c", text)
|
|
||||||
text = re.sub(r"\{rdquo\}", "\u201d", text)
|
|
||||||
frag.setText(text)
|
|
||||||
if frag.audio().isValid():
|
|
||||||
frag.setPadding(3, 0, 0, 5)
|
|
||||||
frag.setBorder(1)
|
|
||||||
frag.setMargin(0, 0, 0, 0)
|
|
||||||
items = self.parseText(frag)
|
|
||||||
self._fragments += items
|
|
||||||
return
|
|
||||||
|
|
||||||
def finalizeLine(self, width: int, base: int) -> None:
|
|
||||||
"""Create all of the positions for all the fragments."""
|
|
||||||
#
|
|
||||||
# Find the maximum hight and max baseline
|
|
||||||
#
|
|
||||||
maxHeight = -1
|
|
||||||
baseLine = -1
|
|
||||||
leading = -1
|
|
||||||
for frag in self._fragments:
|
|
||||||
fm = QFontMetrics(frag.font())
|
|
||||||
height = frag.height(width)
|
|
||||||
bl = fm.height() - fm.descent()
|
|
||||||
if fm.leading() > leading:
|
|
||||||
leading = fm.leading()
|
|
||||||
if height > maxHeight:
|
|
||||||
maxHeight = height
|
|
||||||
if bl > baseLine:
|
|
||||||
baseLine = bl
|
|
||||||
self._baseLine = baseLine
|
|
||||||
self._maxHeight = maxHeight
|
|
||||||
self._leading = leading
|
|
||||||
x = 0
|
|
||||||
for frag in self._fragments:
|
|
||||||
if x < frag.left():
|
|
||||||
x = frag.left()
|
|
||||||
#
|
|
||||||
# We need to calculate the location to draw the
|
|
||||||
# text. We also need to calculate the bounding Rectangle
|
|
||||||
# for this fragment
|
|
||||||
#
|
|
||||||
size = frag.size(width)
|
|
||||||
fm = QFontMetrics(frag.font())
|
|
||||||
offset = (
|
|
||||||
frag.margin().left()
|
|
||||||
+ frag.border().left()
|
|
||||||
+ frag.padding().left()
|
|
||||||
)
|
|
||||||
frag.setPosition(QPoint(x + offset, self._baseLine))
|
|
||||||
if not frag.border().isNull() or not frag.wRef():
|
|
||||||
#
|
|
||||||
# self._baseLine is where the text will be drawn
|
|
||||||
# fm.descent is the distance from the baseline of the
|
|
||||||
# text to the bottom of the rect
|
|
||||||
# The top of the bounding rect is at self._baseLine
|
|
||||||
# + fm.descent - rect.height
|
|
||||||
# The border is drawn at top-padding-border-margin+marin
|
|
||||||
#
|
|
||||||
top = self._baseLine + fm.descent() - fm.height()
|
|
||||||
y = top - frag.padding().top() - frag.border().top()
|
|
||||||
pos = QPoint(x, y)
|
|
||||||
rect = QRect(pos, size.shrunkBy(frag.margin()))
|
|
||||||
frag.setBorderRect(rect)
|
|
||||||
pos.setY(pos.y() + base)
|
|
||||||
frag.setClickRect(QRect(pos, size.shrunkBy(frag.margin())))
|
|
||||||
x += size.width()
|
|
||||||
return
|
|
||||||
|
|
||||||
def getLine(self) -> list[Fragment]:
|
|
||||||
return self._fragments
|
|
||||||
|
|
||||||
def getLineSpacing(self) -> int:
|
|
||||||
return self._leading + self._maxHeight
|
|
||||||
|
|
||||||
_lines: list[Line] = []
|
|
||||||
_resources: Dict[str, Any] = {}
|
_resources: Dict[str, Any] = {}
|
||||||
|
_nam = QNetworkAccessManager()
|
||||||
def __init__(self, word: str) -> None:
|
def __init__(self, word: str) -> None:
|
||||||
self._resources = {}
|
Word.set_resources()
|
||||||
#
|
#
|
||||||
# Have we already retrieved this word?
|
# Have we already retrieved this word?
|
||||||
#
|
#
|
||||||
@@ -645,23 +67,8 @@ class Word:
|
|||||||
# key for MW to decide on the source to use.
|
# key for MW to decide on the source to use.
|
||||||
#
|
#
|
||||||
source = "mw"
|
source = "mw"
|
||||||
manager = QNetworkAccessManager()
|
|
||||||
request = QNetworkRequest()
|
self._words[word] = discovered_plugins[source].fetch(word)
|
||||||
url = QUrl(MWAPI.format(word=word))
|
|
||||||
request.setUrl(url)
|
|
||||||
reply = manager.get(request)
|
|
||||||
assert reply is not None
|
|
||||||
loop = QEventLoop()
|
|
||||||
reply.finished.connect(loop.quit)
|
|
||||||
loop.exec()
|
|
||||||
content = reply.readAll()
|
|
||||||
data = json.loads(content.data().decode("utf-8"))
|
|
||||||
print(data)
|
|
||||||
self._words[word] = {
|
|
||||||
"word": word,
|
|
||||||
"source": source,
|
|
||||||
"definition": data,
|
|
||||||
}
|
|
||||||
self.current = Word._words[word]
|
self.current = Word._words[word]
|
||||||
query.prepare(
|
query.prepare(
|
||||||
"INSERT INTO words "
|
"INSERT INTO words "
|
||||||
@@ -675,37 +82,10 @@ class Word:
|
|||||||
query_error(query)
|
query_error(query)
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot()
|
@classmethod
|
||||||
def playPRS(self) -> None:
|
def set_resources(cls) -> None:
|
||||||
try:
|
if len(cls._resources.keys()) > 0:
|
||||||
prs = self.current[0]["hwi"]["prs"]
|
|
||||||
audio = QUrl(self.mw_sound_url(prs))
|
|
||||||
snd = SoundOff()
|
|
||||||
snd.playSound(audio)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def finished(self) -> None:
|
|
||||||
print("finished")
|
|
||||||
return
|
|
||||||
|
|
||||||
def getWord(self) -> str:
|
|
||||||
return cast(str, self.current["word"])
|
|
||||||
|
|
||||||
def get_html(self) -> str | None:
|
|
||||||
if self.current["source"] == "mw":
|
|
||||||
return self.mw_html()
|
|
||||||
elif self.current["source"] == "apidictionary":
|
|
||||||
return self.apidictionary_html()
|
|
||||||
else:
|
|
||||||
raise Exception(f"Unknown source: {self.current['source']}")
|
|
||||||
|
|
||||||
def get_def(self) -> list[Line] | None:
|
|
||||||
if len(self._lines) > 0:
|
|
||||||
return self._lines
|
|
||||||
if len(self._resources.keys()) < 1:
|
|
||||||
#
|
#
|
||||||
# Colors we used
|
# Colors we used
|
||||||
#
|
#
|
||||||
@@ -729,10 +109,11 @@ class Word:
|
|||||||
phonicFont = QFontDatabase.font("Gentium", None, 10)
|
phonicFont = QFontDatabase.font("Gentium", None, 10)
|
||||||
phonicFont.setPixelSize(20)
|
phonicFont.setPixelSize(20)
|
||||||
|
|
||||||
self._resources = {
|
cls._resources = {
|
||||||
"colors": {
|
"colors": {
|
||||||
"base": QColor(Qt.GlobalColor.white),
|
"base": QColor(Qt.GlobalColor.white),
|
||||||
"blue": QColor("#4a7d95"),
|
"link": QColor("#4a7d95"),
|
||||||
|
"subdued": QColor(Qt.GlobalColor.gray),
|
||||||
},
|
},
|
||||||
"fonts": {
|
"fonts": {
|
||||||
"header": headerFont,
|
"header": headerFont,
|
||||||
@@ -745,15 +126,35 @@ class Word:
|
|||||||
"smallCaps": smallCapsFont,
|
"smallCaps": smallCapsFont,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if self.current["source"] == "mw":
|
@pyqtSlot()
|
||||||
return self.mw_def()
|
def playSound(self) -> None:
|
||||||
elif self.current["source"] == "apidictionary":
|
url = discovered_plugins[self.current['source']].getFirstSound(self.current['definition'])
|
||||||
return None
|
if url.isValid():
|
||||||
else:
|
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']}")
|
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[Line] = []
|
||||||
# print(json.dumps(self.current,indent=2))
|
# print(json.dumps(self.current,indent=2))
|
||||||
for entry in self.current["definition"]:
|
for entry in self.current["definition"]:
|
||||||
lines += self.mw_def_entry(entry)
|
lines += self.mw_def_entry(entry)
|
||||||
@@ -761,7 +162,7 @@ class Word:
|
|||||||
return lines
|
return lines
|
||||||
|
|
||||||
def mw_seq(self, seq: list[Any]) -> list[Line]:
|
def mw_seq(self, seq: list[Any]) -> list[Line]:
|
||||||
lines: list[Word.Line] = []
|
lines: list[Line] = []
|
||||||
outer = " "
|
outer = " "
|
||||||
inner = " "
|
inner = " "
|
||||||
for value in seq:
|
for value in seq:
|
||||||
@@ -785,7 +186,7 @@ class Word:
|
|||||||
inner = sns[0]
|
inner = sns[0]
|
||||||
try:
|
try:
|
||||||
text = ", ".join(sense["sls"])
|
text = ", ".join(sense["sls"])
|
||||||
line = Word.Line()
|
line = Line()
|
||||||
frag = Fragment(
|
frag = Fragment(
|
||||||
f"{outer} {inner} ",
|
f"{outer} {inner} ",
|
||||||
self._resources["fonts"]["bold"],
|
self._resources["fonts"]["bold"],
|
||||||
@@ -805,7 +206,7 @@ class Word:
|
|||||||
try:
|
try:
|
||||||
for dt in sense["dt"]:
|
for dt in sense["dt"]:
|
||||||
if dt[0] == "text":
|
if dt[0] == "text":
|
||||||
line = Word.Line()
|
line = Line()
|
||||||
frag = Fragment(
|
frag = Fragment(
|
||||||
f"{outer} {inner} ",
|
f"{outer} {inner} ",
|
||||||
self._resources["fonts"]["bold"],
|
self._resources["fonts"]["bold"],
|
||||||
@@ -824,7 +225,7 @@ class Word:
|
|||||||
lines.append(line)
|
lines.append(line)
|
||||||
elif dt[0] == "vis":
|
elif dt[0] == "vis":
|
||||||
for vis in dt[1]:
|
for vis in dt[1]:
|
||||||
line = Word.Line()
|
line = Line()
|
||||||
frag = Fragment(
|
frag = Fragment(
|
||||||
f" ",
|
f" ",
|
||||||
self._resources["fonts"]["bold"],
|
self._resources["fonts"]["bold"],
|
||||||
@@ -846,7 +247,7 @@ class Word:
|
|||||||
try:
|
try:
|
||||||
line = lines.pop()
|
line = lines.pop()
|
||||||
except IndexError:
|
except IndexError:
|
||||||
line = Word.Line()
|
line = Line()
|
||||||
frag = Fragment(
|
frag = Fragment(
|
||||||
"\u27F6 " + seg[1],
|
"\u27F6 " + seg[1],
|
||||||
self._resources["fonts"]["text"],
|
self._resources["fonts"]["text"],
|
||||||
@@ -855,7 +256,8 @@ class Word:
|
|||||||
frag.setLeft(30)
|
frag.setLeft(30)
|
||||||
line.addFragment(frag)
|
line.addFragment(frag)
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
|
elif dt[0] == 'ca':
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Unknown key {dt[0]} in {sense['dt']}")
|
raise Exception(f"Unknown key {dt[0]} in {sense['dt']}")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -869,8 +271,8 @@ class Word:
|
|||||||
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[Line] = []
|
||||||
line = Word.Line()
|
line = Line()
|
||||||
hw = re.sub(r"\*", "", entry["hwi"]["hw"])
|
hw = re.sub(r"\*", "", entry["hwi"]["hw"])
|
||||||
frag = Fragment(hw, self._resources["fonts"]["header"], color=base)
|
frag = Fragment(hw, self._resources["fonts"]["header"], color=base)
|
||||||
line.addFragment(frag)
|
line.addFragment(frag)
|
||||||
@@ -881,7 +283,7 @@ class Word:
|
|||||||
lines.append(line)
|
lines.append(line)
|
||||||
|
|
||||||
if "vrs" in entry.keys():
|
if "vrs" in entry.keys():
|
||||||
line = self.Line()
|
line = Line()
|
||||||
space = ""
|
space = ""
|
||||||
for vrs in entry["vrs"]:
|
for vrs in entry["vrs"]:
|
||||||
frag = Fragment(
|
frag = Fragment(
|
||||||
@@ -892,8 +294,8 @@ class Word:
|
|||||||
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"]:
|
||||||
line = self.Line()
|
line = Line()
|
||||||
frag = Fragment(
|
frag = Fragment(
|
||||||
entry["hwi"]["hw"] + " ",
|
entry["hwi"]["hw"] + " ",
|
||||||
self._resources["fonts"]["phonic"],
|
self._resources["fonts"]["phonic"],
|
||||||
@@ -911,7 +313,7 @@ class Word:
|
|||||||
line.addFragment(frag)
|
line.addFragment(frag)
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
if "ins" in entry.keys():
|
if "ins" in entry.keys():
|
||||||
line = self.Line()
|
line = Line()
|
||||||
space = ""
|
space = ""
|
||||||
for ins in entry["ins"]:
|
for ins in entry["ins"]:
|
||||||
try:
|
try:
|
||||||
@@ -931,7 +333,7 @@ class Word:
|
|||||||
space = "; "
|
space = "; "
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
if "lbs" in entry.keys():
|
if "lbs" in entry.keys():
|
||||||
line = self.Line()
|
line = Line()
|
||||||
frag = Fragment(
|
frag = Fragment(
|
||||||
"; ".join(entry["lbs"]),
|
"; ".join(entry["lbs"]),
|
||||||
self._resources["fonts"]["bold"],
|
self._resources["fonts"]["bold"],
|
||||||
@@ -946,7 +348,7 @@ class Word:
|
|||||||
r = self.mw_seq(seq)
|
r = self.mw_seq(seq)
|
||||||
lines += r
|
lines += r
|
||||||
elif k == "vd":
|
elif k == "vd":
|
||||||
line = self.Line()
|
line = Line()
|
||||||
line.addFragment(
|
line.addFragment(
|
||||||
Fragment(
|
Fragment(
|
||||||
v, self._resources["fonts"]["italic"], color=blue
|
v, self._resources["fonts"]["italic"], color=blue
|
||||||
@@ -955,20 +357,6 @@ class Word:
|
|||||||
lines.append(line)
|
lines.append(line)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def mw_sound_url(self, prs: dict[str, Any], fmt: str = "ogg") -> str | None:
|
|
||||||
"""Create a URL from a PRS structure."""
|
|
||||||
base = f"https://media.merriam-webster.com/audio/prons/en/us/{fmt}"
|
|
||||||
if "sound" not in prs.keys():
|
|
||||||
return None
|
|
||||||
audio = prs["sound"]["audio"]
|
|
||||||
m = re.match(r"(bix|gg|[a-zA-Z])", audio)
|
|
||||||
if m:
|
|
||||||
url = base + f"/{m.group(1)}/"
|
|
||||||
else:
|
|
||||||
url = base + "/number/"
|
|
||||||
url += audio + f".{fmt}"
|
|
||||||
return url
|
|
||||||
|
|
||||||
def mw_html(self) -> str:
|
def mw_html(self) -> str:
|
||||||
#
|
#
|
||||||
# Create the header, base word and its label
|
# Create the header, base word and its label
|
||||||
@@ -1047,98 +435,6 @@ class Word:
|
|||||||
return html
|
return html
|
||||||
|
|
||||||
|
|
||||||
class Definition(QWidget):
|
|
||||||
pronounce = pyqtSignal(str)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, word: Optional[Word] = None, *args: Any, **kwargs: Any
|
|
||||||
) -> None:
|
|
||||||
super(Definition, self).__init__(*args, **kwargs)
|
|
||||||
self._word = word
|
|
||||||
if word is not None:
|
|
||||||
self.setWord(word)
|
|
||||||
return
|
|
||||||
|
|
||||||
def setWord(self, word: Word) -> None:
|
|
||||||
self._word = word
|
|
||||||
lines = word.get_def()
|
|
||||||
assert lines is not None
|
|
||||||
self._lines = lines
|
|
||||||
self._buttons: list[Fragment] = []
|
|
||||||
base = 0
|
|
||||||
|
|
||||||
for line in self._lines:
|
|
||||||
line.finalizeLine(self.width(), base)
|
|
||||||
for frag in line.getLine():
|
|
||||||
if frag.audio().isValid():
|
|
||||||
self._buttons.append(frag)
|
|
||||||
if frag.wRef():
|
|
||||||
print(f"Adding {frag} as an anchor")
|
|
||||||
self._buttons.append(frag)
|
|
||||||
base += line.getLineSpacing()
|
|
||||||
self.setFixedHeight(base)
|
|
||||||
return
|
|
||||||
|
|
||||||
def resizeEvent(self, event: Optional[QResizeEvent] = None) -> None:
|
|
||||||
base = 0
|
|
||||||
for line in self._lines:
|
|
||||||
line.finalizeLine(self.width(), base)
|
|
||||||
base += line.getLineSpacing()
|
|
||||||
self.setFixedHeight(base)
|
|
||||||
super(Definition, self).resizeEvent(event)
|
|
||||||
return
|
|
||||||
|
|
||||||
_downFrag: Optional[Fragment | None] = None
|
|
||||||
|
|
||||||
def mousePressEvent(self, event: Optional[QMouseEvent]) -> None:
|
|
||||||
if not event:
|
|
||||||
return super().mousePressEvent(event)
|
|
||||||
print(f"mousePressEvent: {event.pos()}")
|
|
||||||
for frag in self._buttons:
|
|
||||||
rect = frag.clickRect()
|
|
||||||
if rect.contains(event.pos()):
|
|
||||||
self._downFrag = frag
|
|
||||||
return
|
|
||||||
return super().mousePressEvent(event)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event: Optional[QMouseEvent]) -> None:
|
|
||||||
if not event:
|
|
||||||
return super().mouseReleaseEvent(event)
|
|
||||||
if self._downFrag is not None and self._downFrag.clickRect().contains(
|
|
||||||
event.pos()
|
|
||||||
):
|
|
||||||
audio = self._downFrag.audio().url()
|
|
||||||
print(audio)
|
|
||||||
self.pronounce.emit(audio)
|
|
||||||
print("emit done")
|
|
||||||
self._downFrag = None
|
|
||||||
return
|
|
||||||
self._downFrag = None
|
|
||||||
return super().mouseReleaseEvent(event)
|
|
||||||
|
|
||||||
def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa
|
|
||||||
painter = QPainter(self)
|
|
||||||
painter.save()
|
|
||||||
painter.setBrush(QBrush())
|
|
||||||
painter.setPen(QColor("white"))
|
|
||||||
|
|
||||||
#
|
|
||||||
# Each line needs a base calculated. To do that, we need to find the
|
|
||||||
# bounding rectangle of the text. Once we have the bounding rectangle,
|
|
||||||
# we can use the descendant to calculate the baseline within that
|
|
||||||
# bounding box.
|
|
||||||
#
|
|
||||||
# All text on this line needs to be on the same baseline
|
|
||||||
#
|
|
||||||
assert self._lines is not None
|
|
||||||
base = 0
|
|
||||||
for line in self._lines:
|
|
||||||
transform = QTransform()
|
|
||||||
transform.translate(0, base)
|
|
||||||
painter.setTransform(transform)
|
|
||||||
base += line.repaintEvent(painter)
|
|
||||||
painter.restore()
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class DefinitionArea(QScrollArea):
|
class DefinitionArea(QScrollArea):
|
||||||
|
|||||||
0
plugins/__init__.py
Normal file
0
plugins/__init__.py
Normal file
253
plugins/merriam-webster.py
Normal file
253
plugins/merriam-webster.py
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
from trycast import trycast
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import Any, NamedTuple, TypedDict, cast
|
||||||
|
|
||||||
|
from PyQt6.QtCore import QEventLoop, QUrl, Qt
|
||||||
|
from PyQt6.QtGui import QColor, QFont
|
||||||
|
from PyQt6.QtNetwork import QNetworkRequest
|
||||||
|
from lib.words import Word
|
||||||
|
from lib.definition import Line, Fragment
|
||||||
|
|
||||||
|
registration = {
|
||||||
|
'source': 'mw',
|
||||||
|
'name': 'Merriam-Webster',
|
||||||
|
}
|
||||||
|
|
||||||
|
API = "https://www.dictionaryapi.com/api/v3/references/collegiate/json/{word}?key={key}"
|
||||||
|
key = "51d9df34-ee13-489e-8656-478c215e846c"
|
||||||
|
|
||||||
|
class TextTuple(NamedTuple):
|
||||||
|
type_: str # 'text'
|
||||||
|
text: str
|
||||||
|
class TTuple(NamedTuple):
|
||||||
|
type_: str # 't'
|
||||||
|
text: str
|
||||||
|
class VerbalIllustration(TypedDict):
|
||||||
|
t: str
|
||||||
|
aq: str
|
||||||
|
|
||||||
|
class VerbalIllustrationTuple(NamedTuple):
|
||||||
|
type_: str # 'vis'
|
||||||
|
data: list[VerbalIllustration]
|
||||||
|
|
||||||
|
class Sound(TypedDict):
|
||||||
|
audio: str
|
||||||
|
ref: str
|
||||||
|
stat: str
|
||||||
|
|
||||||
|
class Pronunciation(TypedDict):
|
||||||
|
mw: str
|
||||||
|
l: str
|
||||||
|
l2: str
|
||||||
|
pun: str
|
||||||
|
sound: Sound
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Meta(TypedDict):
|
||||||
|
id: str
|
||||||
|
uuid: str
|
||||||
|
sort: str
|
||||||
|
src: str
|
||||||
|
section: str
|
||||||
|
stems: list[str]
|
||||||
|
offensive: bool
|
||||||
|
|
||||||
|
class HeadWordInfo(TypedDict):
|
||||||
|
hw: str
|
||||||
|
prs: list[Pronunciation]
|
||||||
|
|
||||||
|
class HeadWord(TypedDict):
|
||||||
|
hw: str
|
||||||
|
prs: list[Pronunciation]
|
||||||
|
psl: str
|
||||||
|
|
||||||
|
class Variant(TypedDict):
|
||||||
|
va: str
|
||||||
|
vl: str
|
||||||
|
prs: list[Pronunciation]
|
||||||
|
spl: str
|
||||||
|
|
||||||
|
class Inflection(TypedDict):
|
||||||
|
if_: str
|
||||||
|
ifc: str
|
||||||
|
il: str
|
||||||
|
prs: list[Pronunciation]
|
||||||
|
spl: str
|
||||||
|
|
||||||
|
class DividedSense(TypedDict):
|
||||||
|
sd: str
|
||||||
|
et: list[str] # Not full
|
||||||
|
ins: list[Inflection]
|
||||||
|
lbs: list[str]
|
||||||
|
prs: list[Pronunciation]
|
||||||
|
sgram: str
|
||||||
|
sls: list[str]
|
||||||
|
vrs: list[Variant]
|
||||||
|
|
||||||
|
class BioGraphicalNameWrap(TypedDict):
|
||||||
|
pname: str
|
||||||
|
sname: str
|
||||||
|
altname: str
|
||||||
|
prs: list[Pronunciation]
|
||||||
|
|
||||||
|
class CalledAlsoTarget(TypedDict):
|
||||||
|
cat: str
|
||||||
|
catref: str
|
||||||
|
pn: str
|
||||||
|
prs: list[Pronunciation]
|
||||||
|
psl: str
|
||||||
|
|
||||||
|
class CalledAlso(TypedDict):
|
||||||
|
intro: str
|
||||||
|
cats: list[CalledAlsoTarget]
|
||||||
|
|
||||||
|
class RunInWrap(TypedDict):
|
||||||
|
rie: str
|
||||||
|
prs: list[Pronunciation]
|
||||||
|
text: str
|
||||||
|
vrs: list[Variant]
|
||||||
|
|
||||||
|
class Sense:
|
||||||
|
dt: list[str] # not full
|
||||||
|
et: list[str] # not full
|
||||||
|
ins: list[Inflection]
|
||||||
|
lbs: list[str]
|
||||||
|
prs: list[Pronunciation]
|
||||||
|
sdsense: DividedSense
|
||||||
|
sgram: str
|
||||||
|
sls: list[str]
|
||||||
|
sn: str
|
||||||
|
vrs: list[Variant]
|
||||||
|
|
||||||
|
class SenseSequence(TypedDict):
|
||||||
|
sense: Sense
|
||||||
|
sen: Sense
|
||||||
|
|
||||||
|
class Definition(TypedDict):
|
||||||
|
sseq: list[SenseSequence]
|
||||||
|
vd: str
|
||||||
|
|
||||||
|
class Entry(TypedDict):
|
||||||
|
meta: Meta
|
||||||
|
hom: str
|
||||||
|
hwi: HeadWordInfo
|
||||||
|
ahws: list[HeadWord]
|
||||||
|
vrs: list[Variant]
|
||||||
|
fl: str
|
||||||
|
def_: list[Definition]
|
||||||
|
|
||||||
|
def fetch(word:str) -> dict[str, Any]:
|
||||||
|
request = QNetworkRequest()
|
||||||
|
url = QUrl(API.format(word=word, key=key))
|
||||||
|
request.setUrl(url)
|
||||||
|
request.setTransferTimeout(3000)
|
||||||
|
reply = Word._nam.get(request)
|
||||||
|
assert reply is not None
|
||||||
|
loop = QEventLoop()
|
||||||
|
reply.finished.connect(loop.quit)
|
||||||
|
loop.exec()
|
||||||
|
content = reply.readAll()
|
||||||
|
data = json.loads(content.data().decode('utf-8'))
|
||||||
|
return {
|
||||||
|
'word': word,
|
||||||
|
'source': 'mw',
|
||||||
|
'definition': data,
|
||||||
|
}
|
||||||
|
|
||||||
|
def soundUrl(sound:Sound, fmt='ogg') -> QUrl:
|
||||||
|
"""Create a URL from a PRS structure."""
|
||||||
|
base = f"https://media.merriam-webster.com/audio/prons/en/us/{fmt}"
|
||||||
|
audio = sound['audio']
|
||||||
|
m = re.match(r"(bix|gg|[a-zA-Z])", audio)
|
||||||
|
if m:
|
||||||
|
url = base + f"/{m.group(1)}/"
|
||||||
|
else:
|
||||||
|
url = base + "/number/"
|
||||||
|
url += audio + f".{fmt}"
|
||||||
|
return QUrl(url)
|
||||||
|
|
||||||
|
def getFirstSound(definition: list[Entry]) -> QUrl:
|
||||||
|
# ahws, cats, dros, hwi, ins, ri, sdsense, sen, sense, uros, vrs
|
||||||
|
for entry in definition:
|
||||||
|
for v in entry.values():
|
||||||
|
hwi = trycast(HeadWordInfo, v)
|
||||||
|
if hwi is None:
|
||||||
|
continue
|
||||||
|
if 'prs' in hwi:
|
||||||
|
for pr in hwi['prs']:
|
||||||
|
if 'sound' in pr:
|
||||||
|
url = soundUrl(pr['sound'])
|
||||||
|
if url.isValid():
|
||||||
|
return url
|
||||||
|
return QUrl()
|
||||||
|
|
||||||
|
def do_prs(prs: list[Pronunciation]) -> list[Fragment]:
|
||||||
|
frags: list[Fragment] = []
|
||||||
|
font = trycast(QFont, Word._resources['fonts']['label'])
|
||||||
|
assert font is not None
|
||||||
|
linkColor = trycast(QColor, Word._resources['colors']['link'])
|
||||||
|
assert linkColor is not None
|
||||||
|
subduedColor = trycast(QColor, Word._resources['colors']['subdued'])
|
||||||
|
assert subduedColor is not None
|
||||||
|
|
||||||
|
for pr in prs:
|
||||||
|
if 'pun' in pr:
|
||||||
|
pun = pr['pun']
|
||||||
|
else:
|
||||||
|
pun = ' '
|
||||||
|
if 'l' in pr:
|
||||||
|
frags.append(
|
||||||
|
Fragment(pr['l'] + pun, font, color=subduedColor)
|
||||||
|
)
|
||||||
|
frag = Fragment(pr['mw'], font, color=subduedColor)
|
||||||
|
if 'sound' in pr:
|
||||||
|
frag.setAudio(soundUrl(pr['sound']))
|
||||||
|
frags.append(frag)
|
||||||
|
if 'l2' in pr:
|
||||||
|
frags.append(
|
||||||
|
Fragment(pun + pr['l2'], font, color=subduedColor)
|
||||||
|
)
|
||||||
|
return frags
|
||||||
|
|
||||||
|
def getDef(definition: list[Entry]) -> list[Line]:
|
||||||
|
lines = []
|
||||||
|
headerFont = trycast(QFont, Word._resources['fonts']['header'])
|
||||||
|
assert headerFont is not None
|
||||||
|
textFont = trycast(QFont, Word._resources['fonts']['text'])
|
||||||
|
assert textFont is not None
|
||||||
|
labelFont = trycast(QFont, Word._resources['fonts']['label'])
|
||||||
|
assert labelFont is not None
|
||||||
|
|
||||||
|
baseColor = trycast(QColor, Word._resources['colors']['base'])
|
||||||
|
assert baseColor is not None
|
||||||
|
linkColor = trycast(QColor, Word._resources['colors']['link'])
|
||||||
|
assert linkColor is not None
|
||||||
|
subduedColor = trycast(QColor, Word._resources['colors']['subdued'])
|
||||||
|
assert subduedColor is not None
|
||||||
|
entries = len(definition)
|
||||||
|
for count, entry in enumerate(definition):
|
||||||
|
#
|
||||||
|
# Create the First line from the hwi and fl
|
||||||
|
#
|
||||||
|
line = Line()
|
||||||
|
hwi = trycast(HeadWordInfo, entry['hwi'])
|
||||||
|
assert hwi is not None
|
||||||
|
hw = re.sub(r'\*', '', hwi['hw'])
|
||||||
|
line.addFragment(Fragment(hw + ' ', headerFont, color=baseColor))
|
||||||
|
frag = Fragment(f"{count} of {entries} ", textFont, color=linkColor)
|
||||||
|
frag.setBackground(QColor(Qt.GlobalColor.gray))
|
||||||
|
line.addFragment(frag)
|
||||||
|
line.addFragment(Fragment(entry['fl'], labelFont, color=baseColor))
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Next is the pronunciation.
|
||||||
|
#
|
||||||
|
line = Line()
|
||||||
|
hw = re.sub(r'\*', '\u00b7', hwi['hw'])
|
||||||
|
line.addFragment(Fragment(hw + ' ', textFont, color=subduedColor))
|
||||||
|
for frag in do_prs(hwi['prs']):
|
||||||
|
line.addFragment(frag)
|
||||||
|
return [Line()]
|
||||||
Reference in New Issue
Block a user