Add a QTextDocument format class for previews (#1892)

This commit is contained in:
Veronica Berglyd Olsen
2024-05-26 00:57:15 +02:00
committed by GitHub
24 changed files with 1208 additions and 411 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ class nwHeaders:
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
H_SIZES = {0: 1.00, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
H_SIZES = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
class nwFiles:
+29 -14
View File
@@ -39,6 +39,7 @@ from novelwriter.core.tohtml import ToHtml
from novelwriter.core.tokenizer import Tokenizer
from novelwriter.core.tomarkdown import ToMarkdown
from novelwriter.core.toodt import ToOdt
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.enum import nwBuildFmt
from novelwriter.error import formatException, logException
@@ -54,7 +55,7 @@ class NWBuildDocument:
__slots__ = (
"_project", "_build", "_queue", "_error", "_cache", "_count",
"_outline", "_preview"
"_outline",
)
def __init__(self, project: NWProject, build: BuildSettings) -> None:
@@ -65,7 +66,6 @@ class NWBuildDocument:
self._cache = None
self._count = False
self._outline = False
self._preview = False
return
##
@@ -99,15 +99,6 @@ class NWBuildDocument:
self._outline = state
return
def setPreviewMode(self, state: bool) -> None:
"""Set the preview mode of the build. This also enables stats
count and outline mode.
"""
self._preview = state
self._outline = state
self._count = state
return
##
# Special Methods
##
@@ -134,6 +125,32 @@ class NWBuildDocument:
self._queue.append(item.itemHandle)
return
def iterBuildPreview(self, theme: TextDocumentTheme) -> Iterable[tuple[int, bool]]:
"""Build a preview QTextDocument."""
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)
self._outline = True
self._count = True
font = QFont()
font.fromString(self._build.getStr("format.textFont"))
makeObj.initDocument(font, theme)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
makeObj.appendFootnotes()
self._error = None
self._cache = makeObj
return
def iterBuild(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
"""Wrapper for builders based on format."""
if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT):
@@ -182,8 +199,6 @@ class NWBuildDocument:
makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj)
makeObj.setPreview(self._preview)
makeObj.setLinkHeadings(self._preview)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
@@ -193,7 +208,7 @@ class NWBuildDocument:
makeObj.appendFootnotes()
if not (self._build.getBool("html.preserveTabs") or self._preview):
if not self._build.getBool("html.preserveTabs"):
makeObj.replaceTabs()
self._error = None
+14 -55
View File
@@ -38,24 +38,6 @@ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
logger = logging.getLogger(__name__)
HTML4_TAGS = {
Tokenizer.FMT_B_B: "<b>",
Tokenizer.FMT_B_E: "</b>",
Tokenizer.FMT_I_B: "<i>",
Tokenizer.FMT_I_E: "</i>",
Tokenizer.FMT_D_B: "<span style='text-decoration: line-through;'>",
Tokenizer.FMT_D_E: "</span>",
Tokenizer.FMT_U_B: "<u>",
Tokenizer.FMT_U_E: "</u>",
Tokenizer.FMT_M_B: "<mark>",
Tokenizer.FMT_M_E: "</mark>",
Tokenizer.FMT_SUP_B: "<sup>",
Tokenizer.FMT_SUP_E: "</sup>",
Tokenizer.FMT_SUB_B: "<sub>",
Tokenizer.FMT_SUB_E: "</sub>",
Tokenizer.FMT_STRIP: "",
}
HTML5_TAGS = {
Tokenizer.FMT_B_B: "<strong>",
Tokenizer.FMT_B_E: "</strong>",
@@ -82,14 +64,9 @@ class ToHtml(Tokenizer):
also used by the Document Viewer, and Manuscript Build Preview.
"""
M_PREVIEW = 0 # Tweak output for the DocViewer
M_EXPORT = 1 # Tweak output for saving to HTML or printing
M_EBOOK = 2 # Tweak output for converting to epub
def __init__(self, project: NWProject) -> None:
super().__init__(project)
self._genMode = self.M_EXPORT
self._cssStyles = True
self._fullHTML: list[str] = []
@@ -112,11 +89,6 @@ class ToHtml(Tokenizer):
# Setters
##
def setPreview(self, state: bool) -> None:
"""Set to preview generator mode."""
self._genMode = self.M_PREVIEW if state else self.M_EXPORT
return
def setStyles(self, cssStyles: bool) -> None:
"""Enable or disable CSS styling. Some elements may still have
class tags.
@@ -157,8 +129,7 @@ class ToHtml(Tokenizer):
"""Convert the list of text tokens into an HTML document."""
self._result = ""
hTags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS
if self._isNovel and self._genMode != self.M_PREVIEW:
if self._isNovel:
# For story files, we bump the titles one level up
h1Cl = " class='title'"
h1 = "h1"
@@ -240,7 +211,7 @@ class ToHtml(Tokenizer):
# Process Text Type
if tType == self.T_TEXT:
lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat, hTags)}</p>\n")
lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat)}</p>\n")
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
@@ -269,13 +240,13 @@ class ToHtml(Tokenizer):
lines.append(f"<p class='skip'{hStyle}>&nbsp;</p>\n")
elif tType == self.T_SYNOPSIS and self._doSynopsis:
lines.append(self._formatSynopsis(self._formatText(tText, tFormat, hTags), True))
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), True))
elif tType == self.T_SHORT and self._doSynopsis:
lines.append(self._formatSynopsis(self._formatText(tText, tFormat, hTags), False))
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), False))
elif tType == self.T_COMMENT and self._doComments:
lines.append(self._formatComments(self._formatText(tText, tFormat, hTags)))
lines.append(self._formatComments(self._formatText(tText, tFormat)))
elif tType == self.T_KEYWORD and self._doKeywords:
tag, text = self._formatKeywords(tText)
@@ -291,7 +262,6 @@ class ToHtml(Tokenizer):
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
if self._usedNotes:
tags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS
footnotes = self._localLookup("Footnotes")
lines = []
@@ -299,7 +269,7 @@ class ToHtml(Tokenizer):
lines.append("<ol>\n")
for key, index in self._usedNotes.items():
if content := self._footnotes.get(key):
text = self._formatText(*content, tags)
text = self._formatText(*content)
lines.append(f"<li id='footnote_{index}'><p>{text}</p></li>\n")
lines.append("</ol>\n")
@@ -468,7 +438,7 @@ class ToHtml(Tokenizer):
# Internal Functions
##
def _formatText(self, text: str, tFmt: T_Formats, tags: dict[int, str]) -> str:
def _formatText(self, text: str, tFmt: T_Formats) -> str:
"""Apply formatting tags to text."""
temp = text
for pos, fmt, data in reversed(tFmt):
@@ -481,7 +451,7 @@ class ToHtml(Tokenizer):
else:
html = "<sup>ERR</sup>"
else:
html = tags.get(fmt, "ERR")
html = HTML5_TAGS.get(fmt, "ERR")
temp = f"{temp[:pos]}{html}{temp[pos:]}"
temp = temp.replace("\n", "<br>")
return stripEscape(temp)
@@ -492,18 +462,12 @@ class ToHtml(Tokenizer):
sSynop = self._localLookup("Synopsis")
else:
sSynop = self._localLookup("Short Description")
if self._genMode == self.M_PREVIEW:
return f"<p class='note'><span class='modifier'>{sSynop}:</span> {text}</p>\n"
else:
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
def _formatComments(self, text: str) -> str:
"""Apply HTML formatting to comments."""
if self._genMode == self.M_PREVIEW:
return f"<p class='comment'>{text}</p>\n"
else:
sComm = self._localLookup("Comment")
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
sComm = self._localLookup("Comment")
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
def _formatKeywords(self, text: str) -> tuple[str, str]:
"""Apply HTML formatting to keywords."""
@@ -519,13 +483,8 @@ class ToHtml(Tokenizer):
if two:
result += f" | <span class='optional'>{two}</a>"
else:
if self._genMode == self.M_PREVIEW:
result += ", ".join(
f"<a class='tag' href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:]
)
else:
result += ", ".join(
f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
)
result += ", ".join(
f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
)
return bits[0][1:], result
+16 -6
View File
@@ -119,6 +119,7 @@ class Tokenizer(ABC):
# Lookups
L_HEADINGS = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD3, T_HEAD4]
L_SKIP_INDENT = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD2, T_HEAD3, T_HEAD4, T_SEP, T_SKIP]
L_SUMMARY = [T_SYNOPSIS, T_SHORT]
def __init__(self, project: NWProject) -> None:
@@ -155,14 +156,15 @@ class Tokenizer(ABC):
self._keepBreaks = True # Keep line breaks in paragraphs
# Margins
self._marginTitle = (1.000, 0.500)
self._marginHead1 = (1.000, 0.500)
self._marginHead2 = (0.834, 0.500)
self._marginHead3 = (0.584, 0.500)
self._marginHead4 = (0.584, 0.500)
self._marginTitle = (1.417, 0.500)
self._marginHead1 = (1.417, 0.500)
self._marginHead2 = (1.668, 0.500)
self._marginHead3 = (1.168, 0.500)
self._marginHead4 = (1.168, 0.500)
self._marginText = (0.000, 0.584)
self._marginMeta = (0.000, 0.584)
self._marginFoot = (1.417, 0.467)
self._marginSep = (1.168, 1.168)
# Title Formats
self._fmtTitle = nwHeadFmt.TITLE # Formatting for titles
@@ -378,6 +380,11 @@ class Tokenizer(ABC):
self._marginMeta = (float(upper), float(lower))
return
def setSeparatorMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower meta text margin."""
self._marginSep = (float(upper), float(lower))
return
def setLinkHeadings(self, state: bool) -> None:
"""Enable or disable adding an anchor before headings."""
self._linkHeadings = state
@@ -597,7 +604,10 @@ class Tokenizer(ABC):
# are automatically skipped.
valid, bits, _ = self._project.index.scanThis(aLine)
if valid and bits and bits[0] not in self._skipKeywords:
if (
valid and bits and bits[0] in nwLabels.KEY_NAME
and bits[0] not in self._skipKeywords
):
tokens.append((
self.T_KEYWORD, nHead, aLine[1:].strip(), [], sAlign
))
+7 -3
View File
@@ -192,6 +192,7 @@ class ToOdt(Tokenizer):
self._mTopHead = "0.423cm"
self._mTopText = "0.000cm"
self._mTopMeta = "0.000cm"
self._mTopSep = "0.247cm"
self._mBotTitle = "0.212cm"
self._mBotHead1 = "0.212cm"
@@ -201,6 +202,7 @@ class ToOdt(Tokenizer):
self._mBotHead = "0.212cm"
self._mBotText = "0.247cm"
self._mBotMeta = "0.106cm"
self._mBotSep = "0.247cm"
self._mBotFoot = "0.106cm"
self._mLeftFoot = "0.600cm"
@@ -299,6 +301,7 @@ class ToOdt(Tokenizer):
self._mTopHead = self._emToCm(mScale * self._marginHead4[0])
self._mTopText = self._emToCm(mScale * self._marginText[0])
self._mTopMeta = self._emToCm(mScale * self._marginMeta[0])
self._mTopSep = self._emToCm(mScale * self._marginSep[0])
self._mBotTitle = self._emToCm(mScale * self._marginTitle[1])
self._mBotHead1 = self._emToCm(mScale * self._marginHead1[1])
@@ -308,6 +311,7 @@ class ToOdt(Tokenizer):
self._mBotHead = self._emToCm(mScale * self._marginHead4[1])
self._mBotText = self._emToCm(mScale * self._marginText[1])
self._mBotMeta = self._emToCm(mScale * self._marginMeta[1])
self._mBotSep = self._emToCm(mScale * self._marginSep[1])
self._mLeftFoot = self._emToCm(self._marginFoot[0])
self._mBotFoot = self._emToCm(self._marginFoot[1])
@@ -501,7 +505,7 @@ class ToOdt(Tokenizer):
self._addTextPar(xText, S_SEP, oStyle, tText)
elif tType == self.T_SKIP:
self._addTextPar(xText, S_SEP, oStyle, "")
self._addTextPar(xText, S_TEXT, oStyle, "")
elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, tFmt = self._formatSynopsis(tText, tFormat, True)
@@ -944,8 +948,8 @@ class ToOdt(Tokenizer):
style.setParentStyleName("Standard")
style.setNextStyleName(S_TEXT)
style.setClass("text")
style.setMarginTop(self._mTopText)
style.setMarginBottom(self._mBotText)
style.setMarginTop(self._mTopSep)
style.setMarginBottom(self._mBotSep)
style.setLineHeight(self._fLineHeight)
style.setTextAlign("center")
style.setFontName(self._fontFamily)
+403
View File
@@ -0,0 +1,403 @@
"""
novelWriter QTextDocument Converter
=====================================
File History:
Created: 2024-05-21 [2.5b1] ToQTextDocument
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 (
QColor, QFont, QFontMetrics, QTextBlockFormat, QTextCharFormat,
QTextCursor, QTextDocument
)
from novelwriter.constants import nwHeaders, nwHeadFmt, nwKeyWords, nwLabels, nwUnicode
from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import T_Formats, Tokenizer
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
QtBlack, QtPageBreakAfter, QtPageBreakBefore, QtTransparent,
QtVAlignNormal, QtVAlignSub, QtVAlignSuper
)
logger = logging.getLogger(__name__)
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
class TextDocumentTheme:
text: QColor = QtBlack
highlight: QColor = QtTransparent
head: QColor = QtBlack
comment: QColor = QtBlack
note: QColor = QtBlack
code: QColor = QtBlack
modifier: QColor = QtBlack
keyword: QColor = QtBlack
tag: QColor = QtBlack
optional: QColor = QtBlack
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
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)
self._theme = TextDocumentTheme()
self._styles: dict[int, T_TextStyle] = {}
self._usedNotes: dict[str, int] = {}
self._init = False
self._bold = QFont.Weight.Bold
self._normal = QFont.Weight.Normal
return
def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
"""Initialise all computed values of the document."""
self._textFont = font
self._theme = theme
self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
self._document.clear()
self._document.setDefaultFont(self._textFont)
qMetric = QFontMetrics(self._textFont)
mScale = qMetric.height()
fPt = self._textFont.pointSizeF()
# Scaled Sizes
# ============
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(0, 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._mSep = (mScale * self._marginSep[0], mScale * self._marginSep[1])
self._mIndent = mScale * 2.0
# Block Format
# ============
self._blockFmt = QTextBlockFormat()
self._blockFmt.setTopMargin(self._mText[0])
self._blockFmt.setBottomMargin(self._mText[1])
self._blockFmt.setAlignment(QtAlignJustify if self._doJustify else QtAlignAbsolute)
# Character Formats
# =================
self._cText = QTextCharFormat()
self._cText.setBackground(QtTransparent)
self._cText.setForeground(self._theme.text)
self._cHead = QTextCharFormat(self._cText)
self._cHead.setForeground(self._theme.head)
self._cComment = QTextCharFormat(self._cText)
self._cComment.setForeground(self._theme.comment)
self._cCommentMod = QTextCharFormat(self._cText)
self._cCommentMod.setForeground(self._theme.comment)
self._cCommentMod.setFontWeight(self._bold)
self._cNote = QTextCharFormat(self._cText)
self._cNote.setForeground(self._theme.note)
self._cCode = QTextCharFormat(self._cText)
self._cCode.setForeground(self._theme.code)
self._cModifier = QTextCharFormat(self._cText)
self._cModifier.setForeground(self._theme.modifier)
self._cModifier.setFontWeight(self._bold)
self._cKeyword = QTextCharFormat(self._cText)
self._cKeyword.setForeground(self._theme.keyword)
self._cTag = QTextCharFormat(self._cText)
self._cTag.setForeground(self._theme.tag)
self._cOptional = QTextCharFormat(self._cText)
self._cOptional.setForeground(self._theme.optional)
self._init = True
return
##
# Properties
##
@property
def document(self) -> QTextDocument:
"""Return the document."""
return self._document
##
# Class Methods
##
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, nHead, tText, tFormat, tStyle in self._tokens:
# Styles
bFmt = QTextBlockFormat(self._blockFmt)
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._mIndent)
if tStyle & self.A_IND_R:
bFmt.setRightMargin(self._mIndent)
if tType == self.T_TEXT:
newBlock(cursor, bFmt)
self._insertFragments(tText, tFormat, cursor, self._cText)
elif tType in self.L_HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, nHead, bFmt)
newBlock(cursor, bFmt)
cursor.insertText(tText.replace(nwHeadFmt.BR, "\n"), cFmt)
elif tType == self.T_SEP:
sFmt = QTextBlockFormat(bFmt)
sFmt.setTopMargin(self._mSep[0])
sFmt.setBottomMargin(self._mSep[1])
newBlock(cursor, sFmt)
cursor.insertText(tText, self._cText)
elif tType == self.T_SKIP:
newBlock(cursor, bFmt)
cursor.insertText(nwUnicode.U_NBSP, self._cText)
elif tType in self.L_SUMMARY and self._doSynopsis:
newBlock(cursor, bFmt)
modifier = self._localLookup(
"Short Description" if tType == self.T_SHORT else "Synopsis"
)
cursor.insertText(f"{modifier}: ", self._cModifier)
self._insertFragments(tText, tFormat, cursor, self._cNote)
elif tType == self.T_COMMENT and self._doComments:
newBlock(cursor, bFmt)
modifier = self._localLookup("Comment")
cursor.insertText(f"{modifier}: ", self._cCommentMod)
self._insertFragments(tText, tFormat, cursor, self._cComment)
elif tType == self.T_KEYWORD and self._doKeywords:
newBlock(cursor, bFmt)
self._insertKeywords(tText, cursor)
self._document.blockSignals(False)
return
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
if self._usedNotes:
self._document.blockSignals(True)
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
bFmt, cFmt = self._genHeadStyle(self.T_HEAD4, -1, 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._cCode)
cFmt.setAnchor(True)
cFmt.setAnchorNames([f"footnote_{index}"])
newBlock(cursor, self._blockFmt)
cursor.insertText(f"{index}. ", cFmt)
self._insertFragments(*content, cursor, self._cText)
self._document.blockSignals(False)
return
##
# Internal Functions
##
def _insertFragments(
self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
) -> None:
"""Apply formatting tags to text."""
cFmt = QTextCharFormat(dFmt)
start = 0
temp = text.replace("\n", nwUnicode.U_LSEP)
for pos, fmt, data in tFmt:
# Insert buffer with previous format
cursor.insertText(temp[start:pos], cFmt)
# Construct next format
if fmt == self.FMT_B_B:
cFmt.setFontWeight(self._bold)
elif fmt == self.FMT_B_E:
cFmt.setFontWeight(self._normal)
elif fmt == self.FMT_I_B:
cFmt.setFontItalic(True)
elif fmt == self.FMT_I_E:
cFmt.setFontItalic(False)
elif fmt == self.FMT_D_B:
cFmt.setFontStrikeOut(True)
elif fmt == self.FMT_D_E:
cFmt.setFontStrikeOut(False)
elif fmt == self.FMT_U_B:
cFmt.setFontUnderline(True)
elif fmt == self.FMT_U_E:
cFmt.setFontUnderline(False)
elif fmt == self.FMT_M_B:
cFmt.setBackground(self._theme.highlight)
elif fmt == self.FMT_M_E:
cFmt.setBackground(QtTransparent)
elif fmt == self.FMT_SUP_B:
cFmt.setVerticalAlignment(QtVAlignSuper)
elif fmt == self.FMT_SUP_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == self.FMT_SUB_B:
cFmt.setVerticalAlignment(QtVAlignSub)
elif fmt == self.FMT_SUB_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == self.FMT_FNOTE:
xFmt = QTextCharFormat(self._cCode)
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)
# Move pos for next pass
start = pos
# Insert whatever is left in the buffer
cursor.insertText(temp[start:], cFmt)
return
def _insertKeywords(self, text: str, cursor: QTextCursor) -> None:
"""Apply Markdown formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text)
if valid and bits:
key = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: "
cursor.insertText(key, self._cKeyword)
if (num := len(bits)) > 1:
if bits[0] == nwKeyWords.TAG_KEY:
one, two = self._project.index.parseValue(bits[1])
cFmt = QTextCharFormat(self._cTag)
cFmt.setAnchor(True)
cFmt.setAnchorNames([f"tag_{one}".lower()])
cursor.insertText(one, cFmt)
if two:
cursor.insertText(" | ", self._cText)
cursor.insertText(two, self._cOptional)
else:
for n, bit in enumerate(bits[1:], 2):
cFmt = QTextCharFormat(self._cTag)
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
cFmt.setAnchorHref(f"#tag_{bit}".lower())
cursor.insertText(bit, cFmt)
if n < num:
cursor.insertText(", ", self._cText)
return
def _genHeadStyle(self, hType: int, nHead: int, 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)
cFmt = QTextCharFormat(self._cText if hType == self.T_TITLE else self._cHead)
cFmt.setFontWeight(self._bold)
cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
if nHead >= 0:
cFmt.setAnchorNames([f"{self._handle}:T{nHead:04d}"])
cFmt.setAnchor(True)
return bFmt, cFmt
+5 -13
View File
@@ -321,8 +321,11 @@ class GuiDocEditor(QPlainTextEdit):
# Reload spell check and dictionaries
SHARED.updateSpellCheckLanguage()
# Set font
self.initFont()
# Set the font. See issues #1862 and #1875.
self.setFont(CONFIG.textFont)
self.docHeader.updateFont()
self.docFooter.updateFont()
self.docSearch.updateFont()
# Update highlighter settings
self._qDocument.syntaxHighlighter.initHighlighter()
@@ -372,17 +375,6 @@ class GuiDocEditor(QPlainTextEdit):
return
def initFont(self) -> None:
"""Set the font of the main widget and sub-widgets. This needs
special attention since there appears to be a bug in Qt 5.15.3.
See issues #1862 and #1875.
"""
self.setFont(CONFIG.textFont)
self.docHeader.updateFont()
self.docFooter.updateFont()
self.docSearch.updateFont()
return
def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
"""Load text from a document into the editor. If we have an I/O
error, we must handle this and clear the editor so that we don't
+36 -73
View File
@@ -31,24 +31,21 @@ import logging
from enum import Enum
from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor, QTextOption
from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor
from PyQt5.QtWidgets import (
QAction, QApplication, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser,
QToolButton, QWidget
)
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.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.enum import nwDocAction, nwDocMode, nwItemType
from novelwriter.error import logException
from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.types import (
QtAlignCenterTop, QtAlignJustify, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
)
from novelwriter.types import QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
logger = logging.getLogger(__name__)
@@ -69,6 +66,7 @@ class GuiDocViewer(QTextBrowser):
# Internal Variables
self._docHandle = None
self._docTheme = TextDocumentTheme()
# Settings
self.setMinimumWidth(CONFIG.pxInt(300))
@@ -137,8 +135,10 @@ class GuiDocViewer(QTextBrowser):
def initViewer(self) -> None:
"""Set editor settings from main config."""
self._makeStyleSheet()
self.initFont()
# Set the font. See issues #1862 and #1875.
self.setFont(CONFIG.textFont)
self.docHeader.updateFont()
self.docFooter.updateFont()
# Set the widget colours to match syntax theme
mainPalette = self.palette()
@@ -151,16 +151,23 @@ class GuiDocViewer(QTextBrowser):
docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.viewport().setPalette(docPalette)
self.docHeader.matchColours()
self.docFooter.matchColours()
# Update theme colours
self._docTheme.text = SHARED.theme.colText
self._docTheme.highlight = SHARED.theme.colMark
self._docTheme.head = SHARED.theme.colHead
self._docTheme.comment = SHARED.theme.colHidden
self._docTheme.note = SHARED.theme.colNote
self._docTheme.code = SHARED.theme.colCode
self._docTheme.modifier = SHARED.theme.colMod
self._docTheme.keyword = SHARED.theme.colKey
self._docTheme.tag = SHARED.theme.colTag
self._docTheme.optional = SHARED.theme.colOpt
# Set default text margins
self.document().setDocumentMargin(0)
options = QTextOption()
if CONFIG.doJustify:
options.setAlignment(QtAlignJustify)
self.document().setDefaultTextOption(options)
# Scroll bars
if CONFIG.hideVScroll:
@@ -181,16 +188,6 @@ class GuiDocViewer(QTextBrowser):
return
def initFont(self) -> None:
"""Set the font of the main widget and sub-widgets. This needs
special attention since there appears to be a bug in Qt 5.15.3.
See issues #1862 and #1875.
"""
self.setFont(CONFIG.textFont)
self.docHeader.updateFont()
self.docFooter.updateFont()
return
def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
"""Load text into the viewer from an item handle."""
if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
@@ -202,22 +199,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.setJustify(CONFIG.doJustify)
qDoc.initDocument(CONFIG.textFont, self._docTheme)
qDoc.setKeywords(True)
qDoc.setComments(CONFIG.viewComments)
qDoc.setSynopsis(CONFIG.viewSynopsis)
# 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()
@@ -233,11 +230,7 @@ class GuiDocViewer(QTextBrowser):
self.docHistory.append(tHandle)
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.setDocument(qDoc.document)
if self._docHandle == tHandle:
# This is a refresh, so we set the scrollbar back to where it was
@@ -375,12 +368,10 @@ class GuiDocViewer(QTextBrowser):
@pyqtSlot("QUrl")
def _linkClicked(self, url: QUrl) -> None:
"""Process a clicked link in the document."""
link = url.url()
logger.debug("Clicked link: '%s'", link)
if len(link) > 0:
bits = link.split("=")
if len(bits) == 2:
self.loadDocumentTagRequest.emit(bits[1], nwDocMode.VIEW)
if link := url.url():
logger.debug("Clicked link: '%s'", link)
if (bits := link.partition("_")) and bits[2]:
self.loadDocumentTagRequest.emit(bits[2], nwDocMode.VIEW)
return
@pyqtSlot("QPoint")
@@ -470,34 +461,6 @@ class GuiDocViewer(QTextBrowser):
self._makeSelection(selType)
return
def _makeStyleSheet(self) -> None:
"""Generate an appropriate style sheet for the document viewer,
based on the current syntax highlighter theme.
"""
colHead = cssCol(SHARED.theme.colHead)
colHide = cssCol(SHARED.theme.colHidden)
colKeys = cssCol(SHARED.theme.colKey)
colMark = cssCol(SHARED.theme.colMark)
colMods = cssCol(SHARED.theme.colMod)
colNote = cssCol(SHARED.theme.colNote)
colOpts = cssCol(SHARED.theme.colOpt)
colTags = cssCol(SHARED.theme.colTag)
colText = cssCol(SHARED.theme.colText)
self.document().setDefaultStyleSheet(
f"body {{color: {colText};}}\n"
f"h1, h2, h3, h4 {{color: {colHead};}}\n"
f"mark {{background-color: {colMark};}}\n"
f".keyword {{color: {colKeys};}}\n"
f".tag {{color: {colTags};}}\n"
f".optional {{color: {colOpts};}}\n"
f".comment {{color: {colHide};}}\n"
f".note {{color: {colNote};}}\n"
f".modifier {{color: {colMods};}}\n"
".title {text-align: center;}\n"
)
return
class GuiDocViewHistory:
+2 -4
View File
@@ -800,8 +800,7 @@ class GuiMain(QMainWindow):
def showBuildManuscriptDialog(self) -> None:
"""Open the build manuscript dialog."""
if SHARED.hasProject:
if (dialog := SHARED.findTopLevelWidget(GuiManuscript)) is None:
dialog = GuiManuscript(self)
dialog = GuiManuscript(self)
dialog.activateDialog()
dialog.loadContent()
return
@@ -819,8 +818,7 @@ class GuiMain(QMainWindow):
def showWritingStatsDialog(self) -> None:
"""Open the session stats dialog."""
if SHARED.hasProject:
if (dialog := SHARED.findTopLevelWidget(GuiWritingStats)) is None:
dialog = GuiWritingStats(self)
dialog = GuiWritingStats(self)
dialog.activateDialog()
dialog.populateGUI()
return
+41 -96
View File
@@ -23,15 +23,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import json
import logging
from datetime import datetime
from time import time
from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent, QTextDocument
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtWidgets import (
QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout,
@@ -41,20 +39,19 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import checkInt, fuzzyTime
from novelwriter.common import fuzzyTime
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.error import logException
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.extensions.circularprogress import NProgressCircle
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignRight, QtAlignTop,
QtSizeExpanding, QtSizeIgnored, QtUserRole
QtAlignCenter, QtAlignRight, QtAlignTop, QtSizeExpanding, QtSizeIgnored,
QtUserRole
)
if TYPE_CHECKING: # pragma: no cover
@@ -249,20 +246,7 @@ class GuiManuscript(NToolDialog):
self._updateBuildsList()
if selected in self._buildMap:
self.buildList.setCurrentItem(self._buildMap[selected])
logger.debug("Loading build cache")
cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
if cache.is_file():
try:
with open(cache, mode="r", encoding="utf-8") as fObj:
data = json.load(fObj)
build = self._builds.getBuild(data.get("uuid", ""))
if isinstance(build, BuildSettings):
self._updatePreview(data, build)
except Exception:
logger.error("Failed to load build cache")
logException()
return
QTimer.singleShot(200, self._generatePreview)
return
@@ -342,36 +326,37 @@ class GuiManuscript(NToolDialog):
SHARED.saveDocument()
docBuild = NWBuildDocument(SHARED.project, build)
docBuild.setPreviewMode(True)
docBuild.queueAll()
theme = TextDocumentTheme()
theme.text = QColor(0, 0, 0)
theme.highlight = QColor(255, 255, 166)
theme.head = QColor(66, 113, 174)
theme.comment = QColor(100, 100, 100)
theme.note = QColor(129, 55, 9)
theme.code = QColor(66, 113, 174)
theme.modifier = QColor(129, 55, 9)
theme.keyword = QColor(245, 135, 31)
theme.tag = QColor(66, 113, 174)
theme.optional = QColor(66, 113, 174)
self.docPreview.beginNewBuild(len(docBuild))
for step, _ in docBuild.iterBuildHTML(None):
for step, _ in docBuild.iterBuildPreview(theme):
self.docPreview.buildStep(step + 1)
QApplication.processEvents()
buildObj = docBuild.lastBuild
assert isinstance(buildObj, ToHtml)
result = {
"uuid": build.buildID,
"time": int(time()),
"stats": buildObj.textStats,
"outline": buildObj.textOutline,
"styles": buildObj.getStyleSheet(),
"html": buildObj.fullHTML,
}
assert isinstance(buildObj, ToQTextDocument)
self._updatePreview(result, build)
font = QFont()
font.fromString(build.getStr("format.textFont"))
logger.debug("Saving build cache")
cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
try:
with open(cache, mode="w+", encoding="utf-8") as outFile:
outFile.write(json.dumps(result, indent=2))
except Exception:
logger.error("Failed to save build cache")
logException()
return
self.docPreview.setTextFont(font)
self.docPreview.setContent(buildObj.document)
self.docPreview.setBuildName(build.name)
self.docStats.updateStats(buildObj.textStats)
self.buildOutline.updateOutline(buildObj.textOutline)
return
@@ -379,8 +364,8 @@ class GuiManuscript(NToolDialog):
def _buildManuscript(self) -> None:
"""Open the build dialog and build the manuscript."""
if build := self._getSelectedBuild():
dlgBuild = GuiManuscriptBuild(self, build)
dlgBuild.exec()
dialog = GuiManuscriptBuild(self, build)
dialog.exec()
# After the build is done, save build settings changes
if build.changed:
@@ -400,21 +385,6 @@ class GuiManuscript(NToolDialog):
# Internal Functions
##
def _updatePreview(self, data: dict, build: BuildSettings) -> None:
"""Update the preview widget and set relevant values."""
textFont = QFont()
textFont.fromString(build.getStr("format.textFont"))
self.docPreview.setContent(data)
self.docPreview.setBuildName(build.name)
self.docPreview.setTextFont(textFont)
self.docPreview.setJustify(
build.getBool("format.justifyText")
)
self.docStats.updateStats(data.get("stats", {}))
self.buildOutline.updateOutline(data.get("outline", {}))
return
def _getSelectedBuild(self) -> BuildSettings | None:
"""Get the currently selected build. If none are selected,
automatically select the first one.
@@ -807,16 +777,6 @@ class _PreviewWidget(QTextBrowser):
self._updateBuildAge()
return
def setJustify(self, state: bool) -> None:
"""Enable/disable the justify text option."""
pOptions = self.document().defaultTextOption()
if state:
pOptions.setAlignment(QtAlignJustify)
else:
pOptions.setAlignment(QtAlignAbsolute)
self.document().setDefaultTextOption(pOptions)
return
def setTextFont(self, font: QFont) -> None:
"""Set the text font properties and then reset for sub-widgets.
This needs special attention since there appears to be a bug in
@@ -848,31 +808,19 @@ class _PreviewWidget(QTextBrowser):
QApplication.processEvents()
return
def setContent(self, data: dict) -> None:
def setContent(self, document: QTextDocument) -> None:
"""Set the content of the preview widget."""
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
self.buildProgress.setCentreText(self.tr("Processing ..."))
QApplication.processEvents()
styles = "\n".join(data.get("styles", []))
self.document().setDefaultStyleSheet(styles)
document.setDocumentMargin(CONFIG.getTextMargin())
self.setDocument(document)
html = "".join(data.get("html", []))
html = html.replace("\t", "!!tab!!")
self.setHtml(html)
QApplication.processEvents()
while self.find("!!tab!!"):
cursor = self.textCursor()
cursor.insertText("\t")
self._docTime = checkInt(data.get("time"), 0)
self._docTime = int(time())
self._updateBuildAge()
# Since we change the content while it may still be rendering, we mark
# the document as dirty again to make sure it's re-rendered properly.
self.document().markContentsDirty(0, self.document().characterCount())
self.buildProgress.setCentreText(self.tr("Done"))
QApplication.restoreOverrideCursor()
QApplication.processEvents()
@@ -917,17 +865,14 @@ class _PreviewWidget(QTextBrowser):
@pyqtSlot()
def _updateBuildAge(self) -> None:
"""Update the build time and the fuzzy age."""
if self._docTime > 0:
strBuildTime = "%s (%s)" % (
CONFIG.localDateTime(datetime.fromtimestamp(self._docTime)),
fuzzyTime(int(time()) - self._docTime)
)
if self._buildName and self._docTime > 0:
self.ageLabel.setText("<b>{0}</b><br>{1}: {2}".format(
self._buildName,
self.tr("Built"),
fuzzyTime(int(time()) - self._docTime),
))
else:
strBuildTime = self.tr("Unknown")
text = "{0}: {1}".format(self.tr("Built"), strBuildTime)
if self._buildName:
text = "<b>{0}</b><br>{1}".format(self._buildName, text)
self.ageLabel.setText(text)
self.ageLabel.setText("<b>{0}</b>".format(self.tr("No Preview")))
return
@pyqtSlot()
+6 -1
View File
@@ -24,7 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from PyQt5.QtCore import QRegularExpression, Qt
from PyQt5.QtGui import QColor, QFont, QPainter, QTextCursor, QTextFormat
from PyQt5.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextCursor, QTextFormat
from PyQt5.QtWidgets import QDialogButtonBox, QSizePolicy, QStyle
# Qt Alignment Flags
@@ -44,6 +44,10 @@ QtAlignRightMiddle = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
QtAlignRightTop = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
QtAlignTop = Qt.AlignmentFlag.AlignTop
QtVAlignNormal = QTextCharFormat.VerticalAlignment.AlignNormal
QtVAlignSub = QTextCharFormat.VerticalAlignment.AlignSubScript
QtVAlignSuper = QTextCharFormat.VerticalAlignment.AlignSuperScript
# Qt Page Break
QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore
@@ -52,6 +56,7 @@ QtPageBreakAfter = QTextFormat.PageBreakFlag.PageBreak_AlwaysAfter
# Qt Painter Types
QtTransparent = QColor(0, 0, 0, 0)
QtBlack = QColor(0, 0, 0)
QtNoBrush = Qt.BrushStyle.NoBrush
QtNoPen = Qt.PenStyle.NoPen
QtRoundCap = Qt.PenCapStyle.RoundCap
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta>
<meta:creation-date>2024-05-22T23:05:27</meta:creation-date>
<meta:creation-date>2024-05-24T21:31:13</meta:creation-date>
<meta:generator>novelWriter/2.5a4</meta:generator>
<meta:initial-creator>Jane Smith</meta:initial-creator>
<meta:editing-cycles>1234</meta:editing-cycles>
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
<dc:title>Test Project</dc:title>
<dc:date>2024-05-22T23:05:27</dc:date>
<dc:date>2024-05-24T21:31:13</dc:date>
<dc:creator>Jane Smith</dc:creator>
</office:meta>
<office:font-face-decls>
@@ -22,7 +22,7 @@
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="12pt" />
</style:style>
<style:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" fo:keep-with-next="always" />
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" fo:keep-with-next="always" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="14pt" />
</style:style>
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
@@ -38,27 +38,27 @@
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" fo:color="#813709" loext:opacity="100%" />
</style:style>
<style:style style:name="Title" style:family="paragraph" style:display-name="Title" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:class="chapter">
<style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0.212cm" fo:text-align="center" />
<style:paragraph-properties fo:margin-top="0.600cm" fo:margin-bottom="0.212cm" fo:text-align="center" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="30pt" fo:font-weight="bold" />
</style:style>
<style:style style:name="Separator" style:family="paragraph" style:display-name="Separator" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.247cm" fo:line-height="115%" fo:text-align="center" />
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.494cm" fo:line-height="115%" fo:text-align="center" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
</style:style>
<style:style style:name="Heading_20_1" style:family="paragraph" style:display-name="Heading 1" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="1" style:class="text">
<style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0.212cm" />
<style:paragraph-properties fo:margin-top="0.600cm" fo:margin-bottom="0.212cm" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="24pt" fo:font-weight="bold" fo:color="#2a6099" loext:opacity="100%" />
</style:style>
<style:style style:name="Heading_20_2" style:family="paragraph" style:display-name="Heading 2" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="2" style:class="text">
<style:paragraph-properties fo:margin-top="0.353cm" fo:margin-bottom="0.212cm" />
<style:paragraph-properties fo:margin-top="0.706cm" fo:margin-bottom="0.212cm" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="19pt" fo:font-weight="bold" fo:color="#2a6099" loext:opacity="100%" />
</style:style>
<style:style style:name="Heading_20_3" style:family="paragraph" style:display-name="Heading 3" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="3" style:class="text">
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" />
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="16pt" fo:font-weight="bold" fo:color="#444444" loext:opacity="100%" />
</style:style>
<style:style style:name="Heading_20_4" style:family="paragraph" style:display-name="Heading 4" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="4" style:class="text">
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" />
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="14pt" fo:font-weight="bold" fo:color="#444444" loext:opacity="100%" />
</style:style>
<style:style style:name="Header" style:family="paragraph" style:display-name="Header" style:parent-style-name="Header_20_and_20_Footer">
@@ -12,7 +12,7 @@
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="12pt" />
</style:style>
<style:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" fo:keep-with-next="always" />
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" fo:keep-with-next="always" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="14pt" />
</style:style>
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
@@ -28,27 +28,27 @@
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
</style:style>
<style:style style:name="Title" style:family="paragraph" style:display-name="Title" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:class="chapter">
<style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0.212cm" fo:text-align="center" />
<style:paragraph-properties fo:margin-top="0.600cm" fo:margin-bottom="0.212cm" fo:text-align="center" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="30pt" fo:font-weight="bold" />
</style:style>
<style:style style:name="Separator" style:family="paragraph" style:display-name="Separator" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.247cm" fo:line-height="115%" fo:text-align="center" />
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.494cm" fo:line-height="115%" fo:text-align="center" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
</style:style>
<style:style style:name="Heading_20_1" style:family="paragraph" style:display-name="Heading 1" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="1" style:class="text">
<style:paragraph-properties fo:margin-top="0.423cm" fo:margin-bottom="0.212cm" />
<style:paragraph-properties fo:margin-top="0.600cm" fo:margin-bottom="0.212cm" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="24pt" fo:font-weight="bold" />
</style:style>
<style:style style:name="Heading_20_2" style:family="paragraph" style:display-name="Heading 2" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="2" style:class="text">
<style:paragraph-properties fo:margin-top="0.353cm" fo:margin-bottom="0.212cm" />
<style:paragraph-properties fo:margin-top="0.706cm" fo:margin-bottom="0.212cm" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="19pt" fo:font-weight="bold" />
</style:style>
<style:style style:name="Heading_20_3" style:family="paragraph" style:display-name="Heading 3" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="3" style:class="text">
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" />
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="16pt" fo:font-weight="bold" />
</style:style>
<style:style style:name="Heading_20_4" style:family="paragraph" style:display-name="Heading 4" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="4" style:class="text">
<style:paragraph-properties fo:margin-top="0.247cm" fo:margin-bottom="0.212cm" />
<style:paragraph-properties fo:margin-top="0.494cm" fo:margin-bottom="0.212cm" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="14pt" fo:font-weight="bold" />
</style:style>
<style:style style:name="Header" style:family="paragraph" style:display-name="Header" style:parent-style-name="Header_20_and_20_Footer">
@@ -7,11 +7,11 @@
<style>
body {font-family: 'Arial'; font-size: 12pt; font-weight: 400; font-style: normal;}
p {text-align: justify; line-height: 150%; margin-top: 0.00em; margin-bottom: 0.76em;}
h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.30em; margin-bottom: 0.65em;}
h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.09em; margin-bottom: 0.65em;}
h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}
h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}
.title {font-size: 2.5em; margin-top: 1.30em; margin-bottom: 0.65em;}
h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.85em; margin-bottom: 0.65em;}
h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 2.18em; margin-bottom: 0.65em;}
h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 1.52em; margin-bottom: 0.65em;}
h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 1.52em; margin-bottom: 0.65em;}
.title {font-size: 2.5em; margin-top: 1.85em; margin-bottom: 0.65em;}
.sep, .skip {text-align: center; margin-top: 1.30em; margin-bottom: 1.30em;}
a {color: rgb(66, 113, 174);}
mark {background: rgb(255, 255, 166);}
@@ -2,18 +2,18 @@
"meta": {
"projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
"buildTime": 1716412100,
"buildTimeStr": "2024-05-22 23:08:20"
"buildTime": 1716579341,
"buildTimeStr": "2024-05-24 21:35:41"
},
"text": {
"css": [
"body {font-family: 'Arial'; font-size: 12pt; font-weight: 400; font-style: normal;}",
"p {text-align: justify; line-height: 150%; margin-top: 0.00em; margin-bottom: 0.76em;}",
"h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.30em; margin-bottom: 0.65em;}",
"h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.09em; margin-bottom: 0.65em;}",
"h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}",
"h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}",
".title {font-size: 2.5em; margin-top: 1.30em; margin-bottom: 0.65em;}",
"h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.85em; margin-bottom: 0.65em;}",
"h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 2.18em; margin-bottom: 0.65em;}",
"h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 1.52em; margin-bottom: 0.65em;}",
"h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 1.52em; margin-bottom: 0.65em;}",
".title {font-size: 2.5em; margin-top: 1.85em; margin-bottom: 0.65em;}",
".sep, .skip {text-align: center; margin-top: 1.30em; margin-bottom: 1.30em;}",
"a {color: rgb(66, 113, 174);}",
"mark {background: rgb(255, 255, 166);}",
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta>
<meta:creation-date>2024-05-22T23:05:26</meta:creation-date>
<meta:creation-date>2024-05-24T21:31:12</meta:creation-date>
<meta:generator>novelWriter/2.5a4</meta:generator>
<meta:initial-creator>lipsum.com</meta:initial-creator>
<meta:editing-cycles>45</meta:editing-cycles>
<meta:editing-duration>P0DT0H36M8S</meta:editing-duration>
<dc:title>Lorem Ipsum</dc:title>
<dc:date>2024-05-22T23:05:26</dc:date>
<dc:date>2024-05-24T21:31:12</dc:date>
<dc:creator>lipsum.com</dc:creator>
</office:meta>
<office:font-face-decls>
@@ -22,7 +22,7 @@
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-weight="normal" fo:font-style="normal" fo:font-size="12pt" />
</style:style>
<style:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
<style:paragraph-properties fo:margin-top="0.322cm" fo:margin-bottom="0.276cm" fo:keep-with-next="always" />
<style:paragraph-properties fo:margin-top="0.645cm" fo:margin-bottom="0.276cm" fo:keep-with-next="always" />
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-weight="normal" fo:font-style="normal" fo:font-size="14pt" />
</style:style>
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
@@ -38,27 +38,27 @@
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="12pt" fo:font-weight="normal" fo:color="#813709" loext:opacity="100%" />
</style:style>
<style:style style:name="Title" style:family="paragraph" style:display-name="Title" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:class="chapter">
<style:paragraph-properties fo:margin-top="0.552cm" fo:margin-bottom="0.276cm" fo:text-align="center" />
<style:paragraph-properties fo:margin-top="0.782cm" fo:margin-bottom="0.276cm" fo:text-align="center" />
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="30pt" fo:font-weight="bold" />
</style:style>
<style:style style:name="Separator" style:family="paragraph" style:display-name="Separator" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text">
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.322cm" fo:line-height="150%" fo:text-align="center" />
<style:paragraph-properties fo:margin-top="0.645cm" fo:margin-bottom="0.645cm" fo:line-height="150%" fo:text-align="center" />
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="12pt" fo:font-weight="normal" />
</style:style>
<style:style style:name="Heading_20_1" style:family="paragraph" style:display-name="Heading 1" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="1" style:class="text">
<style:paragraph-properties fo:margin-top="0.552cm" fo:margin-bottom="0.276cm" />
<style:paragraph-properties fo:margin-top="0.782cm" fo:margin-bottom="0.276cm" />
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="24pt" fo:font-weight="bold" fo:color="#2a6099" loext:opacity="100%" />
</style:style>
<style:style style:name="Heading_20_2" style:family="paragraph" style:display-name="Heading 2" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="2" style:class="text">
<style:paragraph-properties fo:margin-top="0.461cm" fo:margin-bottom="0.276cm" />
<style:paragraph-properties fo:margin-top="0.921cm" fo:margin-bottom="0.276cm" />
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="19pt" fo:font-weight="bold" fo:color="#2a6099" loext:opacity="100%" />
</style:style>
<style:style style:name="Heading_20_3" style:family="paragraph" style:display-name="Heading 3" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="3" style:class="text">
<style:paragraph-properties fo:margin-top="0.322cm" fo:margin-bottom="0.276cm" />
<style:paragraph-properties fo:margin-top="0.645cm" fo:margin-bottom="0.276cm" />
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="16pt" fo:font-weight="bold" fo:color="#444444" loext:opacity="100%" />
</style:style>
<style:style style:name="Heading_20_4" style:family="paragraph" style:display-name="Heading 4" style:parent-style-name="Heading" style:next-style-name="Text_20_body" style:default-outline-level="4" style:class="text">
<style:paragraph-properties fo:margin-top="0.322cm" fo:margin-bottom="0.276cm" />
<style:paragraph-properties fo:margin-top="0.645cm" fo:margin-bottom="0.276cm" />
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="14pt" fo:font-weight="bold" fo:color="#444444" loext:opacity="100%" />
</style:style>
<style:style style:name="Header" style:family="paragraph" style:display-name="Header" style:parent-style-name="Header_20_and_20_Footer">
-41
View File
@@ -284,21 +284,6 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
"</ol>\n"
)
# Preview Mode
# ============
html.setPreview(True)
# Text (HTML4)
html._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
html.tokenizeText()
html.doConvert()
assert html.result == (
"<p>Some <b>nested bold and <i>italic</i> and "
"<span style='text-decoration: line-through;'>strikethrough</span> "
"text</b> here</p>\n"
)
@pytest.mark.core
def testCoreToHtml_ConvertDirect(mockGUI):
@@ -682,29 +667,3 @@ def testCoreToHtml_Format(mockGUI):
"<a class='tag' href='#tag_Bod'>Bod</a>, "
"<a class='tag' href='#tag_Jane'>Jane</a>"
)
# Preview Mode
# ============
html.setPreview(True)
assert html._formatSynopsis("synopsis text", True) == (
"<p class='note'><span class='modifier'>Synopsis:</span> synopsis text</p>\n"
)
assert html._formatSynopsis("short text", False) == (
"<p class='note'><span class='modifier'>Short Description:</span> short text</p>\n"
)
assert html._formatComments("comment text") == (
"<p class='comment'>comment text</p>\n"
)
assert html._formatKeywords("") == ("", "")
assert html._formatKeywords("tag: Jane") == (
"tag", "<span class='keyword'>Tag:</span> <a class='tag' name='tag_Jane'>Jane</a>"
)
assert html._formatKeywords("char: Bod, Jane") == (
"char",
"<span class='keyword'>Characters:</span> "
"<a class='tag' href='#char=Bod'>Bod</a>, "
"<a class='tag' href='#char=Jane'>Jane</a>"
)
+8 -5
View File
@@ -56,13 +56,14 @@ def testCoreToken_Setters(mockGUI):
assert tokens._lineHeight == 1.15
assert tokens._blockIndent == 4.0
assert tokens._doJustify is False
assert tokens._marginTitle == (1.000, 0.500)
assert tokens._marginHead1 == (1.000, 0.500)
assert tokens._marginHead2 == (0.834, 0.500)
assert tokens._marginHead3 == (0.584, 0.500)
assert tokens._marginHead4 == (0.584, 0.500)
assert tokens._marginTitle == (1.417, 0.500)
assert tokens._marginHead1 == (1.417, 0.500)
assert tokens._marginHead2 == (1.668, 0.500)
assert tokens._marginHead3 == (1.168, 0.500)
assert tokens._marginHead4 == (1.168, 0.500)
assert tokens._marginText == (0.000, 0.584)
assert tokens._marginMeta == (0.000, 0.584)
assert tokens._marginSep == (1.168, 1.168)
assert tokens._hideTitle is False
assert tokens._hideChapter is False
assert tokens._hideUnNum is False
@@ -93,6 +94,7 @@ def testCoreToken_Setters(mockGUI):
tokens.setHead4Margins(2.0, 2.0)
tokens.setTextMargins(2.0, 2.0)
tokens.setMetaMargins(2.0, 2.0)
tokens.setSeparatorMargins(2.0, 2.0)
tokens.setLinkHeadings(True)
tokens.setBodyText(False)
tokens.setSynopsis(True)
@@ -117,6 +119,7 @@ def testCoreToken_Setters(mockGUI):
assert tokens._marginHead4 == (2.0, 2.0)
assert tokens._marginText == (2.0, 2.0)
assert tokens._marginMeta == (2.0, 2.0)
assert tokens._marginSep == (2.0, 2.0)
assert tokens._hideTitle is True
assert tokens._hideChapter is True
assert tokens._hideUnNum is True
+2 -2
View File
@@ -520,9 +520,9 @@ def testCoreToOdt_ConvertParagraphs(mockGUI):
assert odt.errData == []
assert xmlToText(odt._xText) == (
'<office:text>'
'<text:p text:style-name="Separator" />'
'<text:p text:style-name="Text_20_body" />'
'<text:p text:style-name="Text_20_body">Text</text:p>'
'<text:p text:style-name="Separator" />'
'<text:p text:style-name="Text_20_body" />'
'<text:p text:style-name="Text_20_body">Text</text:p>'
'</office:text>'
)
+571
View File
@@ -0,0 +1,571 @@
"""
novelWriter ToQTextDocument Class Tester
==========================================
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 pytest
from PyQt5.QtGui import QColor, QTextBlock, QTextCharFormat, QTextCursor
from novelwriter import CONFIG
from novelwriter.constants import nwUnicode
from novelwriter.core.project import NWProject
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
QtPageBreakAfter, QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
)
THEME = TextDocumentTheme()
THEME.text = QColor(0, 0, 0)
THEME.highlight = QColor(255, 255, 166)
THEME.head = QColor(66, 113, 174)
THEME.comment = QColor(100, 100, 100)
THEME.note = QColor(129, 55, 9)
THEME.code = QColor(66, 113, 174)
THEME.modifier = QColor(129, 55, 9)
THEME.keyword = QColor(245, 135, 31)
THEME.tag = QColor(66, 113, 174)
THEME.optional = QColor(66, 113, 174)
def charFmtInBlock(block: QTextBlock, pos: int) -> QTextCharFormat:
"""Get the character format at a given place in a block."""
cursor = QTextCursor(block)
cursor.setPosition(block.position() + pos)
return cursor.charFormat()
@pytest.mark.core
def testCoreToQTextDocument_ConvertHeaders(mockGUI):
"""Test header formats in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc._isNovel = True
qdoc._isFirst = True
qdoc._text = (
"#! Title\n"
"# Partition\n"
"## Chapter\n"
"### Scene\n"
"#### Section\n"
)
qdoc.tokenizeText()
qdoc.doConvert()
assert qdoc.document.blockCount() == 5
# Title
block = qdoc.document.findBlockByNumber(0)
assert block.text() == "Title"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_TITLE][0]
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_TITLE][1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.fontWeight() == qdoc._bold
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_TITLE]
assert cFmt.foreground().color() == THEME.text
# Partition
block = qdoc.document.findBlockByNumber(1)
assert block.text() == "Partition"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD1][0]
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD1][1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.fontWeight() == qdoc._bold
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD1]
assert cFmt.foreground().color() == THEME.head
# Chapter
block = qdoc.document.findBlockByNumber(2)
assert block.text() == "Chapter"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD2][0]
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD2][1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.fontWeight() == qdoc._bold
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD2]
assert cFmt.foreground().color() == THEME.head
# Scene
block = qdoc.document.findBlockByNumber(3)
assert block.text() == "Scene"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD3][0]
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD3][1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.fontWeight() == qdoc._bold
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD3]
assert cFmt.foreground().color() == THEME.head
# Section
block = qdoc.document.findBlockByNumber(4)
assert block.text() == "Section"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mHead[qdoc.T_HEAD4][0]
assert bFmt.bottomMargin() == qdoc._mHead[qdoc.T_HEAD4][1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.fontWeight() == qdoc._bold
assert cFmt.fontPointSize() == qdoc._sHead[qdoc.T_HEAD4]
assert cFmt.foreground().color() == THEME.head
@pytest.mark.core
def testCoreToQTextDocument_SeparatorSkip(mockGUI):
"""Test separator and skip in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc._isNovel = True
qdoc._isFirst = True
qdoc._text = (
"#! Title\n"
"## Chapter\n"
"### Scene 1\n"
"Text 1\n"
"### Scene 2\n"
"Text 2\n"
"#### Section\n"
"Text 3\n"
)
qdoc.setSceneFormat("* * *", False)
qdoc.setSectionFormat("", False)
qdoc.tokenizeText()
qdoc.doConvert()
assert qdoc.document.blockCount() == 7
# 0: Title
block = qdoc.document.findBlockByNumber(0)
assert block.text() == "Title"
# 1: Chapter
block = qdoc.document.findBlockByNumber(1)
assert block.text() == "Chapter"
# Hidden: Scene 1
# 2: Text 1
block = qdoc.document.findBlockByNumber(2)
assert block.text() == "Text 1"
# 3: Scene 2
block = qdoc.document.findBlockByNumber(3)
assert block.text() == "* * *"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mSep[0]
assert bFmt.bottomMargin() == qdoc._mSep[1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.foreground().color() == THEME.text
# 4: Text 2
block = qdoc.document.findBlockByNumber(4)
assert block.text() == "Text 2"
# 5: Section
block = qdoc.document.findBlockByNumber(5)
assert block.text() == nwUnicode.U_NBSP
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mText[0]
assert bFmt.bottomMargin() == qdoc._mText[1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.foreground().color() == THEME.text
# 6: Text 3
block = qdoc.document.findBlockByNumber(6)
assert block.text() == "Text 3"
@pytest.mark.core
def testCoreToQTextDocument_NovelMeta(mockGUI):
"""Test novel meta formats in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc._isNovel = True
qdoc._isFirst = True
qdoc.setComments(True)
qdoc.setSynopsis(True)
qdoc.setKeywords(True)
qdoc._text = (
"### Scene\n\n"
"@pov: Jane\n"
"@char: John, Bob\n\n"
"%Synopsis: Stuff that happened\n\n"
"% A regular comment\n\n"
"Text\n\n"
)
qdoc.tokenizeText()
qdoc.doConvert()
assert qdoc.document.blockCount() == 6
# 0: Scene
block = qdoc.document.findBlockByNumber(0)
assert block.text() == "Scene"
# 1: Jane
block = qdoc.document.findBlockByNumber(1)
assert block.text() == "Point of View: Jane"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mMeta[0]
assert bFmt.bottomMargin() == 0.0
cFmt = charFmtInBlock(block, 1)
assert cFmt.foreground().color() == THEME.keyword
cFmt = charFmtInBlock(block, 16)
assert cFmt.foreground().color() == THEME.tag
# 2: John, Bob
block = qdoc.document.findBlockByNumber(2)
assert block.text() == "Characters: John, Bob"
bFmt = block.blockFormat()
assert bFmt.topMargin() == 0.0
assert bFmt.bottomMargin() == qdoc._mMeta[1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.foreground().color() == THEME.keyword
cFmt = charFmtInBlock(block, 13)
assert cFmt.foreground().color() == THEME.tag
# 3: Synopsis
block = qdoc.document.findBlockByNumber(3)
assert block.text() == "Synopsis: Stuff that happened"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mText[0]
assert bFmt.bottomMargin() == qdoc._mText[1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.foreground().color() == THEME.modifier
cFmt = charFmtInBlock(block, 11)
assert cFmt.foreground().color() == THEME.note
# 4: Comment
block = qdoc.document.findBlockByNumber(4)
assert block.text() == "Comment: A regular comment"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mText[0]
assert bFmt.bottomMargin() == qdoc._mText[1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.foreground().color() == THEME.comment
cFmt = charFmtInBlock(block, 10)
assert cFmt.foreground().color() == THEME.comment
# 5: Text
block = qdoc.document.findBlockByNumber(5)
assert block.text() == "Text"
@pytest.mark.core
def testCoreToQTextDocument_NoteMeta(mockGUI):
"""Test note meta formats in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc._isNovel = False
qdoc._isFirst = True
qdoc.setComments(True)
qdoc.setSynopsis(True)
qdoc.setKeywords(True)
qdoc._text = (
"# Jane Smith\n\n"
"@tag: Jane | Jane Smith\n"
"%Short: All about Jane\n\n"
"Text\n\n"
)
qdoc.tokenizeText()
qdoc.doConvert()
assert qdoc.document.blockCount() == 4
# 0: Title
block = qdoc.document.findBlockByNumber(0)
assert block.text() == "Jane Smith"
# 1: Tag
block = qdoc.document.findBlockByNumber(1)
assert block.text() == "Tag: Jane | Jane Smith"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mMeta[0]
assert bFmt.bottomMargin() == qdoc._mMeta[1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.foreground().color() == THEME.keyword
cFmt = charFmtInBlock(block, 6)
assert cFmt.foreground().color() == THEME.tag
cFmt = charFmtInBlock(block, 11)
assert cFmt.foreground().color() == THEME.text
cFmt = charFmtInBlock(block, 13)
assert cFmt.foreground().color() == THEME.optional
# 2: Short
block = qdoc.document.findBlockByNumber(2)
assert block.text() == "Short Description: All about Jane"
bFmt = block.blockFormat()
assert bFmt.topMargin() == qdoc._mText[0]
assert bFmt.bottomMargin() == qdoc._mText[1]
cFmt = charFmtInBlock(block, 1)
assert cFmt.foreground().color() == THEME.modifier
cFmt = charFmtInBlock(block, 20)
assert cFmt.foreground().color() == THEME.note
# 3: Text
block = qdoc.document.findBlockByNumber(3)
assert block.text() == "Text"
@pytest.mark.core
def testCoreToQTextDocument_TextBlockFormats(mockGUI):
"""Test text block formats in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc._isNovel = True
qdoc._isFirst = True
# Alignment & Indent
# ==================
qdoc.document.clear()
qdoc._text = (
"### Scene\n\n"
"Left <<\n\n"
">> Center <<\n\n"
">> Right\n\n"
"> Left Indent\n\n"
"Right Indent <\n\n"
"> Double Indent <\n\n"
)
qdoc.tokenizeText()
qdoc.doConvert()
assert qdoc.document.blockCount() == 7
# 0: Scene
block = qdoc.document.findBlockByNumber(0)
assert block.text() == "Scene"
# 1: Left
block = qdoc.document.findBlockByNumber(1)
assert block.text() == "Left"
bFmt = block.blockFormat()
assert bFmt.alignment() == QtAlignLeft
# 2: Center
block = qdoc.document.findBlockByNumber(2)
assert block.text() == "Center"
bFmt = block.blockFormat()
assert bFmt.alignment() == QtAlignCenter
# 3: Right
block = qdoc.document.findBlockByNumber(3)
assert block.text() == "Right"
bFmt = block.blockFormat()
assert bFmt.alignment() == QtAlignRight
# 4: Left Indent
block = qdoc.document.findBlockByNumber(4)
assert block.text() == "Left Indent"
bFmt = block.blockFormat()
assert bFmt.alignment() == QtAlignAbsolute
assert bFmt.leftMargin() == qdoc._mIndent
assert bFmt.rightMargin() == 0.0
# 5: Right Indent
block = qdoc.document.findBlockByNumber(5)
assert block.text() == "Right Indent"
bFmt = block.blockFormat()
assert bFmt.alignment() == QtAlignAbsolute
assert bFmt.leftMargin() == 0.0
assert bFmt.rightMargin() == qdoc._mIndent
# 6: Double Indent
block = qdoc.document.findBlockByNumber(6)
assert block.text() == "Double Indent"
bFmt = block.blockFormat()
assert bFmt.alignment() == QtAlignAbsolute
assert bFmt.leftMargin() == qdoc._mIndent
assert bFmt.rightMargin() == qdoc._mIndent
# Unreachable
# ===========
# Some formatting markers are currently not reachable
qdoc.document.clear()
qdoc._tokens = [
(qdoc.T_TEXT, 1, "This is justified", [], qdoc.A_JUSTIFY),
(qdoc.T_TEXT, 1, "This has a page break", [], qdoc.A_PBA),
]
qdoc.doConvert()
assert qdoc.document.blockCount() == 2
# 0: Justify
block = qdoc.document.findBlockByNumber(0)
assert block.text() == "This is justified"
bFmt = block.blockFormat()
assert bFmt.alignment() == QtAlignJustify
# 1: Page Break After
block = qdoc.document.findBlockByNumber(1)
assert block.text() == "This has a page break"
bFmt = block.blockFormat()
assert bFmt.pageBreakPolicy() == QtPageBreakAfter
@pytest.mark.core
def testCoreToQTextDocument_TextCharFormats(mockGUI):
"""Test text char formats in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
# Convert before init
qdoc._text = "Blabla"
qdoc.doConvert()
qdoc.tokenizeText()
assert qdoc.document.toPlainText() == ""
# Init
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc._isNovel = True
qdoc._isFirst = True
qdoc._text = (
"### Scene\n\n"
"With [b]bold[/b] text\n\n"
"With [i]italic[/i] text\n\n"
"With [s]deleted[/s] text\n\n"
"With [u]underlined[/u] text\n\n"
"With [m]highlighted[/m] text\n\n"
"With super[sup]script[/sup] text\n\n"
"With sub[sub]script[/sub] text\n\n"
)
qdoc.tokenizeText()
qdoc.doConvert()
assert qdoc.document.blockCount() == 8
# 0: Scene
block = qdoc.document.findBlockByNumber(0)
assert block.text() == "Scene"
# 1: Bold
block = qdoc.document.findBlockByNumber(1)
assert block.text() == "With bold text"
cFmt = charFmtInBlock(block, 1)
assert cFmt.fontWeight() == qdoc._normal
cFmt = charFmtInBlock(block, 6)
assert cFmt.fontWeight() == qdoc._bold
cFmt = charFmtInBlock(block, 10)
assert cFmt.fontWeight() == qdoc._normal
# 2: Italic
block = qdoc.document.findBlockByNumber(2)
assert block.text() == "With italic text"
cFmt = charFmtInBlock(block, 1)
assert cFmt.fontItalic() is False
cFmt = charFmtInBlock(block, 6)
assert cFmt.fontItalic() is True
cFmt = charFmtInBlock(block, 12)
assert cFmt.fontItalic() is False
# 3: Deleted
block = qdoc.document.findBlockByNumber(3)
assert block.text() == "With deleted text"
cFmt = charFmtInBlock(block, 1)
assert cFmt.fontStrikeOut() is False
cFmt = charFmtInBlock(block, 6)
assert cFmt.fontStrikeOut() is True
cFmt = charFmtInBlock(block, 13)
assert cFmt.fontStrikeOut() is False
# 4: Underlined
block = qdoc.document.findBlockByNumber(4)
assert block.text() == "With underlined text"
cFmt = charFmtInBlock(block, 1)
assert cFmt.fontUnderline() is False
cFmt = charFmtInBlock(block, 6)
assert cFmt.fontUnderline() is True
cFmt = charFmtInBlock(block, 16)
assert cFmt.fontUnderline() is False
# 5: Highlighted
block = qdoc.document.findBlockByNumber(5)
assert block.text() == "With highlighted text"
cFmt = charFmtInBlock(block, 1)
assert cFmt.background() == QtTransparent
cFmt = charFmtInBlock(block, 6)
assert cFmt.background() == THEME.highlight
cFmt = charFmtInBlock(block, 17)
assert cFmt.background() == QtTransparent
# 6: Superscript
block = qdoc.document.findBlockByNumber(6)
assert block.text() == "With superscript text"
cFmt = charFmtInBlock(block, 1)
assert cFmt.verticalAlignment() == QtVAlignNormal
cFmt = charFmtInBlock(block, 11)
assert cFmt.verticalAlignment() == QtVAlignSuper
cFmt = charFmtInBlock(block, 17)
assert cFmt.verticalAlignment() == QtVAlignNormal
# 7: Subscript
block = qdoc.document.findBlockByNumber(7)
assert block.text() == "With subscript text"
cFmt = charFmtInBlock(block, 1)
assert cFmt.verticalAlignment() == QtVAlignNormal
cFmt = charFmtInBlock(block, 9)
assert cFmt.verticalAlignment() == QtVAlignSub
cFmt = charFmtInBlock(block, 15)
assert cFmt.verticalAlignment() == QtVAlignNormal
@pytest.mark.core
def testCoreToQTextDocument_Footnotes(mockGUI):
"""Test footnotes in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc._isNovel = True
qdoc._isFirst = True
qdoc._text = (
"### Scene\n\n"
"Text with valid[footnote:fn1] and invalid[footnote:fn2] footnotes.\n\n"
"%Footnote.fn1: Here's the first note.\n\n"
)
qdoc.tokenizeText()
qdoc.doConvert()
qdoc.appendFootnotes()
assert qdoc.document.blockCount() == 4
# 0: Scene
block = qdoc.document.findBlockByNumber(0)
assert block.text() == "Scene"
# 1: Text
block = qdoc.document.findBlockByNumber(1)
assert block.text() == "Text with valid[1] and invalid[ERR] footnotes."
# 2: Footnotes
block = qdoc.document.findBlockByNumber(2)
assert block.text() == "Footnotes"
# 3: Footnote 1
block = qdoc.document.findBlockByNumber(3)
assert block.text() == "1. Here's the first note."
+8 -8
View File
@@ -27,7 +27,7 @@ from PyQt5.QtGui import QMouseEvent, QTextCursor
from PyQt5.QtWidgets import QAction, QApplication, QMenu
from novelwriter import CONFIG, SHARED
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.toqdoc import ToQTextDocument
from novelwriter.enum import nwDocAction
from novelwriter.gui.docviewer import GuiDocViewer
from novelwriter.types import QtModeNone, QtMouseLeft
@@ -119,7 +119,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
# Select All
assert docViewer.docAction(nwDocAction.SEL_ALL) is True
cursor = docViewer.textCursor()
assert len(cursor.selectedText()) == 3061
assert len(cursor.selectedText()) == 3060
# Other actions
assert docViewer.docAction(nwDocAction.NO_ACTION) is False
@@ -157,7 +157,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
docViewer.setTextCursor(cursor)
docViewer._makeSelection(QTextCursor.WordUnderCursor)
rect = docViewer.cursorRect()
docViewer._linkClicked(QUrl("#char=Bod"))
docViewer._linkClicked(QUrl("#tag_bod"))
assert docViewer.docHandle == "4c4f28287af27"
# Click mouse nav buttons
@@ -196,19 +196,19 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
# Document footer show/hide synopsis
assert nwGUI.viewDocument("f96ec11c6a3da") is True
assert len(docViewer.toPlainText()) == 4315
assert len(docViewer.toPlainText()) == 4314
docViewer.docFooter._doToggleSynopsis(False)
assert len(docViewer.toPlainText()) == 4099
assert len(docViewer.toPlainText()) == 4098
# Document footer show/hide comments
assert nwGUI.viewDocument("846352075de7d") is True
assert len(docViewer.toPlainText()) == 675
assert len(docViewer.toPlainText()) == 683
docViewer.docFooter._doToggleComments(False)
assert len(docViewer.toPlainText()) == 635
assert len(docViewer.toPlainText()) == 634
# Crash the HTML rendering
with monkeypatch.context() as mp:
mp.setattr(ToHtml, "doConvert", causeException)
mp.setattr(ToQTextDocument, "doConvert", causeException)
assert docViewer.loadText("846352075de7d") is False
assert docViewer.toPlainText() == "An error occurred while generating the preview."
+1 -1
View File
@@ -40,7 +40,7 @@ from tests.tools import buildTestProject
@pytest.mark.gui
def testManuscriptBuild_Main(
def testToolManuscriptBuild_Main(
monkeypatch, qtbot: QtBot, nwGUI: GuiMain, fncPath: Path, projPath: Path, mockRnd
):
"""Test the GuiManuscriptBuild dialog."""
+15 -45
View File
@@ -28,20 +28,19 @@ from PyQt5.QtCore import pyqtSlot
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from PyQt5.QtWidgets import QAction, QListWidgetItem
from novelwriter import CONFIG, SHARED
from novelwriter import SHARED
from novelwriter.constants import nwHeadFmt
from novelwriter.core.buildsettings import BuildSettings
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.manussettings import GuiBuildSettings
from novelwriter.types import QtAlignAbsolute, QtAlignJustify, QtDialogApply, QtDialogSave
from novelwriter.types import QtDialogApply, QtDialogSave
from tests.mocked import causeOSError
from tests.tools import C, buildTestProject
@pytest.mark.gui
def testManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
def testToolManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
"""Test the init/main functionality of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -55,38 +54,27 @@ def testManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.show()
assert manus.docPreview.toPlainText().strip() == ""
# Run the default build
# First load should have create a default build
assert manus.buildList.count() == 1
# Loading again should not add a new build
manus.loadContent()
assert manus.buildList.count() == 1
# Build a preview
manus.buildList.clearSelection()
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() == allText
manus.close()
# A new dialog should load the old build
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
assert manus.docPreview.toPlainText().strip() == allText
manus.close()
# But blocking the reload should leave it empty
with monkeypatch.context() as mp:
mp.setattr("builtins.open", lambda *a, **k: causeOSError)
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
assert manus.docPreview.toPlainText().strip() == ""
nwGUI.closeProject() # This should auto-close the manuscript tool
assert manus.isHidden()
# qtbot.stop()
@pytest.mark.gui
def testManuscript_Builds(qtbot, nwGUI, projPath):
def testToolManuscript_Builds(qtbot, nwGUI, projPath):
"""Test the handling of builds in the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -157,7 +145,7 @@ def testManuscript_Builds(qtbot, nwGUI, projPath):
@pytest.mark.gui
def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
def testToolManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
"""Test other features of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -186,7 +174,6 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.show()
manus.loadContent()
cacheFile = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
manus.buildList.setCurrentRow(0)
build = manus._getSelectedBuild()
assert isinstance(build, BuildSettings)
@@ -199,17 +186,9 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.btnPreview.click()
qtbot.wait(200) # Should be enough to run the build
assert manus.docPreview.toPlainText().strip() == ""
assert cacheFile.exists() is False
manus._updateBuildsList()
# Preview the first, but fail to save cache
manus.buildList.setCurrentRow(0)
with monkeypatch.context() as mp:
mp.setattr("builtins.open", lambda *a, **k: causeOSError)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert cacheFile.exists() is False
# Preview the first
first = manus.buildList.item(0)
assert isinstance(first, QListWidgetItem)
build = manus._builds.getBuild(first.data(GuiManuscript.D_KEY))
@@ -218,12 +197,10 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
build.setValue("headings.fmtAltScene", nwHeadFmt.TITLE)
manus._builds.setBuild(build)
# Preview again, and allow cache file to be created
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() != ""
assert cacheFile.exists() is True
# Check Outline
assert manus.buildOutline._outline == {
@@ -265,13 +242,6 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
assert manus.docStats.maxTotalWords.text() == "25"
assert manus.docStats.maxTotalChars.text() == "117"
# Toggle justify
assert manus.docPreview.document().defaultTextOption().alignment() == QtAlignAbsolute
manus.docPreview.setJustify(True)
assert manus.docPreview.document().defaultTextOption().alignment() == QtAlignJustify
manus.docPreview.setJustify(False)
assert manus.docPreview.document().defaultTextOption().alignment() == QtAlignAbsolute
# Tests are too fast to trigger this one, so we trigger it manually to ensure it isn't failing
manus.docPreview._postUpdate()
@@ -299,7 +269,7 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
@pytest.mark.gui
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
def testManuscript_Print(monkeypatch, qtbot, nwGUI, projPath):
def testToolManuscript_Print(monkeypatch, qtbot, nwGUI, projPath):
"""Test the print feature of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
+6 -6
View File
@@ -40,7 +40,7 @@ from tests.tools import C, buildTestProject
@pytest.mark.gui
def testBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
"""Test the initialisation of the GuiBuildSettings dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -110,7 +110,7 @@ def testBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
@pytest.mark.gui
def testBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
"""Test the Filter Tab of the GuiBuildSettings dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
@@ -314,7 +314,7 @@ def testBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
@pytest.mark.gui
def testBuildSettings_Headings(qtbot, nwGUI):
def testToolBuildSettings_Headings(qtbot, nwGUI):
"""Test the Headings Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
@@ -484,7 +484,7 @@ def testBuildSettings_Headings(qtbot, nwGUI):
@pytest.mark.gui
def testBuildSettings_Content(qtbot, nwGUI):
def testToolBuildSettings_Content(qtbot, nwGUI):
"""Test the Content Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
@@ -544,7 +544,7 @@ def testBuildSettings_Content(qtbot, nwGUI):
@pytest.mark.gui
def testBuildSettings_Format(monkeypatch, qtbot, nwGUI):
def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
"""Test the Format Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
@@ -650,7 +650,7 @@ def testBuildSettings_Format(monkeypatch, qtbot, nwGUI):
@pytest.mark.gui
def testBuildSettings_Output(qtbot, nwGUI):
def testToolBuildSettings_Output(qtbot, nwGUI):
"""Test the Output Tab of the GuiBuildSettings dialog."""
build = BuildSettings()