diff --git a/lib/definition.py b/lib/definition.py index deb1ac6..22f9222 100644 --- a/lib/definition.py +++ b/lib/definition.py @@ -521,8 +521,10 @@ class Definition(QWidget): self.pronounce.emit(url) elif url.scheme() == 'word': self.newWord.emit(url.path()) + elif url.scheme() == 'sense': + self.newWord.emit(url.path()) else: - print(f"{clk['fmt'].anchorHref()}: {url.scheme()}") + print(f"{clk['fmt'].anchorHref()}") self.alert.emit() self._downClickable = None return diff --git a/lib/read.py b/lib/read.py index 091a12c..b687588 100644 --- a/lib/read.py +++ b/lib/read.py @@ -1,12 +1,9 @@ -import json -from typing import Any, Dict, List, Optional, cast +from typing import Dict, List, Optional, cast -import requests from PyQt6.QtCore import QPoint, QResource, Qt, QTimer, pyqtSignal, pyqtSlot from PyQt6.QtGui import ( QBrush, QColor, - QCursor, QKeyEvent, QPainter, QPainterPath, @@ -15,7 +12,7 @@ from PyQt6.QtGui import ( QTextCursor, ) from PyQt6.QtSql import QSqlQuery -from PyQt6.QtWidgets import QDialog, QTextEdit, QWidget +from PyQt6.QtWidgets import QDialog, QWidget from lib import query_error from lib.preferences import Preferences @@ -89,6 +86,7 @@ class ReadDialog(QDialog, Ui_ReadDialog): self.playSound.connect(self.sound.playSound) self.playAlert.connect(self.sound.alert) self.definition.pronounce.connect(self.sound.playSound) + self.definition.newWord.connect(self.newWord) return # @@ -98,6 +96,15 @@ class ReadDialog(QDialog, Ui_ReadDialog): # # slots # + @pyqtSlot(str) + def newWord(self, word: str) -> None: + w = Word(word) + if not w.isValid(): + self.playAlert.emit() + return + self.definition.setWord(w) + return + @pyqtSlot() def timerAction(self) -> None: if self.session.isActive(): # We are stopping @@ -127,6 +134,9 @@ class ReadDialog(QDialog, Ui_ReadDialog): cursor.select(QTextCursor.SelectionType.WordUnderCursor) text = cursor.selectedText().strip() word = Word(text) + if not word.isValid(): + self.playAlert.emit() + return word.playPRS() return @@ -221,6 +231,9 @@ class ReadDialog(QDialog, Ui_ReadDialog): cursor.select(cursor.SelectionType.WordUnderCursor) text = cursor.selectedText().strip() word = Word(text) + if not word.isValid(): + self.playAlert.emit() + return self.definition.setWord(word) self.showDefinition() return diff --git a/lib/words.py b/lib/words.py index 2dbf44c..26a6bc0 100644 --- a/lib/words.py +++ b/lib/words.py @@ -40,6 +40,7 @@ class Word: """All processing of a dictionary word.""" _words: dict[str, WordType] = {} + _valid = False def __init__(self, word: str) -> None: # @@ -62,14 +63,18 @@ class Word: "definition": json.loads(query.value("definition")), } self.current = Word._words[word] + self._valid = True 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) + if self._words[word] is None: + self._valid = False + return self.current = Word._words[word] query.prepare( "INSERT INTO words " @@ -81,8 +86,12 @@ class Word: query.bindValue(":definition", json.dumps(self.current["definition"])) if not query.exec(): query_error(query) + self._valid = True return + def isValid(self) -> bool: + return self._valid + @pyqtSlot() def playSound(self) -> None: url = discovered_plugins[self.current["source"]].getFirstSound( diff --git a/plugins/merriam-webster.py b/plugins/merriam-webster.py index c0d2ae3..85066b6 100644 --- a/plugins/merriam-webster.py +++ b/plugins/merriam-webster.py @@ -4,7 +4,7 @@ from typing import Any, Literal, NotRequired, TypedDict, cast from PyQt6.QtCore import QEventLoop, QUrl from PyQt6.QtGui import QFont, QFontDatabase, QTextCharFormat, QTextLayout -from PyQt6.QtNetwork import QNetworkRequest +from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest from trycast import trycast from lib.definition import Fragment, Line @@ -91,7 +91,7 @@ Inflection = TypedDict( class CrossReferenceTarget(TypedDict): - cxl: str + cxl: NotRequired[str] cxr: NotRequired[str] cxt: str cxn: NotRequired[str] @@ -277,7 +277,7 @@ class WordType(TypedDict): definition: Any -def fetch(word: str) -> WordType: +def fetch(word: str) -> WordType|None: request = QNetworkRequest() url = QUrl(API.format(word=word, key=key)) request.setUrl(url) @@ -287,6 +287,9 @@ def fetch(word: str) -> WordType: loop = QEventLoop() reply.finished.connect(loop.quit) loop.exec() + if reply.error() != QNetworkReply.NetworkError.NoError: + print(f"Error fetching {word}: {reply.errorString()}") + return None content = reply.readAll() data = json.loads(content.data().decode("utf-8")) return { @@ -295,7 +298,6 @@ def fetch(word: str) -> WordType: "definition": data, } - def soundUrl(sound: Sound, fmt="ogg") -> QUrl: """Create a URL from a PRS structure.""" base = f"audio://media.merriam-webster.com/audio/prons/en/us/{fmt}" @@ -343,16 +345,12 @@ def do_prs(frag: Fragment, prs: list[Pronunciation] | None) -> None: fmt.setAnchorHref(soundUrl(pr["sound"]).toString()) fmt.setForeground(r.linkColor) #text = pr["mw"] +' \N{SPEAKER} ' - text = pr["mw"] +' ' + text = ' '+pr["mw"] +' ' else: text = pr['mw'] + ' ' - print(f"text: {text}, length: {len(text)}") frag.addText(text, fmt) if "l2" in pr: frag.addText(pun + pr["l2"], r.subduedLabelFormat) - text = frag.layout().text() - for fmt in frag.layout().formats(): - print(f"start: {fmt.start}, length: {fmt.length}, text: \"{text[fmt.start:fmt.start+fmt.length]}\"") return @@ -741,6 +739,42 @@ def do_uros(uros: list[UndefinedRunOn]|None) -> list[Line]: lines.append(line) lines += newLines return lines + +def do_cxs(cxs: list[CognateCrossRef]|None) -> list[Line]: + assert cxs is not None + r = Resources() + lines: list[Line] = [] + for cx in cxs: + frag = Fragment() + frag.addText(cx['cxl']+' ', r.italicFormat) + for cxt in cx['cxtis']: + if 'cxl' in cxt: + frag.addText(cxt['cxl'], r.italicFormat) + text = cxt['cxt'] + anchor = text + if 'cxr' in cxt: + anchor = cxt['cxr'] + if 'cxn' in cxt: + anchor += f"/{cxt['cxn']}" + + fmt = QTextCharFormat(r.smallCapsFormat) + fmt.setAnchor(True) + fmt.setForeground(r.linkColor) + fmt.setFontUnderline(True) + fmt.setUnderlineColor(r.linkColor) + fmt.setFontUnderline(True) + fmt.setAnchorHref('sense:///'+anchor) + # + # XXX - Capitalization does not work + # + text = text.upper() + fmt.setFontPointSize(fmt.fontPointSize() * 0.90) + frag.addText(text, fmt) + line = Line() + line.addFragment(frag) + lines.append(line) + return lines + def getDef(defines: Any) -> list[Line]: Line.setParseText(parseText) workList = restructure(defines) @@ -831,15 +865,17 @@ def getDef(defines: Any) -> list[Line]: lines.append(line) line = Line() frag = Fragment() - defines = trycast(list[DefinitionSection], work["def"]) - assert defines is not None - for define in defines: - try: - lines += do_def(define) - except NotImplementedError: - raise + if 'def' in work: + defines = trycast(list[DefinitionSection], work["def"]) + assert defines is not None + for define in defines: + try: + lines += do_def(define) + except NotImplementedError: + pass + if 'cxs' in work: + lines += do_cxs(trycast(list[CognateCrossRef], work['cxs'])) if "uros" in work: - print(json.dumps(work['uros'],indent=2)) uros = trycast(list[UndefinedRunOn], work['uros']) lines += do_uros(uros) if "dros" in work: @@ -852,10 +888,6 @@ def getDef(defines: Any) -> list[Line]: phrases.append(line) phrases += do_dros(dros) if "et" in work: - line = Line() - frag = Fragment('', r.textFont) - frag.addText(f"{work['fl']} ({used[work['fl']]})",r.labelFormat) - line.addFragment(frag) ets += do_ets(trycast(list[list[Pair]], work["et"])) for k in work.keys(): if k not in [ @@ -872,8 +904,9 @@ def getDef(defines: Any) -> list[Line]: "vrs", "dros", 'uros', + 'cxs', ]: - raise NotImplementedError(f"Unknown key {k} in work") + print( NotImplementedError(f"Unknown key {k} in work")) if len(phrases) > 0: lines += phrases if len(ets) > 0: @@ -914,6 +947,11 @@ def replaceCode(code:str) -> tuple[str, QTextCharFormat]: fmt.setFontItalic(True) elif token == 'sx': fmt.setFontCapitalization(QFont.Capitalization.SmallCaps) + # + # XXX - Capitalization does not work + # + text = text.upper() + fmt.setFontPointSize(fmt.fontPointSize() * 0.90) elif token == 'dxt': if fields[3] == 'illustration': fmt.setAnchorHref('article:///'+fields[2]) @@ -928,10 +966,14 @@ def replaceCode(code:str) -> tuple[str, QTextCharFormat]: fmt.setAnchorHref('etymology:///'+fields[2]) else: fmt.setAnchorHref('etymology:///' + fields[1]) + elif token == 'd_link': + if fields[2] != '': + fmt.setAnchorHref('direct:///' + fields[2]) + else: + fmt.setAnchorHref('direct:///' + fields[1]) else: raise NotImplementedError(f"Token {code} not implimented") fmt.setForeground(r.linkColor) - print(f"Format.capitalization(): {fmt.fontCapitalization()}") return (text,fmt) def markup(offset: int, text:str) -> tuple[str, list[QTextLayout.FormatRange]]: