From 7d2532d775eebce80bb3aeeabc0262944a26731f Mon Sep 17 00:00:00 2001 From: "Christopher T. Johnson" Date: Tue, 7 May 2024 11:26:15 -0400 Subject: [PATCH] Layout is good, click boxes is wrong --- deftest.py | 38 ++- lib/__init__.py | 2 +- lib/definition.py | 421 ++++++++++++------------- lib/utils.py | 56 +++- lib/words.py | 10 - plugins/merriam-webster.py | 615 ++++++++++++++++++------------------- 6 files changed, 568 insertions(+), 574 deletions(-) mode change 100644 => 100755 deftest.py diff --git a/deftest.py b/deftest.py old mode 100644 new mode 100755 index 80daa5f..396632e --- a/deftest.py +++ b/deftest.py @@ -3,24 +3,32 @@ import faulthandler import os import signal import sys -from typing import cast +from typing import Any, cast -from PyQt6.QtCore import QResource, QSettings +from PyQt6.QtCore import QResource, QSettings, Qt from PyQt6.QtGui import QFontDatabase from PyQt6.QtSql import QSqlDatabase, QSqlQuery -from PyQt6.QtWidgets import QApplication +from PyQt6.QtWidgets import QApplication, QScrollArea -from lib import DefinitionArea, Word +from lib import Word from lib.sounds import SoundOff from lib.utils import query_error from lib.words import Definition +class DefinitionArea(QScrollArea): + def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None: + super(DefinitionArea, self).__init__(*args, *kwargs) + d = Definition(w) + self.setWidget(d) + self.setWidgetResizable(True) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + return -def monkeyClose(self, event): - settings = QSettings("Troglodite", "esl_reader") - settings.setValue("geometry", self.saveGeometry()) - super(DefinitionArea, self).closeEvent(event) - return + def closeEvent(self, event): + settings = QSettings("Troglodite", "esl_reader") + settings.setValue("geometry", self.saveGeometry()) + super(DefinitionArea, self).closeEvent(event) + return def main() -> int: @@ -66,12 +74,15 @@ def main() -> int: ): query_error(query) - word = Word("cowbell") + word = Word("lower") snd = SoundOff() - DefinitionArea.closeEvent = monkeyClose + print("Pre widget") widget = DefinitionArea(word) # xnoqa: F841 + print("post widget") settings = QSettings("Troglodite", "esl_reader") - widget.restoreGeometry(settings.value("geometry")) + geometry = settings.value("geometry") + if geometry is not None: + widget.restoreGeometry(geometry) d = cast(Definition, widget.widget()) assert d is not None d.pronounce.connect(snd.playSound) @@ -81,4 +92,7 @@ def main() -> int: if __name__ == "__main__": faulthandler.register(signal.Signals.SIGUSR1) + faulthandler.register(signal.Signals.SIGTERM) + faulthandler.register(signal.Signals.SIGHUP) + faulthandler.enable() sys.exit(main()) diff --git a/lib/__init__.py b/lib/__init__.py index 79f826a..5e69f44 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -5,4 +5,4 @@ from .definition import Definition, Fragment, Line from .person import PersonDialog from .read import ReadDialog from .session import SessionDialog -from .words import DefinitionArea, Word +from .words import Word diff --git a/lib/definition.py b/lib/definition.py index 59e495e..dbc1696 100644 --- a/lib/definition.py +++ b/lib/definition.py @@ -1,20 +1,23 @@ -import re -from typing import Any, Callable, Optional, Self, cast, overload +import unicodedata +from typing import Any, Callable, Optional, Self, TypedDict, cast -from PyQt6.QtCore import QMargins, QPoint, QRect, QSize, Qt, QUrl, pyqtSignal +from PyQt6.QtCore import QMargins, QPoint, QPointF, QRect, QRectF, QSize, Qt, QUrl, pyqtSignal from PyQt6.QtGui import ( QBrush, QColor, QFont, + QFontDatabase, QFontMetrics, QMouseEvent, QPainter, QPaintEvent, QResizeEvent, + QTextCharFormat, + QTextLayout, QTextOption, - QTransform, ) from PyQt6.QtWidgets import QWidget +from trycast import trycast class Fragment: @@ -24,7 +27,7 @@ class Fragment: def __init__( self, - which: str | Self, + which: str | Self | None = None, font: QFont | None = None, audio: str = "", color: Optional[QColor] = None, @@ -34,19 +37,22 @@ class Fragment: for k, v in which.__dict__.items(): self.__dict__[k] = v return - self._text: str = which + self._layout = QTextLayout() if font is None: - raise TypeError("Missing required parameter 'font'") - self._font = font - self._audio: QUrl = QUrl(audio) - self._align = QTextOption( + self._layout.setFont( + QFontDatabase.font("OpenDyslexic", None, 20) + ) + else: + self._layout.setFont(font) + align = QTextOption( Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline ) + self._layout.setTextOption(align) + self._audio: QUrl = QUrl(audio) self._padding = QMargins() self._border = QMargins() self._margin = QMargins() self._wref = "" - self._position = QPoint() self._rect = QRect() self._borderRect = QRect() self._clickRect = QRect() @@ -57,133 +63,109 @@ class Fragment: self._background = QColor() self._asis = asis self._indent = 0 - self._target = "word" + if which is not None: + self.setText(which) return - def __str__(self) -> str: - return self.__repr__() + def size(self) -> QSize: + return self.paintEvent() - def size(self, width: int) -> QSize: - return self.paintEvent(width) + def height(self) -> int: + return self.size().height() - def height(self, width: int) -> int: - return self.size(width).height() - - def width(self, width: int) -> int: - return self.size(width).width() + def width(self) -> int: + return self.size().width() def __repr__(self) -> str: - return f"({self._position.x()}, {self._position.y()}): {self._text}" + rect = self._layout.boundingRect() + text = self._layout.text() + return f"{text}: (({rect.x()}, {rect.y()}), {rect.width()}, {rect.height()})" - @overload - def paintEvent(self, widthSrc: int) -> QSize: - ... + def doLayout(self, width: int) -> QPointF: + leading = QFontMetrics(self._layout.font()).leading() + eol = self._layout.position() + base = 0 + indent = 0 + self._layout.setCacheEnabled(True) + self._layout.beginLayout() + while True: + line = self._layout.createLine() + if not line.isValid(): + break + line.setLineWidth(width - self._layout.position().x()) + line.setPosition(QPointF(indent, base+leading)) + rect = line.naturalTextRect() + eol = rect.bottomRight() + assert isinstance(eol, QPointF) + base += line.height() + indent = self.pixelIndent() - self._layout.position().x() + self._layout.endLayout() + result = eol + return result - @overload - def paintEvent(self, widthSrc: QPainter) -> int: - ... - - def paintEvent(self, widthSrc: QPainter | int) -> int | QSize: - if isinstance(widthSrc, QPainter): - viewportWidth = widthSrc.viewport().width() - painter = widthSrc - else: - viewportWidth = widthSrc - painter = None - fm = QFontMetrics(self._font) - top = self._position.y() + fm.descent() - fm.height() - left = self._position.x() - width = viewportWidth - left - height = 2000 - rect = QRect(left, top, width, height) - indent = self._indent * self._indentAmount - flags = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBaseline - boundingNoWrap = fm.boundingRect( - rect, flags | Qt.TextFlag.TextSingleLine, self._text - ) - bounding = fm.boundingRect( - rect, flags | Qt.TextFlag.TextWordWrap, self._text - ) - text = self._text - remainingText = "" - if boundingNoWrap.height() < bounding.height(): - # - # This is not optimal, but it is only a few iterations - # - lastSpace = 0 - char = 0 - pos = rect.x() - while pos < rect.right(): - if text[char] == " ": - lastSpace = char - pos += fm.horizontalAdvance(text[char]) - char += 1 - if lastSpace > 0: - remainingText = text[lastSpace + 1 :] - text = text[:lastSpace] - - size = boundingNoWrap.size() - - boundingNoWrap = fm.boundingRect( - rect, flags | Qt.TextFlag.TextSingleLine, text - ) - rect.setSize(boundingNoWrap.size()) - - if remainingText != "": - top += size.height() - remainingRect = QRect(indent, top, viewportWidth - indent, height) - boundingRemaingRect = fm.boundingRect( - remainingRect, flags | Qt.TextFlag.TextWordWrap, remainingText - ) - - size = size.grownBy(QMargins(0, 0, 0, boundingRemaingRect.height())) - remainingRect.setSize(boundingRemaingRect.size()) - size = size.grownBy(self._margin) - size = size.grownBy(self._border) - size = size.grownBy(self._padding) + def paintEvent(self, painter: Optional[QPainter] | None = None) -> QSize: + rect = self._layout.boundingRect() + size = rect.size() + assert size is not None if painter is None: - return size + return QSize(int(size.width()), int(size.height())) painter.save() - painter.setFont(self._font) - 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) - if self._background.isValid(): - brush = painter.brush() - brush.setColor(self._background) - brush.setStyle(Qt.BrushStyle.SolidPattern) - painter.setBrush(brush) - painter.fillRect(rect, brush) - painter.drawText(rect, flags, text) - if remainingText: - if self._background.isValid(): - painter.fillRect(remainingRect, brush) - painter.drawText( - remainingRect, flags | Qt.TextFlag.TextWordWrap, remainingText - ) + self._layout.draw(painter, QPointF(0,0)) + # + # TODO: draw the rounded rect around audio buttons + # + painter.brush().setColor(Qt.GlobalColor.green) + for fmt in self._layout.formats(): + if fmt.format.isAnchor(): + #text = self._layout.text()[fmt.start:fmt.start+fmt.length] + runs = self._layout.glyphRuns(fmt.start, fmt.length) + bb = runs[0].boundingRect() + bb.moveTo(bb.topLeft() + self._layout.position()) + painter.drawRect(bb) + #print(f"({bb.left()}-{bb.right()}, {bb.top()}-{bb.bottom()}): {text}") + painter.restore() - return size.height() + return QSize(int(size.width()), int(size.height())) # # Setters # - def setText(self, text: str) -> None: - self._text = text + def addText(self, text: str, fmt: Optional[QTextCharFormat] = None) -> None: + oldText = self._layout.text() + self._layout.setText(oldText + text) + if Line.parseText: + self._layout = Line.parseText(self) + + if fmt is not None: + fr = QTextLayout.FormatRange() + fr.format = fmt + fr.length = len(self._layout.text()) - len(oldText) + fr.start = len(oldText) + fmts = self._layout.formats() + fmts.append(fr) + self._layout.setFormats(fmts) return - def setTarget(self, target: str) -> None: - self._target = target + def setText(self, text: str) -> None: + text = unicodedata.normalize("NFKD",text) + self._layout.setText(text) + if Line.parseText: + self._layout = Line.parseText(self) + if self.audio().isValid(): + fr = QTextLayout.FormatRange() + fr.start=0 + fr.length = len(self._layout.text()) + fmt = QTextCharFormat() + fmt.setAnchor(True) + fmt.setAnchorHref(self._audio.toString()) + fr.format = fmt + formats = self._layout.formats() + formats.append(fr) + self._layout.setFormats(formats) return def setFont(self, font: QFont) -> None: - self._font = font + self._layout.setFont(font) return def setAudio(self, audio: str | QUrl) -> None: @@ -194,7 +176,7 @@ class Fragment: return def setAlign(self, align: QTextOption) -> None: - self._align = align + self._layout.setTextOption(align) return def setRect(self, rect: QRect) -> None: @@ -291,7 +273,7 @@ class Fragment: return def setPosition(self, pnt: QPoint) -> None: - self._position = pnt + self._layout.setPosition(QPointF(pnt.x(), pnt.y())) return def setBorderRect(self, rect: QRect) -> None: @@ -317,20 +299,23 @@ class Fragment: # # Getters # + def background(self) -> QColor: + return self._background + def wRef(self) -> str: return self._wref def text(self) -> str: - return self._text + return self._layout.text() def font(self) -> QFont: - return self._font + return self._layout.font() def audio(self) -> QUrl: return self._audio def align(self) -> QTextOption: - return self._align + return self._layout.textOption() def rect(self) -> QRect: return self._rect @@ -344,8 +329,8 @@ class Fragment: def margin(self) -> QMargins: return self._margin - def position(self) -> QPoint: - return self._position + def position(self) -> QPointF: + return self._layout.position() def borderRect(self) -> QRect: return self._borderRect @@ -365,6 +350,8 @@ class Fragment: def pixelIndent(self) -> int: return self._indent * self._indentAmount + def layout(self) -> QTextLayout: + return self._layout class Line: parseText = None @@ -391,99 +378,48 @@ class Line: # # we do not have an event field because we are not a true widget # - lineSpacing = 0 + pos = QSize(0,0) for frag in self._fragments: - ls = frag.paintEvent(painter) - if ls > lineSpacing: - lineSpacing = ls - return lineSpacing + pos = frag.paintEvent(painter) + return pos.height() def addFragment( self, frags: Fragment | list[Fragment], ) -> None: - SPEAKER = "\U0001F508" + #SPEAKER = "\U0001F508" if not isinstance(frags, list): frags = [ frags, ] - for frag in frags: - 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) - if Line.parseText: - items = Line.parseText(frag) - self._fragments += items - else: - self._fragments.append(frag) + self._fragments += frags 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 + # Each fragment needs to be positioned to the left of the + # last fragment or at the indent level. + # It needs to be aligned with the baseline of all the + # other fragments in the line. # - maxHeight = -1 - baseLine = -1 - leading = -1 + + left = 0 # Left size of rect + maxHeight = 0 + 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: - left = frag.pixelIndent() - if x < left: - x = 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() + if left < frag.pixelIndent(): + left = frag.pixelIndent() + frag.setPosition(QPoint(left, base)) + eol =frag.doLayout(width) + left = int(eol.x()+0.5) + if frag.layout().lineCount() > 1: + base = int(eol.y()+0.5) + if eol.y() > maxHeight: + maxHeight = eol.y() + self._maxHeight = int(maxHeight+0.5) + self._leading = 0 return def getLine(self) -> list[Fragment]: @@ -492,6 +428,10 @@ class Line: def getLineSpacing(self) -> int: return self._leading + self._maxHeight +class Clickable(TypedDict): + bb: QRectF + frag: Fragment + fmt: QTextCharFormat class Definition(QWidget): pronounce = pyqtSignal(str) @@ -510,64 +450,77 @@ class Definition(QWidget): lines: list[Line] = word.get_def() assert lines is not None self._lines = lines - self._buttons: list[Fragment] = [] + self._buttons: list[Clickable] = [] 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() + + for line in self._lines: + for frag in line.getLine(): + layout = frag.layout() + for fmtRng in layout.formats(): + if fmtRng.format.isAnchor(): + runs = layout.glyphRuns(fmtRng.start, fmtRng.length) + bb = runs[0].boundingRect() + pos = layout.position() + text = frag.text()[fmtRng.start:fmtRng.start + fmtRng.length] + new = bb.topLeft() + pos + print(f"({bb.left()}, {bb.top()}), ({pos.x()}, {pos.y()}), ({new.x()}, {new.y()}): {text}") + bb.moveTo(bb.topLeft() + pos) + self._buttons.append( + {'bb': bb, + 'fmt': fmtRng.format, + 'frag': frag, + } + ) self.setFixedHeight(base) return def resizeEvent(self, event: Optional[QResizeEvent] = None) -> None: base = 0 - for line in self._lines: + for idx, line in enumerate(self._lines): line.finalizeLine(self.width(), base) base += line.getLineSpacing() self.setFixedHeight(base) super(Definition, self).resizeEvent(event) return - _downFrag: Optional[Fragment | None] = None + _downClickable: Optional[Clickable] = 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 + print(f"mousePressEvent: {event.position()}") + for clk in self._buttons: + if clk["bb"].contains(event.position()): + print("inside") + self._downClickable = clk 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() + if (self._downClickable is not None and + self._downClickable["bb"].contains(event.position()) ): - audio = self._downFrag.audio().url() - print(audio) - self.pronounce.emit(audio) - print("emit done") - self._downFrag = None + print(f"mousePressPseudoEvent: {event.position()}") + clk = self._downClickable + bb = clk['bb'] + print(f"({bb.left()}-{bb.right()}, {bb.top()}-{bb.bottom()})", clk["fmt"].anchorHref(),) + #self.pronounce.emit(audio) + self._downClickable = None return - self._downFrag = None + self._downClickable = None return super().mouseReleaseEvent(event) def paintEvent(self, _: Optional[QPaintEvent]) -> None: # noqa painter = QPainter(self) - painter.save() painter.setBrush(QBrush()) painter.setPen(QColor("white")) - + red = QColor("red") # # 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, @@ -577,11 +530,17 @@ class Definition(QWidget): # 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.paintEvent(painter) - painter.restore() + for idx, line in enumerate(self._lines): + text = '' + for frag in line.getLine(): + text += frag.text() + '_' + line.paintEvent(painter) + green = QColor("green") + for clickRect in self._buttons: + painter.setPen(green) + painter.drawRect(clickRect['bb']) + painter.setPen(red) + bb = clickRect['frag'].layout().boundingRect() + bb.moveTo(clickRect['frag'].layout().position()) + painter.drawRect(bb) return diff --git a/lib/utils.py b/lib/utils.py index 4e492b1..9f834fd 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -2,7 +2,7 @@ from typing import NoReturn, Self from PyQt6.QtCore import QCoreApplication, QDir, QStandardPaths, Qt -from PyQt6.QtGui import QColor, QFont, QFontDatabase +from PyQt6.QtGui import QColor, QFont, QFontDatabase, QTextCharFormat from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkDiskCache from PyQt6.QtSql import QSqlQuery @@ -41,11 +41,58 @@ class Resources: subduedBackground: QColor + headerFormat = QTextCharFormat() + labelFormat = QTextCharFormat() + subduedFormat = QTextCharFormat() + subduedItalicFormat = QTextCharFormat() + sOnSFormat = QTextCharFormat() + subduedLabelFormat = QTextCharFormat() + phonticFormat = QTextCharFormat() + boldFormat = QTextCharFormat() + boldOnSFormat = QTextCharFormat() + italicFormat = QTextCharFormat() + textFormat = QTextCharFormat() + smallCapsFormat = QTextCharFormat() + def __new__(cls: type[Self]) -> Self: if cls._instance: return cls._instance cls._instance = super(Resources, cls).__new__(cls) # + # colors + # + cls.baseColor = QColor(Qt.GlobalColor.white) + cls.linkColor = QColor("#4a7d95") + cls.subduedColor = QColor(Qt.GlobalColor.gray) + cls.subduedBackground = QColor("#444") + # + # Formats + # + LARGE = 36 + MEDIUM = 22 + SMALL = 18 + cls.headerFormat.setFontPointSize(LARGE) + cls.labelFormat.setFontPointSize(MEDIUM) + cls.sOnSFormat.setForeground(cls.subduedColor) + #cls.sOnSFormat.setBackground(cls.subduedBackground) + cls.sOnSFormat.setFontPointSize(SMALL) + cls.subduedFormat.setForeground(cls.subduedColor) + cls.subduedFormat.setFontPointSize(SMALL) + cls.subduedLabelFormat.setForeground(cls.subduedColor) + cls.subduedLabelFormat.setFontPointSize(SMALL) + cls.phonticFormat.setFont(QFontDatabase.font("Gentium", None,20)) + cls.phonticFormat.setFontPointSize(SMALL) + cls.boldFormat.setFontWeight(QFont.Weight.Bold) + cls.boldFormat.setFontPointSize(SMALL) + cls.boldOnSFormat.setFontWeight(QFont.Weight.Bold) + cls.boldOnSFormat.setFontPointSize(SMALL) + cls.boldOnSFormat.setBackground(cls.subduedBackground) + cls.italicFormat.setFontItalic(True) + cls.italicFormat.setFontPointSize(SMALL) + cls.textFormat.setFontPointSize(SMALL) + cls.smallCapsFormat.setFontPointSize(SMALL) + cls.smallCapsFormat.setFontCapitalization(QFont.Capitalization.SmallCaps) + # # Fonts # cls.headerFont = QFontDatabase.font("OpenDyslexic", None, 10) @@ -68,13 +115,6 @@ class Resources: cls.phonicFont = QFontDatabase.font("Gentium", None, 10) cls.phonicFont.setPixelSize(20) - # - # colors - # - cls.baseColor = QColor(Qt.GlobalColor.white) - cls.linkColor = QColor("#4a7d95") - cls.subduedColor = QColor(Qt.GlobalColor.gray) - cls.subduedBackground = QColor("#444") # # Setup the Network Manager # diff --git a/lib/words.py b/lib/words.py index c97012d..2dbf44c 100644 --- a/lib/words.py +++ b/lib/words.py @@ -115,13 +115,3 @@ class Word: return lines except KeyError: raise Exception(f"Unknown source: {self.current['source']}") - - -class DefinitionArea(QScrollArea): - def __init__(self, w: Word, *args: Any, **kwargs: Any) -> None: - super(DefinitionArea, self).__init__(*args, *kwargs) - d = Definition(w) - self.setWidget(d) - self.setWidgetResizable(True) - self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) - return diff --git a/plugins/merriam-webster.py b/plugins/merriam-webster.py index 610ce31..b562eaf 100644 --- a/plugins/merriam-webster.py +++ b/plugins/merriam-webster.py @@ -2,8 +2,8 @@ import json import re from typing import Any, Literal, NotRequired, TypedDict, cast -from PyQt6.QtCore import QEventLoop, Qt, QUrl -from PyQt6.QtGui import QColor, QFont +from PyQt6.QtCore import QEventLoop, QUrl +from PyQt6.QtGui import QFont, QFontDatabase, QTextCharFormat, QTextLayout from PyQt6.QtNetwork import QNetworkRequest from trycast import trycast @@ -151,6 +151,18 @@ class DefinitionSection(TypedDict): sls: NotRequired[list[str]] sseq: Any # list[list[Pair]] +DefinedRunOn = TypedDict( + "DefinedRunOn", + { + "drp": str, + "def": list[DefinitionSection], + "et": NotRequired[list[Pair]], + "lbs": NotRequired[list[str]], + "prs": NotRequired[list[Pronunciation]], + "sls": NotRequired[list[str]], + "vrs": NotRequired[list[Variant]] + } +) Definition = TypedDict( "Definition", @@ -302,13 +314,9 @@ def getFirstSound(definition: Any) -> QUrl: return QUrl() -def do_prs(prs: list[Pronunciation] | None) -> list[Fragment]: +def do_prs(frag: Fragment, prs: list[Pronunciation] | None) -> None: assert prs is not None r = Resources() - frags: list[Fragment] = [] - font = r.labelFont - linkColor = r.linkColor - subduedColor = r.subduedColor for pr in prs: if "pun" in pr: @@ -316,22 +324,30 @@ def do_prs(prs: list[Pronunciation] | None) -> list[Fragment]: else: pun = " " if "l" in pr: - frags.append( - Fragment(pr["l"] + pun, r.italicFont, color=subduedColor) - ) - frag = Fragment(pr["mw"], font, color=subduedColor) + frag.addText(pr["l"] + pun, r.subduedItalicFormat) + fmt = r.phonticFormat if "sound" in pr: - frag.setAudio(soundUrl(pr["sound"])) - frag.setColor(linkColor) - frags.append(frag) - frags.append(Fragment(" ", r.phonicFont)) + fmt = QTextCharFormat(r.phonticFormat) + fmt.setAnchor(True) + fmt.setAnchorHref(soundUrl(pr["sound"]).toString()) + fmt.setForeground(r.linkColor) + #text = pr["mw"] +' \N{SPEAKER} ' + text = pr["mw"] +' ' + else: + text = pr['mw'] + ' ' + print(f"text: {text}, length: {len(text)}") + frag.addText(text, fmt) if "l2" in pr: - frags.append(Fragment(pun + pr["l2"], font, color=subduedColor)) - return frags + 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 def do_aq(aq: AttributionOfQuote | None) -> list[Line]: assert aq is not None + raise NotImplementedError("aq") return [] @@ -341,7 +357,8 @@ def do_vis(vis: list[VerbalIllustration] | None, indent=0) -> list[Line]: lines: list[Line] = [] for vi in vis: line = Line() - frag = Fragment(vi["t"], r.textFont, color=r.subduedColor) + frag = Fragment() + frag.addText(vi['t'], r.subduedFormat) if indent > 0: frag.setIndent(indent) line.addFragment(frag) @@ -376,90 +393,95 @@ def do_uns( return (frags, lines) -def do_dt( - dt: list[list[Pair]] | None, indent: int -) -> tuple[list[Fragment], list[Line]]: +def do_dt(frag, dt: list[list[Pair]] | None, indent: int) -> list[Line]: assert dt is not None - frags: list[Fragment] = [] lines: list[Line] = [] r = Resources() first = True for entry in dt: for pair in entry: if pair["objType"] == "text": - frag = Fragment(pair["obj"], r.textFont, color=r.baseColor) - frag.setIndent(indent) if first: - frags.append(frag) + frag.setIndent(indent) + frag.addText(pair["obj"], r.textFormat) else: line = Line() + f = Fragment() + f.setIndent(indent) + f.addText(pair["obj"], r.textFormat) line.addFragment(frag) lines.append(line) elif pair["objType"] == "vis": + first = False lines += do_vis( trycast(list[VerbalIllustration], pair["obj"]), indent ) elif pair["objType"] == "uns": + first = False (newFrags, newLines) = do_uns( trycast(list[list[list[Pair]]], pair["obj"]), indent ) - frags += newFrags - lines += newLines + #frags += newFrags + #lines += newLines + raise NotImplementedError("uns") else: print(json.dumps(pair, indent=2)) raise NotImplementedError( f"Unknown or unimplimented element {pair['objType']}" ) first = False - return (frags, lines) + return lines def do_sense( - sense: Sense | None, indent: int = 3 -) -> tuple[list[Fragment], list[Line]]: - if sense is None: - return ([], []) + sense: Sense | None, indent: int = 3 +) -> tuple[Fragment, list[Line]]: + assert sense is not None lines: list[Line] = [] - frags: list[Fragment] = [] r = Resources() + first = True + frag = Fragment() for k, v in sense.items(): if k == "sn": continue elif k == "dt": - (newFrags, newLines) = do_dt( - trycast(list[list[Pair]], sense["dt"]), indent - ) - frags += newFrags + newLines = do_dt(frag, trycast(list[list[Pair]], sense["dt"]), indent) + if first: + firstFrag = frag + frag = Fragment() + else: + line = Line() + line.addFragment(frag) + lines.append(line) lines += newLines elif k == "sdsense": # XXX - This needs to expand to handle et, ins, lbs, prs, sgram, sls, vrs sdsense = trycast(DividedSense, v) assert sdsense is not None - frag = Fragment( - sdsense["sd"] + " ", r.italicFont, color=r.baseColor - ) + frag = Fragment() frag.setIndent(indent) + frag.addText(sdsense["sd"] + ' ', r.italicFormat) line = Line() line.addFragment(frag) - (newFrags, newLines) = do_dt( - trycast(list[list[Pair]], sdsense["dt"]), indent=indent - ) - line.addFragment(newFrags) - lines.append(line) + newLines = do_dt(frag, trycast(list[list[Pair]], sdsense["dt"]), indent=indent) + if first: + firstFrag = frag + frag = Fragment() + else: + line = Line() + line.addFragment(frag) + lines.append(line) lines += newLines elif k == "sls": labels = trycast(list[str], v) assert labels is not None - frag = Fragment( - ", ".join(labels) + " ", r.boldFont, color=r.subduedColor - ) - frag.setIndent(indent) - frag.setBackground(r.subduedBackground) - frags.append(frag) + frag.addText(", ".join(labels) + " ",r.boldOnSFormat) + elif "lbs" == k: + pass else: print(k, v) raise NotImplementedError(f"Unknown or unimplimented element {k}") - return (frags, lines) + return (firstFrag, lines) def do_pseq( @@ -475,28 +497,23 @@ def do_pseq( for pair in entry: if pair["objType"] == "bs": sense = pair["obj"]["sense"] - (newFrags, newLines) = do_sense( + (frag, newLines) = do_sense( trycast(Sense, sense), indent=indent ) - frags += newFrags + frags.append(frag) lines += newLines newLine = True elif pair["objType"] == "sense": - frag = Fragment(f"({count})", r.textFont, color=r.baseColor) - frag.setIndent(indent) + sn = Fragment() + sn.addText(f"({count})", r.textFormat) + sn.setIndent(indent) + (frag, newLines) = do_sense(trycast(Sense, pair["obj"]), indent=indent + 1) if newLine: line = Line() + line.addFragment(sn) line.addFragment(frag) else: - frags.append(frag) - (newFrags, newLines) = do_sense( - trycast(Sense, pair["obj"]), indent=indent + 1 - ) - if newLine: - line.addFragment(newFrags) - lines.append(line) - else: - frags += newFrags + frags = [sn, frag, ] newLine = True lines += newLines count += 1 @@ -510,17 +527,17 @@ def do_pseq( def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]: lines: list[Line] = [] r = Resources() + line = Line() for outer, item_o in enumerate(sseq): - line = Line() - frag = Fragment(str(outer + 1), r.boldFont, color=r.baseColor) + frag = Fragment() frag.setIndent(1) + frag.addText(str(outer +1), r.boldFormat) line.addFragment(frag) for inner, item_i in enumerate(item_o): indent = 2 if len(item_o) > 1: - frag = Fragment( - chr(ord("a") + inner), r.boldFont, color=r.baseColor - ) + frag = Fragment() + frag.addText(chr(ord("a") + inner), r.boldFormat) frag.setIndent(2) line.addFragment(frag) indent = 3 @@ -528,8 +545,8 @@ def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]: objType = pair["objType"] if objType == "sense": sense = trycast(Sense, pair["obj"]) - (frags, newlines) = do_sense(sense, indent=indent) - line.addFragment(frags) + (frag, newlines) = do_sense(sense, indent=indent) + line.addFragment(frag) lines.append(line) line = Line() lines += newlines @@ -542,6 +559,7 @@ def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]: line = Line() lines += newlines elif objType == "bs": + raise NotImplementedError("bs") sense = pair["obj"]["sense"] (newFrags, newLines) = do_sense( trycast(Sense, sense), indent=indent @@ -557,18 +575,15 @@ def do_sseq(sseq: list[list[list[Pair]]]) -> list[Line]: return lines -def do_ins(inflections: list[Inflection] | None) -> list[Fragment]: +def do_ins(frag: Fragment, inflections: list[Inflection] | None) -> None: assert inflections is not None r = Resources() - frags: list[Fragment] = [] sep = "" for inflection in inflections: if sep == "; ": - frag = Fragment("; ", font=r.boldFont, color=r.baseColor) - frags.append(frag) + frag.addText(sep, r.boldFormat) elif sep != "": - frag = Fragment(sep, font=r.italicFont, color=r.baseColor) - frags.append(frag) + frag.addText(sep, r.italicFormat) if "ifc" in inflection: text = inflection["ifc"] @@ -577,19 +592,18 @@ def do_ins(inflections: list[Inflection] | None) -> list[Fragment]: else: raise ValueError(f"Missing 'if' or 'ifc' in {inflection}") - frag = Fragment(text, r.boldFont, color=r.baseColor) - frags.append(frag) + text = re.sub(r'\*', '\u00b7', text) + frag.addText(text, r.boldFormat) sep = "; " if "il" in inflection: sep = " " + inflection["il"] + " " if "prs" in inflection: - newFrags = do_prs(trycast(list[Pronunciation], inflection["prs"])) - frags += newFrags + do_prs(frag, trycast(list[Pronunciation], inflection["prs"])) if "spl" in inflection: raise NotImplementedError( f"We haven't implimented 'spl' for inflection: {inflection}" ) - return frags + return def do_ets(ets: list[list[Pair]] | None) -> list[Line]: @@ -600,17 +614,15 @@ def do_ets(ets: list[list[Pair]] | None) -> list[Line]: for pair in et: if pair["objType"] == "text": line = Line() - line.addFragment( - Fragment(pair["obj"], r.textFont, color=r.baseColor) - ) + frag = Fragment('', r.textFont) + frag.addText(pair['obj'], r.textFormat) + line.addFragment(frag) lines.append(line) elif pair["objType"] == "et_snote": line = Line() - line.addFragment( - Fragment( - "Note: " + pair["obj"], r.textFont, color=r.baseColor - ) - ) + frag = Fragment('', r.textFont) + frag.addText(f"Note: {pair['obj']}",r.textFormat) + line.addFragment(frag) lines.append(line) else: raise NotImplementedError( @@ -625,7 +637,9 @@ def do_def(entry: DefinitionSection) -> list[Line]: lines: list[Line] = [] if "vd" in entry: line = Line() - line.addFragment(Fragment(entry["vd"], r.italicFont, color=r.linkColor)) + frag = Fragment() + frag.addText(entry["vd"], r.italicFormat) + line.addFragment(frag) lines.append(line) # # sseg is required @@ -634,6 +648,46 @@ def do_def(entry: DefinitionSection) -> list[Line]: lines += do_sseq(sseq) return lines +def do_vrs(vrs: list[Variant]|None) -> Line: + assert vrs is not None + r = Resources() + line = Line() + frag = Fragment() + frag.addText('variants: ', r.sOnSFormat) + for var in vrs: + if 'vl' in var: + frag.addText(var['vl']+' ', r.italicFormat) + if 'spl' in var: + frag.addText(var['spl']+' ', r.sOnSFormat) + frag.addText(var['va'], r.boldFormat) + if 'prs' in var: + frag.addText(' ') + do_prs(frag, trycast(list[Pronunciation], var['prs'])) + frag.addText(' ') + line.addFragment(frag) + return line + +def do_dros(dros: list[DefinedRunOn]|None) -> list[Line]: + assert dros is not None + r = Resources() + lines: list[Line] = [] + for dro in dros: + line = Line() + frag = Fragment() + frag.addText(dro["drp"], r.boldFormat) + line.addFragment(frag) + lines.append(line) + for entry in dro['def']: + lines += do_def(entry) + for k,v in dro.items(): + if 'drp' == k or 'def' == k: + continue + elif 'et' == k: + lines += do_ets(trycast(list[list[Pair]], v)) + else: + raise NotImplementedError(f"Key of {k}") + return lines + def getDef(defines: Any) -> list[Line]: Line.setParseText(parseText) @@ -667,7 +721,7 @@ def getDef(defines: Any) -> list[Line]: used[k] = 0 ets: list[Line] = [] - + phrases: list[Line] = [] for count, work in enumerate(workList): testId = work["meta"]["id"].lower().split(":")[0] # @@ -679,30 +733,26 @@ def getDef(defines: Any) -> list[Line]: # Create the First line from the hwi, [ahws] and fl # line = Line() + frag = Fragment() hwi = trycast(HeadWordInformation, work["hwi"]) assert hwi is not None hw = re.sub(r"\*", "", hwi["hw"]) - line.addFragment(Fragment(hw, r.headerFont, color=r.baseColor)) + frag.addText(hw,r.headerFormat) if "ahws" in work: ahws = trycast(list[AlternanteHeadword], work["ahws"]) assert ahws is not None for ahw in ahws: hw = re.sub(r"\*", "", ahw["hw"]) - line.addFragment( - Fragment(", " + hw, r.headerFont, color=r.baseColor) - ) + frag.addText(", " + hw) if entries > 1: - frag = Fragment( - f" {count + 1} of {entries} ", r.textFont, color=r.subduedColor - ) - frag.setBackground(r.subduedBackground) - line.addFragment(frag) + frag.addText(f" {count + 1} of {entries} ", r.sOnSFormat) if "fl" in work: text = work["fl"] used[text] += 1 if uses[text] > 1: text += f" ({used[text]})" - line.addFragment(Fragment(text, r.labelFont, color=r.baseColor)) + frag.addText(text, r.labelFormat) + line.addFragment(frag) lines.append(line) # @@ -710,55 +760,65 @@ def getDef(defines: Any) -> list[Line]: # While 'prs' is optional, the headword is not. This gets us what we want. # line = Line() + frag = Fragment() if hwi["hw"].find("*") >= 0: hw = re.sub(r"\*", "\u00b7", hwi["hw"]) - line.addFragment( - Fragment(hw + " ", r.textFont, color=r.subduedColor) - ) + frag.addText(hw + " ", r.subduedFormat) if "prs" in hwi: - newFrags = do_prs(trycast(list[Pronunciation], hwi["prs"])) - line.addFragment(newFrags) + do_prs(frag, trycast(list[Pronunciation], hwi["prs"])) + line.addFragment(frag) lines.append(line) line = Line() + frag = Fragment() + if 'vrs' in work: + lines.append(do_vrs(trycast(list[Variant], work['vrs']))) if "ins" in work: inflections = trycast(list[Inflection], work["ins"]) - newFrags = do_ins(inflections) - line = Line() - line.addFragment(newFrags) + do_ins(frag,inflections) + line.addFragment(frag) 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 as e: - print(e) + except NotImplementedError: + raise + if "dros" in work: + dros = trycast(list[DefinedRunOn], work["dros"]) + if len(phrases) < 1: + frag = Fragment() + frag.addText("Phrases", r.labelFormat) + line = Line() + line.addFragment(frag) + phrases.append(line) + phrases += do_dros(dros) if "et" in work: line = Line() - line.addFragment( - Fragment( - f"{work['fl']} ({used[work['fl']]})", - r.labelFont, - color=r.baseColor, - ) - ) - ets.append(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 [ - "meta", - "hom", - "hwi", - "fl", - "def", - "ins", - "prs", - "et", - "date", - "shortdef", + "meta", + "hom", + "hwi", + "fl", + "def", + "ins", + "prs", + "et", + "date", + "shortdef", + "vrs", + "dros", ]: - # raise NotImplementedError(f"Unknown key {k} in work") - print(f"Unknown key {k} in work") + raise NotImplementedError(f"Unknown key {k} in work") + if len(phrases) > 0: + lines += phrases if len(ets) > 0: line = Line() line.addFragment(Fragment("Etymology", r.labelFont, color=r.baseColor)) @@ -766,185 +826,116 @@ def getDef(defines: Any) -> list[Line]: lines += ets return lines - -def parseText(frag: Fragment) -> list[Fragment]: - org = frag.text() - if frag.asis(): - return [frag] - - # - # Get the fonts we might need. - # We can't use Resources() because we don't know the original font. - textFont = QFont(frag.font()) - textFont.setWeight(QFont.Weight.Normal) - textFont.setItalic(False) - textFont.setCapitalization(QFont.Capitalization.MixedCase) - boldFont = QFont(textFont) - boldFont.setBold(True) - italicFont = QFont(textFont) - italicFont.setItalic(True) - smallCapsFont = QFont(textFont) - smallCapsFont.setCapitalization(QFont.Capitalization.SmallCaps) - scriptFont = QFont(textFont) - scriptFont.setPixelSize(int(scriptFont.pixelSize() / 4)) - boldItalicFont = QFont(boldFont) - boldItalicFont.setItalic(True) - boldSmallCapsFont = QFont(smallCapsFont) - boldSmallCapsFont.setBold(True) - capsFont = QFont(textFont) - capsFont.setCapitalization(QFont.Capitalization.AllUppercase) - # - # Default color: - # - baseColor = frag.color() +def replaceCode(code:str) -> tuple[str, QTextCharFormat]: r = Resources() - - results: list[Fragment] = [] - while True: - text = frag.text() - start = text.find("{") - if start < 0: - results.append(frag) - return results - if start > 0: - newFrag = Fragment(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 :]) - oldFont = QFont(frag.font()) - if token == "bc": - newFrag = Fragment(": ", boldFont, color=baseColor) - newFrag.setIndent(frag.indent()) - results.append(newFrag) - 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(boldFont) - elif token in ["it", "qword", "wi"]: - frag.setFont(italicFont) - elif token == "sc": - frag.setFont(smallCapsFont) - elif token in ["inf", "sup"]: - frag.setFont(scriptFont) - elif token == "phrase": - frag.setFont(boldItalicFont) - elif token == "parahw": - frag.setFont(boldSmallCapsFont) - 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()) + fmt = QTextCharFormat() + if code == 'bc': + fmt.setFontWeight(QFont.Weight.Bold) + return (': ', fmt) + elif code == 'ldquo': + return ('\u201c', fmt) + elif code == 'rdquo': + return ('\u201d', fmt) + fmt.setAnchor(True) + fmt.setForeground(r.linkColor) + fmt.setFontUnderline(True) + fmt.setUnderlineColor(r.linkColor) + fmt.setFontUnderline(True) + fields = code.split('|') + token = fields[0] + if token == 'a_link': + text = fields[1] + fmt.setAnchorHref(fields[1]) + elif token in ['d_link', 'et_link', 'mat', 'sx', 'i_link']: + text = fields[1] + pre = 'word://' + if fields[2] == '': + fmt.setAnchorHref(pre+fields[1]) + else: + fmt.setAnchorHref(pre+fields[2]) + if token == 'i_link': + fmt.setFontItalic(True) + elif token == 'sx': + fmt.setFontCapitalization(QFont.Capitalization.SmallCaps) + elif token == 'dxt': + if fields[3] == 'illustration': + fmt.setAnchorHref('article://'+fields[2]) + elif fields[3] == 'table': + fmt.setAnchorHref('table://'+fields[2]) + elif fields[3] != "": + fmt.setAnchorHref('sense://'+fields[3]) else: - raise NotImplementedError(f"Unknown block marker: {token}") - results += parseText(frag) - frag = results.pop() - frag.setFont(oldFont) - text = frag.text() - if not text.startswith("{/" + token + "}"): - raise NotImplementedError( - 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(italicFont) - elif token == "sx": - frag.setFont(capsFont) - 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] + fmt.setAnchorHref('word://'+fields[1]) + elif token == 'et_link': + if fields[2] != '': + fmt.setAnchorHref('etymology://'+fields[2]) else: - raise NotImplementedError(f"Unknown code: {token} in {org}") - newFrag = Fragment(frag) - newFrag.setText(htext) - newFrag.setWRef(wref) - newFrag.setTarget(target) - newFrag.setColor(r.linkColor) - results.append(newFrag) - frag.setFont(oldFont) - text = frag.text() - continue - raise NotImplementedError( - f"Unable to locate a known token {token} in {org}" - ) + fmt.setAnchorHref('etymology://' + 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]]: + close = text.find('}') + code = text[1:close] + text = text[close+1:-(close+2)] + fmt = QTextCharFormat() + if code == 'b': + fmt.setFontWeight(QFont.Weight.Bold) + elif code == 'inf': + fmt.setVerticalAlignment(QTextCharFormat.VerticalAlignment.AlignSubScript) + elif code == 'it': + fmt.setFontItalic(True) + elif code == 'sc': + fmt.setFontCapitalization(QFont.Capitalization.SmallCaps) + fr = QTextLayout.FormatRange() + fr.start = offset + fr.length = len(text) + fr.format = fmt + return (text, [fr,]) + +def parseText(frag: Fragment) -> QTextLayout: + layout = frag.layout() + text = layout.text() + formats = layout.formats() + REPLACE_TEXT = [ + 'bc','a_link', 'd_link', 'dxt', 'et_link', 'i_link', 'mat', + 'sx' + ] + pos = 0 + start = text[pos:].find('{') + + while start >= 0: + start += pos + end = text[start+1:].find('}') + end += start + code = text[start+1:end+1] + pos = end+2 + for maybe in REPLACE_TEXT: + if code.startswith(maybe): + (repl, tfmt) = replaceCode(code) + text = text[:start] + repl + text[end+2:] + fmt = QTextLayout.FormatRange() + fmt.format = tfmt + fmt.start=start + fmt.length = len(repl) + formats.append(fmt) + pos = start + len(repl) + code = '' + break + if code != '': + needle = f'{{/{code}}}' + codeEnd = text[start:].find(needle) + codeEnd += start+len(needle) + straw = text[start:codeEnd] + (repl, frs) = markup(start, straw) + fmt = QTextLayout.FormatRange() + formats += frs + text = text[:start] + repl + text[codeEnd:] + pos = start + len(repl) + start = text[pos:].find('{') + layout.setFormats(formats) + layout.setText(text) + return layout