Layout is good, click boxes is wrong

This commit is contained in:
Christopher T. Johnson
2024-05-07 11:26:15 -04:00
parent 51b1121176
commit 7d2532d775
6 changed files with 568 additions and 574 deletions

38
deftest.py Normal file → Executable file
View File

@@ -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())

View File

@@ -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

View File

@@ -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

View File

@@ -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
#

View File

@@ -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

View File

@@ -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