Add a QTextDocument generator class

This commit is contained in:
Veronica Berglyd Olsen
2024-05-21 18:12:08 +02:00
parent aadad6aa8c
commit 1203dd639b
3 changed files with 304 additions and 15 deletions
+1
View File
@@ -152,6 +152,7 @@ class Tokenizer(ABC):
self._doComments = False # Also process comments
self._doKeywords = False # Also process keywords like tags and references
self._skipKeywords = set() # Keywords to ignore
self._keepBreaks = True # Keep line breaks in paragraphs
# Margins
self._marginTitle = (1.000, 0.500)
+287
View File
@@ -0,0 +1,287 @@
"""
novelWriter Markdown Text Converter
=====================================
File History:
Created: 2021-02-06 [1.2b1] ToMarkdown
This file is a part of novelWriter
Copyright 20182024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import logging
from PyQt5.QtGui import (
QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat, QTextCursor,
QTextDocument
)
from novelwriter import SHARED
from novelwriter.constants import nwHeaders
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import T_Formats, Tokenizer
from novelwriter.types import (
QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight, QtPageBreakAfter,
QtPageBreakBefore
)
logger = logging.getLogger(__name__)
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
class ToQTextDocument(Tokenizer):
"""Core: QTextDocument Writer
Extend the Tokenizer class to generate a QTextDocument output. This
is intended for usage in the document viewer and build tool preview.
"""
def __init__(self, project: NWProject) -> None:
super().__init__(project)
self._document = QTextDocument()
self._document.setUndoRedoEnabled(False)
self._styles: dict[int, T_TextStyle] = {}
self._init = False
return
def initDocument(self, font: QFont | None = None) -> None:
"""Initialise all computed values of the document."""
if not font:
font = QFont()
font.setFamily(self._textFont)
font.setPointSize(self._textSize)
self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
self._document.clear()
self._document.setDefaultFont(font)
qMetric = QFontMetrics(font)
mScale = qMetric.height()
fPt = font.pointSizeF()
self._mHead = {
self.T_TITLE: (mScale * self._marginTitle[0], mScale * self._marginTitle[1]),
self.T_HEAD1: (mScale * self._marginHead1[0], mScale * self._marginHead1[1]),
self.T_HEAD2: (mScale * self._marginHead2[0], mScale * self._marginHead2[1]),
self.T_HEAD3: (mScale * self._marginHead3[0], mScale * self._marginHead3[1]),
self.T_HEAD4: (mScale * self._marginHead4[0], mScale * self._marginHead4[1]),
}
self._sHead = {
self.T_TITLE: nwHeaders.H_SIZES.get(1, 1.0) * fPt,
self.T_HEAD1: nwHeaders.H_SIZES.get(1, 1.0) * fPt,
self.T_HEAD2: nwHeaders.H_SIZES.get(2, 1.0) * fPt,
self.T_HEAD3: nwHeaders.H_SIZES.get(3, 1.0) * fPt,
self.T_HEAD4: nwHeaders.H_SIZES.get(4, 1.0) * fPt,
}
self._mText = (mScale * self._marginText[0], mScale * self._marginText[1])
self._mMeta = (mScale * self._marginMeta[0], mScale * self._marginMeta[1])
self._defaultChar = QTextCharFormat()
self._defaultChar.setForeground(SHARED.theme.colText)
self._defaultBlock = QTextBlockFormat()
self._defaultBlock.setTopMargin(self._mText[0])
self._defaultBlock.setBottomMargin(self._mText[1])
self._init = True
return
##
# Properties
##
@property
def document(self) -> QTextDocument:
"""Return the document."""
return self._document
##
# Class Methods
##
def clearDocument(self) -> None:
"""Clear the document so the class can be reused."""
self._document.clear()
return
def doConvert(self) -> None:
"""Write text tokens into the document."""
if not self._init:
return
self._document.blockSignals(True)
cursor = QTextCursor(self._document)
def newBlock(bFmt: QTextBlockFormat) -> None:
if cursor.position() > 0:
cursor.insertBlock(bFmt)
else:
cursor.setBlockFormat(bFmt)
pText = []
pFmt = None
lineSep = "\n" if self._keepBreaks else " "
for tType, _, tText, tFormat, tStyle in self._tokens:
# Styles
bFmt = QTextBlockFormat(self._defaultBlock)
if tStyle is not None:
if tStyle & self.A_LEFT:
bFmt.setAlignment(QtAlignLeft)
elif tStyle & self.A_RIGHT:
bFmt.setAlignment(QtAlignRight)
elif tStyle & self.A_CENTRE:
bFmt.setAlignment(QtAlignCenter)
elif tStyle & self.A_JUSTIFY:
bFmt.setAlignment(QtAlignJustify)
if tStyle & self.A_PBB:
bFmt.setPageBreakPolicy(QtPageBreakBefore)
if tStyle & self.A_PBA:
bFmt.setPageBreakPolicy(QtPageBreakAfter)
if tStyle & self.A_Z_BTMMRG:
bFmt.setBottomMargin(0.0)
if tStyle & self.A_Z_TOPMRG:
bFmt.setTopMargin(0.0)
if tStyle & self.A_IND_L:
bFmt.setLeftMargin(self._blockIndent)
if tStyle & self.A_IND_R:
bFmt.setRightMargin(self._blockIndent)
if tType == self.T_EMPTY:
if pText and pFmt is not None:
tTemp = (lineSep.join(pText)).rstrip(" ")
newBlock(pFmt)
cursor.insertText(tTemp, self._defaultChar)
pText = []
pFmt = None
elif tType in self.L_HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, bFmt)
newBlock(bFmt)
cursor.insertText(tText, cFmt)
elif tType == self.T_SEP:
newBlock(bFmt)
cursor.insertText(tTemp, self._defaultChar)
elif tType == self.T_SKIP:
newBlock(bFmt)
elif tType == self.T_TEXT:
if pFmt is None:
pFmt = bFmt
pText.append(self._formatText(tText, tFormat).rstrip())
elif tType == self.T_SYNOPSIS and self._doSynopsis:
pass
elif tType == self.T_SHORT and self._doSynopsis:
pass
elif tType == self.T_COMMENT and self._doComments:
pass
elif tType == self.T_KEYWORD and self._doKeywords:
pass
self._document.blockSignals(False)
return
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
# if self._usedNotes:
# tags = STD_MD if self._genMode == self.M_STD else EXT_MD
# footnotes = self._localLookup("Footnotes")
# lines = []
# lines.append(f"### {footnotes}\n\n")
# for key, index in self._usedNotes.items():
# if content := self._footnotes.get(key):
# marker = f"{index}. "
# text = self._formatText(*content, tags)
# lines.append(f"{marker}{text}\n")
# lines.append("\n")
# result = "".join(lines)
# self._result += result
# self._fullMD.append(result)
return
##
# Internal Functions
##
def _formatText(self, text: str, tFmt: T_Formats) -> str:
"""Apply formatting tags to text."""
temp = text
# for pos, fmt, data in reversed(tFmt):
# md = ""
# if fmt == self.FMT_FNOTE:
# if data in self._footnotes:
# index = len(self._usedNotes) + 1
# self._usedNotes[data] = index
# md = f"[{index}]"
# else:
# md = "[ERR]"
# else:
# md = tags.get(fmt, "")
# temp = f"{temp[:pos]}{md}{temp[pos:]}"
return temp
def _formatKeywords(self, text: str, style: int) -> str:
"""Apply Markdown formatting to keywords."""
# valid, bits, _ = self._project.index.scanThis("@"+text)
# if not valid or not bits:
# return ""
result = ""
# if bits[0] in nwLabels.KEY_NAME:
# result += f"**{self._localLookup(nwLabels.KEY_NAME[bits[0]])}:** "
# if len(bits) > 1:
# result += ", ".join(bits[1:])
# result += " \n" if style & self.A_Z_BTMMRG else "\n\n"
return result
def _genHeadStyle(self, level: int, rFmt: QTextBlockFormat) -> T_TextStyle:
"""Generate a heading style set."""
mTop, mBottom = self._mHead.get(level, (0.0, 0.0))
bFmt = QTextBlockFormat(rFmt)
bFmt.setTopMargin(mTop)
bFmt.setBottomMargin(mBottom)
cFmt = QTextCharFormat(self._defaultChar)
cFmt.setForeground(SHARED.theme.colHead)
cFmt.setFontWeight(QFont.Weight.Bold)
cFmt.setFontPointSize(self._sHead.get(level, 1.0))
return bFmt, cFmt
+16 -15
View File
@@ -40,7 +40,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import cssCol
from novelwriter.constants import nwHeaders, nwUnicode
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.toqdocument import ToQTextDocument
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
from novelwriter.error import logException
from novelwriter.extensions.eventfilters import WheelEventFilter
@@ -202,22 +202,22 @@ class GuiDocViewer(QTextBrowser):
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
sPos = self.verticalScrollBar().value()
aDoc = ToHtml(SHARED.project)
aDoc.setPreview(True)
aDoc.setKeywords(True)
aDoc.setComments(CONFIG.viewComments)
aDoc.setSynopsis(CONFIG.viewSynopsis)
aDoc.setLinkHeadings(True)
qDoc = ToQTextDocument(SHARED.project)
qDoc.initDocument(CONFIG.textFont)
qDoc.setKeywords(True)
qDoc.setComments(CONFIG.viewComments)
qDoc.setSynopsis(CONFIG.viewSynopsis)
qDoc.setLinkHeadings(True)
# Be extra careful here to prevent crashes when first opening a
# project as a crash here leaves no way of recovering.
# See issue #298
try:
aDoc.setText(tHandle)
aDoc.doPreProcessing()
aDoc.tokenizeText()
aDoc.doConvert()
aDoc.appendFootnotes()
qDoc.setText(tHandle)
qDoc.doPreProcessing()
qDoc.tokenizeText()
qDoc.doConvert()
qDoc.appendFootnotes()
except Exception:
logger.error("Failed to generate preview for document with handle '%s'", tHandle)
logException()
@@ -235,9 +235,10 @@ class GuiDocViewer(QTextBrowser):
self.setDocumentTitle(tHandle)
# Replace tabs before setting the HTML, and then put them back in
self.setHtml(aDoc.result.replace("\t", "!!tab!!"))
while self.find("!!tab!!"):
self.textCursor().insertText("\t")
# self.setHtml(qDoc.result.replace("\t", "!!tab!!"))
# while self.find("!!tab!!"):
# self.textCursor().insertText("\t")
self.setDocument(qDoc.document)
if self._docHandle == tHandle:
# This is a refresh, so we set the scrollbar back to where it was