Files
Veronica Berglyd Olsen c174f8f931 Update linting for main code
2025-08-27 21:00:09 +02:00

484 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
novelWriter QTextDocument Converter
=====================================
File History:
Created: 2024-05-21 [2.5b1] ToQTextDocument
This file is a part of novelWriter
Copyright (C) 2024 Veronica Berglyd Olsen and novelWriter contributors
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/>.
""" # noqa
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from PyQt6.QtCore import QMarginsF, QSizeF
from PyQt6.QtGui import (
QColor, QFont, QFontDatabase, QPageLayout, QPageSize, QTextBlockFormat,
QTextCharFormat, QTextCursor, QTextDocument, QTextFrameFormat
)
from PyQt6.QtPrintSupport import QPrinter
from novelwriter import __version__
from novelwriter.constants import nwStyles, nwUnicode
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
from novelwriter.formats.tokenizer import HEADING_BLOCKS, META_BLOCKS, Tokenizer
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakAuto,
QtPageBreakBefore, QtPropLineHeight, QtTransparent, QtVAlignNormal,
QtVAlignSub, QtVAlignSuper
)
if TYPE_CHECKING:
from pathlib import Path
from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__)
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
"""Insert a new block if not at the beginning of the document."""
if cursor.position() > 0:
cursor.insertBlock(bFmt)
else:
cursor.setBlockFormat(bFmt)
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._document.setDocumentMargin(0.0)
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = []
self._init = False
self._newPage = False
self._anchors = True
self._hWeight = QFont.Weight.Bold
self._dWeight = QFont.Weight.Normal
self._dItalic = False
self._dStrike = False
self._dUnderline = False
self._dpi = 96
self._pageSize = QPageSize(QPageSize.PageSizeId.A4)
self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
##
# Properties
##
@property
def document(self) -> QTextDocument:
"""Return the document."""
return self._document
##
# Setters
##
def setPageLayout(
self, width: float, height: float, top: float, bottom: float, left: float, right: float
) -> None:
"""Set the document page size and margins in millimetres."""
self._pageSize = QPageSize(QSizeF(width, height), QPageSize.Unit.Millimeter)
self._pageMargins = QMarginsF(left, top, right, bottom)
def setShowNewPage(self, state: bool) -> None:
"""Add markers for page breaks."""
self._newPage = state
def disableAnchors(self) -> None:
"""Disable anchors for when writing to file."""
self._anchors = False
##
# Class Methods
##
def initDocument(self, pdf: bool = False) -> None:
"""Initialise all computed values of the document."""
super().initDocument()
if pdf:
family = self._textFont.family()
style = self._textFont.styleName()
self._dpi = 1200 if QFontDatabase.isScalable(family, style) else 72
self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
self._document.clear()
self._document.setDefaultFont(self._textFont)
# Default Styles
self._dWeight = self._textFont.weight()
self._dItalic = self._textFont.italic()
self._dStrike = self._textFont.strikeOut()
self._dUnderline = self._textFont.underline()
# Header Weight
self._hWeight = QFont.Weight.Bold if self._boldHeads else self._dWeight
# Scaled Sizes
# ============
fPt = self._textFont.pointSizeF()
fPx = fPt*96.0/72.0 # 1 em in pixels
mPx = fPx * self._dpi/96.0
self._mHead = {
BlockTyp.TITLE: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]),
BlockTyp.PART: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]),
BlockTyp.HEAD1: (fPx * self._marginHead1[0], fPx * self._marginHead1[1]),
BlockTyp.HEAD2: (fPx * self._marginHead2[0], fPx * self._marginHead2[1]),
BlockTyp.HEAD3: (fPx * self._marginHead3[0], fPx * self._marginHead3[1]),
BlockTyp.HEAD4: (fPx * self._marginHead4[0], fPx * self._marginHead4[1]),
}
hScale = self._scaleHeads
self._sHead = {
BlockTyp.TITLE: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
BlockTyp.PART: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
BlockTyp.HEAD1: (nwStyles.H_SIZES.get(1, 1.0) * fPt) if hScale else fPt,
BlockTyp.HEAD2: (nwStyles.H_SIZES.get(2, 1.0) * fPt) if hScale else fPt,
BlockTyp.HEAD3: (nwStyles.H_SIZES.get(3, 1.0) * fPt) if hScale else fPt,
BlockTyp.HEAD4: (nwStyles.H_SIZES.get(4, 1.0) * fPt) if hScale else fPt,
}
self._mText = (fPx * self._marginText[0], fPx * self._marginText[1])
self._mMeta = (fPx * self._marginMeta[0], fPx * self._marginMeta[1])
self._mSep = (fPx * self._marginSep[0], fPx * self._marginSep[1])
self._mIndent = mPx * 2.0
self._tIndent = mPx * self._firstWidth
# Text Formats
# ============
self._blockFmt = QTextBlockFormat()
self._blockFmt.setTopMargin(self._mText[0])
self._blockFmt.setBottomMargin(self._mText[1])
self._blockFmt.setAlignment(QtAlignAbsolute)
self._blockFmt.setLineHeight(100.0*self._lineHeight, QtPropLineHeight)
self._charFmt = QTextCharFormat()
self._charFmt.setBackground(QtTransparent)
self._charFmt.setForeground(self._theme.text)
self._init = True
def doConvert(self) -> None:
"""Write text tokens into the document."""
if not self._init:
return
self._document.blockSignals(True)
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
for tType, tMeta, tText, tFormat, tStyle in self._blocks:
bFmt = QTextBlockFormat(self._blockFmt)
if tType in META_BLOCKS:
bFmt.setTopMargin(self._mMeta[0])
bFmt.setBottomMargin(self._mMeta[1])
elif tType == BlockTyp.SEP:
bFmt.setTopMargin(self._mSep[0])
bFmt.setBottomMargin(self._mSep[1])
if tStyle & BlockFmt.LEFT:
bFmt.setAlignment(QtAlignLeft)
elif tStyle & BlockFmt.RIGHT:
bFmt.setAlignment(QtAlignRight)
elif tStyle & BlockFmt.CENTRE:
bFmt.setAlignment(QtAlignCenter)
elif tStyle & BlockFmt.JUSTIFY:
bFmt.setAlignment(QtAlignJustify)
if tStyle & BlockFmt.PBB:
self._insertNewPageMarker(cursor)
bFmt.setPageBreakPolicy(QtPageBreakBefore)
if tStyle & BlockFmt.PBA:
bFmt.setPageBreakPolicy(QtPageBreakAfter)
if tStyle & BlockFmt.Z_BTM:
bFmt.setBottomMargin(0.0)
if tStyle & BlockFmt.Z_TOP:
bFmt.setTopMargin(0.0)
if tStyle & BlockFmt.IND_L:
bFmt.setLeftMargin(self._mIndent)
if tStyle & BlockFmt.IND_R:
bFmt.setRightMargin(self._mIndent)
if tStyle & BlockFmt.IND_T:
bFmt.setTextIndent(self._tIndent)
if tType == BlockTyp.TEXT:
newBlock(cursor, bFmt)
self._insertFragments(tText, tFormat, cursor, self._charFmt)
elif tType in HEADING_BLOCKS:
bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt)
for tPart in tText.split("\n"):
newBlock(cursor, bFmt)
cursor.insertText(tPart, cFmt)
bFmt.setPageBreakPolicy(QtPageBreakAuto)
elif tType in META_BLOCKS:
newBlock(cursor, bFmt)
self._insertFragments(tText, tFormat, cursor, self._charFmt)
elif tType == BlockTyp.SEP:
newBlock(cursor, bFmt)
cursor.insertText(tText, self._charFmt)
elif tType == BlockTyp.SKIP:
newBlock(cursor, bFmt)
cursor.insertText(nwUnicode.U_NBSP, self._charFmt)
if tStyle & BlockFmt.PBA:
self._insertNewPageMarker(cursor)
self._document.blockSignals(False)
return
def saveDocument(self, path: Path) -> None:
"""Save the document as a PDF file."""
logger.info("Writing PDF at %d DPI", self._dpi)
printer = QPrinter(QPrinter.PrinterMode.HighResolution)
printer.setDocName(self._project.data.name)
printer.setCreator(f"novelWriter/{__version__}")
printer.setResolution(self._dpi)
printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
printer.setPageSize(self._pageSize)
printer.setPageMargins(self._pageMargins, QPageLayout.Unit.Millimeter)
printer.setOutputFileName(str(path))
if layout := self._document.documentLayout():
layout.setPaintDevice(printer)
self._document.setPageSize(printer.pageRect(QPrinter.Unit.DevicePixel).size())
self._document.print(printer)
def closeDocument(self) -> None:
"""Run close document tasks."""
self._document.blockSignals(True)
# Replace fields if there are stats available
if self._usedFields and self._counts:
cursor = QTextCursor(self._document)
for pos, field in reversed(self._usedFields):
if (value := self._counts.get(field)) is not None:
cursor.setPosition(pos, QtMoveAnchor)
cursor.setPosition(pos + 1, QtKeepAnchor)
cursor.insertText(self._formatInt(value))
# Add footnotes
if self._usedNotes:
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
bFmt, cFmt = self._genHeadStyle(BlockTyp.HEAD4, "", self._blockFmt)
newBlock(cursor, bFmt)
cursor.insertText(self._localLookup("Footnotes"), cFmt)
for key, index in self._usedNotes.items():
if content := self._footnotes.get(key):
cFmt = QTextCharFormat(self._charFmt)
cFmt.setForeground(self._theme.code)
cFmt.setAnchor(True)
cFmt.setAnchorNames([f"footnote_{index}"])
newBlock(cursor, self._blockFmt)
cursor.insertText(f"{index}. ", cFmt)
self._insertFragments(*content, cursor, self._charFmt)
self._document.blockSignals(False)
##
# Internal Functions
##
def _insertFragments(
self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
) -> None:
"""Apply formatting tags to text."""
cFmt = QTextCharFormat(dFmt)
temp = text.replace("\n", nwUnicode.U_LSEP)
start = 0
primary: QColor | None = None
for pos, fmt, data in tFmt:
# Insert buffer with previous format
cursor.insertText(stripEscape(temp[start:pos]), cFmt)
# Construct next format
if fmt == TextFmt.B_B:
cFmt.setFontWeight(QFont.Weight.Bold)
elif fmt == TextFmt.B_E:
cFmt.setFontWeight(self._dWeight)
elif fmt == TextFmt.I_B:
cFmt.setFontItalic(True)
elif fmt == TextFmt.I_E:
cFmt.setFontItalic(self._dItalic)
elif fmt == TextFmt.D_B:
cFmt.setFontStrikeOut(True)
elif fmt == TextFmt.D_E:
cFmt.setFontStrikeOut(self._dStrike)
elif fmt == TextFmt.U_B:
cFmt.setFontUnderline(True)
elif fmt == TextFmt.U_E:
cFmt.setFontUnderline(self._dUnderline)
elif fmt == TextFmt.M_B:
cFmt.setBackground(self._theme.highlight)
elif fmt == TextFmt.M_E:
cFmt.setBackground(QtTransparent)
elif fmt == TextFmt.SUP_B:
cFmt.setVerticalAlignment(QtVAlignSuper)
elif fmt == TextFmt.SUP_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == TextFmt.SUB_B:
cFmt.setVerticalAlignment(QtVAlignSub)
elif fmt == TextFmt.SUB_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == TextFmt.COL_B:
if color := self._classes.get(data):
cFmt.setForeground(color)
primary = color
elif fmt == TextFmt.COL_E:
cFmt.setForeground(self._theme.text)
primary = None
elif fmt == TextFmt.ANM_B:
if self._anchors:
cFmt.setAnchor(True)
cFmt.setAnchorNames([data])
elif fmt == TextFmt.ANM_E:
if self._anchors:
cFmt.setAnchor(False)
elif fmt == TextFmt.ARF_B:
if self._anchors:
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
cFmt.setAnchorHref(data)
elif fmt == TextFmt.ARF_E:
if self._anchors:
cFmt.setFontUnderline(False)
cFmt.setAnchor(False)
cFmt.setAnchorHref("")
elif fmt == TextFmt.HRF_B:
cFmt.setForeground(self._theme.link)
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
cFmt.setAnchorHref(data)
elif fmt == TextFmt.HRF_E:
cFmt.setForeground(primary or self._theme.text)
cFmt.setFontUnderline(self._dUnderline)
cFmt.setAnchor(False)
cFmt.setAnchorHref("")
elif fmt == TextFmt.FNOTE:
xFmt = QTextCharFormat(self._charFmt)
xFmt.setForeground(self._theme.code)
xFmt.setVerticalAlignment(QtVAlignSuper)
if data in self._footnotes:
index = len(self._usedNotes) + 1
self._usedNotes[data] = index
xFmt.setAnchor(True)
xFmt.setAnchorHref(f"#footnote_{index}")
xFmt.setFontUnderline(True)
cursor.insertText(f"[{index}]", xFmt)
else:
cursor.insertText("[ERR]", cFmt)
elif fmt == TextFmt.FIELD:
if field := data.partition(":")[2]:
self._usedFields.append((cursor.position(), field))
cursor.insertText("0", cFmt)
# Move pos for next pass
start = pos
# Insert whatever is left in the buffer
cursor.insertText(stripEscape(temp[start:]), cFmt)
def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
"""Insert a new page marker."""
if self._newPage:
bgCol = QColor(self._theme.text)
bgCol.setAlphaF(0.1)
fgCol = QColor(self._theme.text)
fgCol.setAlphaF(0.8)
fFmt = QTextFrameFormat()
fFmt.setBorderStyle(QTextFrameFormat.BorderStyle.BorderStyle_None)
fFmt.setBackground(bgCol)
fFmt.setTopMargin(self._mSep[0])
fFmt.setBottomMargin(self._mSep[1])
bFmt = QTextBlockFormat(self._blockFmt)
bFmt.setAlignment(QtAlignCenter)
bFmt.setTopMargin(0.0)
bFmt.setBottomMargin(0.0)
bFmt.setLineHeight(100.0, QtPropLineHeight)
cFmt = QTextCharFormat(self._charFmt)
cFmt.setFontItalic(False)
cFmt.setFontUnderline(False)
cFmt.setFontStrikeOut(False)
cFmt.setFontWeight(QFont.Weight.Normal)
cFmt.setFontPointSize(0.75*self._textFont.pointSizeF())
cFmt.setForeground(fgCol)
cursor.insertFrame(fFmt)
cursor.setBlockFormat(bFmt)
cursor.insertText(self._project.localLookup("New Page"), cFmt)
if root := self._document.rootFrame():
cursor.swap(root.lastCursorPosition())
def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
"""Generate a heading style set."""
mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
bFmt = QTextBlockFormat(rFmt)
bFmt.setTopMargin(mTop)
bFmt.setBottomMargin(mBottom)
hCol = self._colorHeads and hType != BlockTyp.TITLE
cFmt = QTextCharFormat(self._charFmt)
cFmt.setForeground(self._theme.head if hCol else self._theme.text)
cFmt.setFontWeight(self._hWeight)
cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
if hKey and self._anchors:
cFmt.setAnchorNames([hKey])
cFmt.setAnchor(True)
return bFmt, cFmt