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_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} 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: 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.tokenizer import Tokenizer
from novelwriter.core.tomarkdown import ToMarkdown from novelwriter.core.tomarkdown import ToMarkdown
from novelwriter.core.toodt import ToOdt from novelwriter.core.toodt import ToOdt
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.enum import nwBuildFmt from novelwriter.enum import nwBuildFmt
from novelwriter.error import formatException, logException from novelwriter.error import formatException, logException
@@ -54,7 +55,7 @@ class NWBuildDocument:
__slots__ = ( __slots__ = (
"_project", "_build", "_queue", "_error", "_cache", "_count", "_project", "_build", "_queue", "_error", "_cache", "_count",
"_outline", "_preview" "_outline",
) )
def __init__(self, project: NWProject, build: BuildSettings) -> None: def __init__(self, project: NWProject, build: BuildSettings) -> None:
@@ -65,7 +66,6 @@ class NWBuildDocument:
self._cache = None self._cache = None
self._count = False self._count = False
self._outline = False self._outline = False
self._preview = False
return return
## ##
@@ -99,15 +99,6 @@ class NWBuildDocument:
self._outline = state self._outline = state
return 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 # Special Methods
## ##
@@ -134,6 +125,32 @@ class NWBuildDocument:
self._queue.append(item.itemHandle) self._queue.append(item.itemHandle)
return 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]]: def iterBuild(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
"""Wrapper for builders based on format.""" """Wrapper for builders based on format."""
if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT): if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT):
@@ -182,8 +199,6 @@ class NWBuildDocument:
makeObj = ToHtml(self._project) makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj) filtered = self._setupBuild(makeObj)
makeObj.setPreview(self._preview)
makeObj.setLinkHeadings(self._preview)
for i, tHandle in enumerate(self._queue): for i, tHandle in enumerate(self._queue):
self._error = None self._error = None
if filtered.get(tHandle, (False, 0))[0]: if filtered.get(tHandle, (False, 0))[0]:
@@ -193,7 +208,7 @@ class NWBuildDocument:
makeObj.appendFootnotes() makeObj.appendFootnotes()
if not (self._build.getBool("html.preserveTabs") or self._preview): if not self._build.getBool("html.preserveTabs"):
makeObj.replaceTabs() makeObj.replaceTabs()
self._error = None self._error = None
+14 -55
View File
@@ -38,24 +38,6 @@ from novelwriter.types import FONT_STYLE, FONT_WEIGHTS
logger = logging.getLogger(__name__) 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 = { HTML5_TAGS = {
Tokenizer.FMT_B_B: "<strong>", Tokenizer.FMT_B_B: "<strong>",
Tokenizer.FMT_B_E: "</strong>", Tokenizer.FMT_B_E: "</strong>",
@@ -82,14 +64,9 @@ class ToHtml(Tokenizer):
also used by the Document Viewer, and Manuscript Build Preview. 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: def __init__(self, project: NWProject) -> None:
super().__init__(project) super().__init__(project)
self._genMode = self.M_EXPORT
self._cssStyles = True self._cssStyles = True
self._fullHTML: list[str] = [] self._fullHTML: list[str] = []
@@ -112,11 +89,6 @@ class ToHtml(Tokenizer):
# Setters # 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: def setStyles(self, cssStyles: bool) -> None:
"""Enable or disable CSS styling. Some elements may still have """Enable or disable CSS styling. Some elements may still have
class tags. class tags.
@@ -157,8 +129,7 @@ class ToHtml(Tokenizer):
"""Convert the list of text tokens into an HTML document.""" """Convert the list of text tokens into an HTML document."""
self._result = "" self._result = ""
hTags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS if self._isNovel:
if self._isNovel and self._genMode != self.M_PREVIEW:
# For story files, we bump the titles one level up # For story files, we bump the titles one level up
h1Cl = " class='title'" h1Cl = " class='title'"
h1 = "h1" h1 = "h1"
@@ -240,7 +211,7 @@ class ToHtml(Tokenizer):
# Process Text Type # Process Text Type
if tType == self.T_TEXT: 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: elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "<br>") tHead = tText.replace(nwHeadFmt.BR, "<br>")
@@ -269,13 +240,13 @@ class ToHtml(Tokenizer):
lines.append(f"<p class='skip'{hStyle}>&nbsp;</p>\n") lines.append(f"<p class='skip'{hStyle}>&nbsp;</p>\n")
elif tType == self.T_SYNOPSIS and self._doSynopsis: 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: 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: 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: elif tType == self.T_KEYWORD and self._doKeywords:
tag, text = self._formatKeywords(tText) tag, text = self._formatKeywords(tText)
@@ -291,7 +262,6 @@ class ToHtml(Tokenizer):
def appendFootnotes(self) -> None: def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer.""" """Append the footnotes in the buffer."""
if self._usedNotes: if self._usedNotes:
tags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS
footnotes = self._localLookup("Footnotes") footnotes = self._localLookup("Footnotes")
lines = [] lines = []
@@ -299,7 +269,7 @@ class ToHtml(Tokenizer):
lines.append("<ol>\n") lines.append("<ol>\n")
for key, index in self._usedNotes.items(): for key, index in self._usedNotes.items():
if content := self._footnotes.get(key): 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(f"<li id='footnote_{index}'><p>{text}</p></li>\n")
lines.append("</ol>\n") lines.append("</ol>\n")
@@ -468,7 +438,7 @@ class ToHtml(Tokenizer):
# Internal Functions # 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.""" """Apply formatting tags to text."""
temp = text temp = text
for pos, fmt, data in reversed(tFmt): for pos, fmt, data in reversed(tFmt):
@@ -481,7 +451,7 @@ class ToHtml(Tokenizer):
else: else:
html = "<sup>ERR</sup>" html = "<sup>ERR</sup>"
else: else:
html = tags.get(fmt, "ERR") html = HTML5_TAGS.get(fmt, "ERR")
temp = f"{temp[:pos]}{html}{temp[pos:]}" temp = f"{temp[:pos]}{html}{temp[pos:]}"
temp = temp.replace("\n", "<br>") temp = temp.replace("\n", "<br>")
return stripEscape(temp) return stripEscape(temp)
@@ -492,18 +462,12 @@ class ToHtml(Tokenizer):
sSynop = self._localLookup("Synopsis") sSynop = self._localLookup("Synopsis")
else: else:
sSynop = self._localLookup("Short Description") sSynop = self._localLookup("Short Description")
if self._genMode == self.M_PREVIEW: return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
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"
def _formatComments(self, text: str) -> str: def _formatComments(self, text: str) -> str:
"""Apply HTML formatting to comments.""" """Apply HTML formatting to comments."""
if self._genMode == self.M_PREVIEW: sComm = self._localLookup("Comment")
return f"<p class='comment'>{text}</p>\n" return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
else:
sComm = self._localLookup("Comment")
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
def _formatKeywords(self, text: str) -> tuple[str, str]: def _formatKeywords(self, text: str) -> tuple[str, str]:
"""Apply HTML formatting to keywords.""" """Apply HTML formatting to keywords."""
@@ -519,13 +483,8 @@ class ToHtml(Tokenizer):
if two: if two:
result += f" | <span class='optional'>{two}</a>" result += f" | <span class='optional'>{two}</a>"
else: else:
if self._genMode == self.M_PREVIEW: result += ", ".join(
result += ", ".join( f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
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:]
)
return bits[0][1:], result return bits[0][1:], result
+16 -6
View File
@@ -119,6 +119,7 @@ class Tokenizer(ABC):
# Lookups # Lookups
L_HEADINGS = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD3, T_HEAD4] 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_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: def __init__(self, project: NWProject) -> None:
@@ -155,14 +156,15 @@ class Tokenizer(ABC):
self._keepBreaks = True # Keep line breaks in paragraphs self._keepBreaks = True # Keep line breaks in paragraphs
# Margins # Margins
self._marginTitle = (1.000, 0.500) self._marginTitle = (1.417, 0.500)
self._marginHead1 = (1.000, 0.500) self._marginHead1 = (1.417, 0.500)
self._marginHead2 = (0.834, 0.500) self._marginHead2 = (1.668, 0.500)
self._marginHead3 = (0.584, 0.500) self._marginHead3 = (1.168, 0.500)
self._marginHead4 = (0.584, 0.500) self._marginHead4 = (1.168, 0.500)
self._marginText = (0.000, 0.584) self._marginText = (0.000, 0.584)
self._marginMeta = (0.000, 0.584) self._marginMeta = (0.000, 0.584)
self._marginFoot = (1.417, 0.467) self._marginFoot = (1.417, 0.467)
self._marginSep = (1.168, 1.168)
# Title Formats # Title Formats
self._fmtTitle = nwHeadFmt.TITLE # Formatting for titles self._fmtTitle = nwHeadFmt.TITLE # Formatting for titles
@@ -378,6 +380,11 @@ class Tokenizer(ABC):
self._marginMeta = (float(upper), float(lower)) self._marginMeta = (float(upper), float(lower))
return 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: def setLinkHeadings(self, state: bool) -> None:
"""Enable or disable adding an anchor before headings.""" """Enable or disable adding an anchor before headings."""
self._linkHeadings = state self._linkHeadings = state
@@ -597,7 +604,10 @@ class Tokenizer(ABC):
# are automatically skipped. # are automatically skipped.
valid, bits, _ = self._project.index.scanThis(aLine) 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(( tokens.append((
self.T_KEYWORD, nHead, aLine[1:].strip(), [], sAlign self.T_KEYWORD, nHead, aLine[1:].strip(), [], sAlign
)) ))
+7 -3
View File
@@ -192,6 +192,7 @@ class ToOdt(Tokenizer):
self._mTopHead = "0.423cm" self._mTopHead = "0.423cm"
self._mTopText = "0.000cm" self._mTopText = "0.000cm"
self._mTopMeta = "0.000cm" self._mTopMeta = "0.000cm"
self._mTopSep = "0.247cm"
self._mBotTitle = "0.212cm" self._mBotTitle = "0.212cm"
self._mBotHead1 = "0.212cm" self._mBotHead1 = "0.212cm"
@@ -201,6 +202,7 @@ class ToOdt(Tokenizer):
self._mBotHead = "0.212cm" self._mBotHead = "0.212cm"
self._mBotText = "0.247cm" self._mBotText = "0.247cm"
self._mBotMeta = "0.106cm" self._mBotMeta = "0.106cm"
self._mBotSep = "0.247cm"
self._mBotFoot = "0.106cm" self._mBotFoot = "0.106cm"
self._mLeftFoot = "0.600cm" self._mLeftFoot = "0.600cm"
@@ -299,6 +301,7 @@ class ToOdt(Tokenizer):
self._mTopHead = self._emToCm(mScale * self._marginHead4[0]) self._mTopHead = self._emToCm(mScale * self._marginHead4[0])
self._mTopText = self._emToCm(mScale * self._marginText[0]) self._mTopText = self._emToCm(mScale * self._marginText[0])
self._mTopMeta = self._emToCm(mScale * self._marginMeta[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._mBotTitle = self._emToCm(mScale * self._marginTitle[1])
self._mBotHead1 = self._emToCm(mScale * self._marginHead1[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._mBotHead = self._emToCm(mScale * self._marginHead4[1])
self._mBotText = self._emToCm(mScale * self._marginText[1]) self._mBotText = self._emToCm(mScale * self._marginText[1])
self._mBotMeta = self._emToCm(mScale * self._marginMeta[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._mLeftFoot = self._emToCm(self._marginFoot[0])
self._mBotFoot = self._emToCm(self._marginFoot[1]) self._mBotFoot = self._emToCm(self._marginFoot[1])
@@ -501,7 +505,7 @@ class ToOdt(Tokenizer):
self._addTextPar(xText, S_SEP, oStyle, tText) self._addTextPar(xText, S_SEP, oStyle, tText)
elif tType == self.T_SKIP: 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: elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, tFmt = self._formatSynopsis(tText, tFormat, True) tTemp, tFmt = self._formatSynopsis(tText, tFormat, True)
@@ -944,8 +948,8 @@ class ToOdt(Tokenizer):
style.setParentStyleName("Standard") style.setParentStyleName("Standard")
style.setNextStyleName(S_TEXT) style.setNextStyleName(S_TEXT)
style.setClass("text") style.setClass("text")
style.setMarginTop(self._mTopText) style.setMarginTop(self._mTopSep)
style.setMarginBottom(self._mBotText) style.setMarginBottom(self._mBotSep)
style.setLineHeight(self._fLineHeight) style.setLineHeight(self._fLineHeight)
style.setTextAlign("center") style.setTextAlign("center")
style.setFontName(self._fontFamily) 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 # Reload spell check and dictionaries
SHARED.updateSpellCheckLanguage() SHARED.updateSpellCheckLanguage()
# Set font # Set the font. See issues #1862 and #1875.
self.initFont() self.setFont(CONFIG.textFont)
self.docHeader.updateFont()
self.docFooter.updateFont()
self.docSearch.updateFont()
# Update highlighter settings # Update highlighter settings
self._qDocument.syntaxHighlighter.initHighlighter() self._qDocument.syntaxHighlighter.initHighlighter()
@@ -372,17 +375,6 @@ class GuiDocEditor(QPlainTextEdit):
return 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: def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
"""Load text from a document into the editor. If we have an I/O """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 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 enum import Enum
from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot 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 ( from PyQt5.QtWidgets import (
QAction, QApplication, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser, QAction, QApplication, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser,
QToolButton, QWidget QToolButton, QWidget
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import cssCol
from novelwriter.constants import nwHeaders, nwUnicode 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.enum import nwDocAction, nwDocMode, nwItemType
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.extensions.eventfilters import WheelEventFilter from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToolButton from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.types import ( from novelwriter.types import QtAlignCenterTop, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
QtAlignCenterTop, QtAlignJustify, QtKeepAnchor, QtMouseLeft, QtMoveAnchor
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -69,6 +66,7 @@ class GuiDocViewer(QTextBrowser):
# Internal Variables # Internal Variables
self._docHandle = None self._docHandle = None
self._docTheme = TextDocumentTheme()
# Settings # Settings
self.setMinimumWidth(CONFIG.pxInt(300)) self.setMinimumWidth(CONFIG.pxInt(300))
@@ -137,8 +135,10 @@ class GuiDocViewer(QTextBrowser):
def initViewer(self) -> None: def initViewer(self) -> None:
"""Set editor settings from main config.""" """Set editor settings from main config."""
self._makeStyleSheet() # Set the font. See issues #1862 and #1875.
self.initFont() self.setFont(CONFIG.textFont)
self.docHeader.updateFont()
self.docFooter.updateFont()
# Set the widget colours to match syntax theme # Set the widget colours to match syntax theme
mainPalette = self.palette() mainPalette = self.palette()
@@ -151,16 +151,23 @@ class GuiDocViewer(QTextBrowser):
docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack) docPalette.setColor(QPalette.ColorRole.Base, SHARED.theme.colBack)
docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText) docPalette.setColor(QPalette.ColorRole.Text, SHARED.theme.colText)
self.viewport().setPalette(docPalette) self.viewport().setPalette(docPalette)
self.docHeader.matchColours() self.docHeader.matchColours()
self.docFooter.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 # Set default text margins
self.document().setDocumentMargin(0) self.document().setDocumentMargin(0)
options = QTextOption()
if CONFIG.doJustify:
options.setAlignment(QtAlignJustify)
self.document().setDefaultTextOption(options)
# Scroll bars # Scroll bars
if CONFIG.hideVScroll: if CONFIG.hideVScroll:
@@ -181,16 +188,6 @@ class GuiDocViewer(QTextBrowser):
return 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: def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
"""Load text into the viewer from an item handle.""" """Load text into the viewer from an item handle."""
if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE): if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
@@ -202,22 +199,22 @@ class GuiDocViewer(QTextBrowser):
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
sPos = self.verticalScrollBar().value() sPos = self.verticalScrollBar().value()
aDoc = ToHtml(SHARED.project) qDoc = ToQTextDocument(SHARED.project)
aDoc.setPreview(True) qDoc.setJustify(CONFIG.doJustify)
aDoc.setKeywords(True) qDoc.initDocument(CONFIG.textFont, self._docTheme)
aDoc.setComments(CONFIG.viewComments) qDoc.setKeywords(True)
aDoc.setSynopsis(CONFIG.viewSynopsis) qDoc.setComments(CONFIG.viewComments)
aDoc.setLinkHeadings(True) qDoc.setSynopsis(CONFIG.viewSynopsis)
# Be extra careful here to prevent crashes when first opening a # Be extra careful here to prevent crashes when first opening a
# project as a crash here leaves no way of recovering. # project as a crash here leaves no way of recovering.
# See issue #298 # See issue #298
try: try:
aDoc.setText(tHandle) qDoc.setText(tHandle)
aDoc.doPreProcessing() qDoc.doPreProcessing()
aDoc.tokenizeText() qDoc.tokenizeText()
aDoc.doConvert() qDoc.doConvert()
aDoc.appendFootnotes() qDoc.appendFootnotes()
except Exception: except Exception:
logger.error("Failed to generate preview for document with handle '%s'", tHandle) logger.error("Failed to generate preview for document with handle '%s'", tHandle)
logException() logException()
@@ -233,11 +230,7 @@ class GuiDocViewer(QTextBrowser):
self.docHistory.append(tHandle) self.docHistory.append(tHandle)
self.setDocumentTitle(tHandle) self.setDocumentTitle(tHandle)
self.setDocument(qDoc.document)
# 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")
if self._docHandle == tHandle: if self._docHandle == tHandle:
# This is a refresh, so we set the scrollbar back to where it was # This is a refresh, so we set the scrollbar back to where it was
@@ -375,12 +368,10 @@ class GuiDocViewer(QTextBrowser):
@pyqtSlot("QUrl") @pyqtSlot("QUrl")
def _linkClicked(self, url: QUrl) -> None: def _linkClicked(self, url: QUrl) -> None:
"""Process a clicked link in the document.""" """Process a clicked link in the document."""
link = url.url() if link := url.url():
logger.debug("Clicked link: '%s'", link) logger.debug("Clicked link: '%s'", link)
if len(link) > 0: if (bits := link.partition("_")) and bits[2]:
bits = link.split("=") self.loadDocumentTagRequest.emit(bits[2], nwDocMode.VIEW)
if len(bits) == 2:
self.loadDocumentTagRequest.emit(bits[1], nwDocMode.VIEW)
return return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
@@ -470,34 +461,6 @@ class GuiDocViewer(QTextBrowser):
self._makeSelection(selType) self._makeSelection(selType)
return 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: class GuiDocViewHistory:
+2 -4
View File
@@ -800,8 +800,7 @@ class GuiMain(QMainWindow):
def showBuildManuscriptDialog(self) -> None: def showBuildManuscriptDialog(self) -> None:
"""Open the build manuscript dialog.""" """Open the build manuscript dialog."""
if SHARED.hasProject: if SHARED.hasProject:
if (dialog := SHARED.findTopLevelWidget(GuiManuscript)) is None: dialog = GuiManuscript(self)
dialog = GuiManuscript(self)
dialog.activateDialog() dialog.activateDialog()
dialog.loadContent() dialog.loadContent()
return return
@@ -819,8 +818,7 @@ class GuiMain(QMainWindow):
def showWritingStatsDialog(self) -> None: def showWritingStatsDialog(self) -> None:
"""Open the session stats dialog.""" """Open the session stats dialog."""
if SHARED.hasProject: if SHARED.hasProject:
if (dialog := SHARED.findTopLevelWidget(GuiWritingStats)) is None: dialog = GuiWritingStats(self)
dialog = GuiWritingStats(self)
dialog.activateDialog() dialog.activateDialog()
dialog.populateGUI() dialog.populateGUI()
return 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 from __future__ import annotations
import json
import logging import logging
from datetime import datetime
from time import time from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot 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.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout, QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout,
@@ -41,20 +39,19 @@ from PyQt5.QtWidgets import (
) )
from novelwriter import CONFIG, SHARED 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.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.tokenizer import HeadingFormatter 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.circularprogress import NProgressCircle
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
from novelwriter.tools.manusbuild import GuiManuscriptBuild from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings from novelwriter.tools.manussettings import GuiBuildSettings
from novelwriter.types import ( from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignRight, QtAlignTop, QtAlignCenter, QtAlignRight, QtAlignTop, QtSizeExpanding, QtSizeIgnored,
QtSizeExpanding, QtSizeIgnored, QtUserRole QtUserRole
) )
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@@ -249,20 +246,7 @@ class GuiManuscript(NToolDialog):
self._updateBuildsList() self._updateBuildsList()
if selected in self._buildMap: if selected in self._buildMap:
self.buildList.setCurrentItem(self._buildMap[selected]) self.buildList.setCurrentItem(self._buildMap[selected])
QTimer.singleShot(200, self._generatePreview)
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
return return
@@ -342,36 +326,37 @@ class GuiManuscript(NToolDialog):
SHARED.saveDocument() SHARED.saveDocument()
docBuild = NWBuildDocument(SHARED.project, build) docBuild = NWBuildDocument(SHARED.project, build)
docBuild.setPreviewMode(True)
docBuild.queueAll() 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)) self.docPreview.beginNewBuild(len(docBuild))
for step, _ in docBuild.iterBuildHTML(None): for step, _ in docBuild.iterBuildPreview(theme):
self.docPreview.buildStep(step + 1) self.docPreview.buildStep(step + 1)
QApplication.processEvents() QApplication.processEvents()
buildObj = docBuild.lastBuild buildObj = docBuild.lastBuild
assert isinstance(buildObj, ToHtml) assert isinstance(buildObj, ToQTextDocument)
result = {
"uuid": build.buildID,
"time": int(time()),
"stats": buildObj.textStats,
"outline": buildObj.textOutline,
"styles": buildObj.getStyleSheet(),
"html": buildObj.fullHTML,
}
self._updatePreview(result, build) font = QFont()
font.fromString(build.getStr("format.textFont"))
logger.debug("Saving build cache") self.docPreview.setTextFont(font)
cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json" self.docPreview.setContent(buildObj.document)
try: self.docPreview.setBuildName(build.name)
with open(cache, mode="w+", encoding="utf-8") as outFile:
outFile.write(json.dumps(result, indent=2)) self.docStats.updateStats(buildObj.textStats)
except Exception: self.buildOutline.updateOutline(buildObj.textOutline)
logger.error("Failed to save build cache")
logException()
return
return return
@@ -379,8 +364,8 @@ class GuiManuscript(NToolDialog):
def _buildManuscript(self) -> None: def _buildManuscript(self) -> None:
"""Open the build dialog and build the manuscript.""" """Open the build dialog and build the manuscript."""
if build := self._getSelectedBuild(): if build := self._getSelectedBuild():
dlgBuild = GuiManuscriptBuild(self, build) dialog = GuiManuscriptBuild(self, build)
dlgBuild.exec() dialog.exec()
# After the build is done, save build settings changes # After the build is done, save build settings changes
if build.changed: if build.changed:
@@ -400,21 +385,6 @@ class GuiManuscript(NToolDialog):
# Internal Functions # 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: def _getSelectedBuild(self) -> BuildSettings | None:
"""Get the currently selected build. If none are selected, """Get the currently selected build. If none are selected,
automatically select the first one. automatically select the first one.
@@ -807,16 +777,6 @@ class _PreviewWidget(QTextBrowser):
self._updateBuildAge() self._updateBuildAge()
return 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: def setTextFont(self, font: QFont) -> None:
"""Set the text font properties and then reset for sub-widgets. """Set the text font properties and then reset for sub-widgets.
This needs special attention since there appears to be a bug in This needs special attention since there appears to be a bug in
@@ -848,31 +808,19 @@ class _PreviewWidget(QTextBrowser):
QApplication.processEvents() QApplication.processEvents()
return return
def setContent(self, data: dict) -> None: def setContent(self, document: QTextDocument) -> None:
"""Set the content of the preview widget.""" """Set the content of the preview widget."""
QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
self.buildProgress.setCentreText(self.tr("Processing ...")) self.buildProgress.setCentreText(self.tr("Processing ..."))
QApplication.processEvents() QApplication.processEvents()
styles = "\n".join(data.get("styles", [])) document.setDocumentMargin(CONFIG.getTextMargin())
self.document().setDefaultStyleSheet(styles) self.setDocument(document)
html = "".join(data.get("html", [])) self._docTime = int(time())
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._updateBuildAge() 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")) self.buildProgress.setCentreText(self.tr("Done"))
QApplication.restoreOverrideCursor() QApplication.restoreOverrideCursor()
QApplication.processEvents() QApplication.processEvents()
@@ -917,17 +865,14 @@ class _PreviewWidget(QTextBrowser):
@pyqtSlot() @pyqtSlot()
def _updateBuildAge(self) -> None: def _updateBuildAge(self) -> None:
"""Update the build time and the fuzzy age.""" """Update the build time and the fuzzy age."""
if self._docTime > 0: if self._buildName and self._docTime > 0:
strBuildTime = "%s (%s)" % ( self.ageLabel.setText("<b>{0}</b><br>{1}: {2}".format(
CONFIG.localDateTime(datetime.fromtimestamp(self._docTime)), self._buildName,
fuzzyTime(int(time()) - self._docTime) self.tr("Built"),
) fuzzyTime(int(time()) - self._docTime),
))
else: else:
strBuildTime = self.tr("Unknown") self.ageLabel.setText("<b>{0}</b>".format(self.tr("No Preview")))
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)
return return
@pyqtSlot() @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 __future__ import annotations
from PyQt5.QtCore import QRegularExpression, Qt 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 from PyQt5.QtWidgets import QDialogButtonBox, QSizePolicy, QStyle
# Qt Alignment Flags # Qt Alignment Flags
@@ -44,6 +44,10 @@ QtAlignRightMiddle = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
QtAlignRightTop = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop QtAlignRightTop = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop
QtAlignTop = Qt.AlignmentFlag.AlignTop QtAlignTop = Qt.AlignmentFlag.AlignTop
QtVAlignNormal = QTextCharFormat.VerticalAlignment.AlignNormal
QtVAlignSub = QTextCharFormat.VerticalAlignment.AlignSubScript
QtVAlignSuper = QTextCharFormat.VerticalAlignment.AlignSuperScript
# Qt Page Break # Qt Page Break
QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore QtPageBreakBefore = QTextFormat.PageBreakFlag.PageBreak_AlwaysBefore
@@ -52,6 +56,7 @@ QtPageBreakAfter = QTextFormat.PageBreakFlag.PageBreak_AlwaysAfter
# Qt Painter Types # Qt Painter Types
QtTransparent = QColor(0, 0, 0, 0) QtTransparent = QColor(0, 0, 0, 0)
QtBlack = QColor(0, 0, 0)
QtNoBrush = Qt.BrushStyle.NoBrush QtNoBrush = Qt.BrushStyle.NoBrush
QtNoPen = Qt.PenStyle.NoPen QtNoPen = Qt.PenStyle.NoPen
QtRoundCap = Qt.PenCapStyle.RoundCap QtRoundCap = Qt.PenCapStyle.RoundCap
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?> <?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: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> <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:generator>novelWriter/2.5a4</meta:generator>
<meta:initial-creator>Jane Smith</meta:initial-creator> <meta:initial-creator>Jane Smith</meta:initial-creator>
<meta:editing-cycles>1234</meta:editing-cycles> <meta:editing-cycles>1234</meta:editing-cycles>
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration> <meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
<dc:title>Test Project</dc:title> <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> <dc:creator>Jane Smith</dc:creator>
</office:meta> </office:meta>
<office:font-face-decls> <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: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:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text"> <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: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: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" /> <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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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:style style:name="Header" style:family="paragraph" style:display-name="Header" style:parent-style-name="Header_20_and_20_Footer"> <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: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:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text"> <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: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: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" /> <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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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:style style:name="Header" style:family="paragraph" style:display-name="Header" style:parent-style-name="Header_20_and_20_Footer"> <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> <style>
body {font-family: 'Arial'; font-size: 12pt; font-weight: 400; font-style: normal;} 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;} 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;} 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: 1.09em; 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: 0.76em; 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: 0.76em; 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.30em; 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;} .sep, .skip {text-align: center; margin-top: 1.30em; margin-bottom: 1.30em;}
a {color: rgb(66, 113, 174);} a {color: rgb(66, 113, 174);}
mark {background: rgb(255, 255, 166);} mark {background: rgb(255, 255, 166);}
@@ -2,18 +2,18 @@
"meta": { "meta": {
"projectName": "Lorem Ipsum", "projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com", "novelAuthor": "lipsum.com",
"buildTime": 1716412100, "buildTime": 1716579341,
"buildTimeStr": "2024-05-22 23:08:20" "buildTimeStr": "2024-05-24 21:35:41"
}, },
"text": { "text": {
"css": [ "css": [
"body {font-family: 'Arial'; font-size: 12pt; font-weight: 400; font-style: normal;}", "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;}", "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;}", "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: 1.09em; 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: 0.76em; 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: 0.76em; 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.30em; 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;}", ".sep, .skip {text-align: center; margin-top: 1.30em; margin-bottom: 1.30em;}",
"a {color: rgb(66, 113, 174);}", "a {color: rgb(66, 113, 174);}",
"mark {background: rgb(255, 255, 166);}", "mark {background: rgb(255, 255, 166);}",
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?> <?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: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> <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:generator>novelWriter/2.5a4</meta:generator>
<meta:initial-creator>lipsum.com</meta:initial-creator> <meta:initial-creator>lipsum.com</meta:initial-creator>
<meta:editing-cycles>45</meta:editing-cycles> <meta:editing-cycles>45</meta:editing-cycles>
<meta:editing-duration>P0DT0H36M8S</meta:editing-duration> <meta:editing-duration>P0DT0H36M8S</meta:editing-duration>
<dc:title>Lorem Ipsum</dc:title> <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> <dc:creator>lipsum.com</dc:creator>
</office:meta> </office:meta>
<office:font-face-decls> <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: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:style style:name="Heading" style:family="paragraph" style:parent-style-name="Standard" style:next-style-name="Text_20_body" style:class="text"> <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: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: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" /> <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: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: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: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:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="30pt" fo:font-weight="bold" />
</style:style> </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: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:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="12pt" fo:font-weight="normal" />
</style:style> </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: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: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: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: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: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: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: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: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: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: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: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:style style:name="Header" style:family="paragraph" style:display-name="Header" style:parent-style-name="Header_20_and_20_Footer"> <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" "</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 @pytest.mark.core
def testCoreToHtml_ConvertDirect(mockGUI): 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_Bod'>Bod</a>, "
"<a class='tag' href='#tag_Jane'>Jane</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._lineHeight == 1.15
assert tokens._blockIndent == 4.0 assert tokens._blockIndent == 4.0
assert tokens._doJustify is False assert tokens._doJustify is False
assert tokens._marginTitle == (1.000, 0.500) assert tokens._marginTitle == (1.417, 0.500)
assert tokens._marginHead1 == (1.000, 0.500) assert tokens._marginHead1 == (1.417, 0.500)
assert tokens._marginHead2 == (0.834, 0.500) assert tokens._marginHead2 == (1.668, 0.500)
assert tokens._marginHead3 == (0.584, 0.500) assert tokens._marginHead3 == (1.168, 0.500)
assert tokens._marginHead4 == (0.584, 0.500) assert tokens._marginHead4 == (1.168, 0.500)
assert tokens._marginText == (0.000, 0.584) assert tokens._marginText == (0.000, 0.584)
assert tokens._marginMeta == (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._hideTitle is False
assert tokens._hideChapter is False assert tokens._hideChapter is False
assert tokens._hideUnNum is False assert tokens._hideUnNum is False
@@ -93,6 +94,7 @@ def testCoreToken_Setters(mockGUI):
tokens.setHead4Margins(2.0, 2.0) tokens.setHead4Margins(2.0, 2.0)
tokens.setTextMargins(2.0, 2.0) tokens.setTextMargins(2.0, 2.0)
tokens.setMetaMargins(2.0, 2.0) tokens.setMetaMargins(2.0, 2.0)
tokens.setSeparatorMargins(2.0, 2.0)
tokens.setLinkHeadings(True) tokens.setLinkHeadings(True)
tokens.setBodyText(False) tokens.setBodyText(False)
tokens.setSynopsis(True) tokens.setSynopsis(True)
@@ -117,6 +119,7 @@ def testCoreToken_Setters(mockGUI):
assert tokens._marginHead4 == (2.0, 2.0) assert tokens._marginHead4 == (2.0, 2.0)
assert tokens._marginText == (2.0, 2.0) assert tokens._marginText == (2.0, 2.0)
assert tokens._marginMeta == (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._hideTitle is True
assert tokens._hideChapter is True assert tokens._hideChapter is True
assert tokens._hideUnNum is True assert tokens._hideUnNum is True
+2 -2
View File
@@ -520,9 +520,9 @@ def testCoreToOdt_ConvertParagraphs(mockGUI):
assert odt.errData == [] assert odt.errData == []
assert xmlToText(odt._xText) == ( assert xmlToText(odt._xText) == (
'<office:text>' '<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="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>' '<text:p text:style-name="Text_20_body">Text</text:p>'
'</office:text>' '</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 PyQt5.QtWidgets import QAction, QApplication, QMenu
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.core.tohtml import ToHtml from novelwriter.core.toqdoc import ToQTextDocument
from novelwriter.enum import nwDocAction from novelwriter.enum import nwDocAction
from novelwriter.gui.docviewer import GuiDocViewer from novelwriter.gui.docviewer import GuiDocViewer
from novelwriter.types import QtModeNone, QtMouseLeft from novelwriter.types import QtModeNone, QtMouseLeft
@@ -119,7 +119,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
# Select All # Select All
assert docViewer.docAction(nwDocAction.SEL_ALL) is True assert docViewer.docAction(nwDocAction.SEL_ALL) is True
cursor = docViewer.textCursor() cursor = docViewer.textCursor()
assert len(cursor.selectedText()) == 3061 assert len(cursor.selectedText()) == 3060
# Other actions # Other actions
assert docViewer.docAction(nwDocAction.NO_ACTION) is False assert docViewer.docAction(nwDocAction.NO_ACTION) is False
@@ -157,7 +157,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
docViewer.setTextCursor(cursor) docViewer.setTextCursor(cursor)
docViewer._makeSelection(QTextCursor.WordUnderCursor) docViewer._makeSelection(QTextCursor.WordUnderCursor)
rect = docViewer.cursorRect() rect = docViewer.cursorRect()
docViewer._linkClicked(QUrl("#char=Bod")) docViewer._linkClicked(QUrl("#tag_bod"))
assert docViewer.docHandle == "4c4f28287af27" assert docViewer.docHandle == "4c4f28287af27"
# Click mouse nav buttons # Click mouse nav buttons
@@ -196,19 +196,19 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
# Document footer show/hide synopsis # Document footer show/hide synopsis
assert nwGUI.viewDocument("f96ec11c6a3da") is True assert nwGUI.viewDocument("f96ec11c6a3da") is True
assert len(docViewer.toPlainText()) == 4315 assert len(docViewer.toPlainText()) == 4314
docViewer.docFooter._doToggleSynopsis(False) docViewer.docFooter._doToggleSynopsis(False)
assert len(docViewer.toPlainText()) == 4099 assert len(docViewer.toPlainText()) == 4098
# Document footer show/hide comments # Document footer show/hide comments
assert nwGUI.viewDocument("846352075de7d") is True assert nwGUI.viewDocument("846352075de7d") is True
assert len(docViewer.toPlainText()) == 675 assert len(docViewer.toPlainText()) == 683
docViewer.docFooter._doToggleComments(False) docViewer.docFooter._doToggleComments(False)
assert len(docViewer.toPlainText()) == 635 assert len(docViewer.toPlainText()) == 634
# Crash the HTML rendering # Crash the HTML rendering
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ToHtml, "doConvert", causeException) mp.setattr(ToQTextDocument, "doConvert", causeException)
assert docViewer.loadText("846352075de7d") is False assert docViewer.loadText("846352075de7d") is False
assert docViewer.toPlainText() == "An error occurred while generating the preview." 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 @pytest.mark.gui
def testManuscriptBuild_Main( def testToolManuscriptBuild_Main(
monkeypatch, qtbot: QtBot, nwGUI: GuiMain, fncPath: Path, projPath: Path, mockRnd monkeypatch, qtbot: QtBot, nwGUI: GuiMain, fncPath: Path, projPath: Path, mockRnd
): ):
"""Test the GuiManuscriptBuild dialog.""" """Test the GuiManuscriptBuild dialog."""
+15 -45
View File
@@ -28,20 +28,19 @@ from PyQt5.QtCore import pyqtSlot
from PyQt5.QtPrintSupport import QPrintPreviewDialog from PyQt5.QtPrintSupport import QPrintPreviewDialog
from PyQt5.QtWidgets import QAction, QListWidgetItem from PyQt5.QtWidgets import QAction, QListWidgetItem
from novelwriter import CONFIG, SHARED from novelwriter import SHARED
from novelwriter.constants import nwHeadFmt from novelwriter.constants import nwHeadFmt
from novelwriter.core.buildsettings import BuildSettings from novelwriter.core.buildsettings import BuildSettings
from novelwriter.tools.manusbuild import GuiManuscriptBuild from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.manussettings import GuiBuildSettings 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 from tests.tools import C, buildTestProject
@pytest.mark.gui @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.""" """Test the init/main functionality of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath) nwGUI.openProject(projPath)
@@ -55,38 +54,27 @@ def testManuscript_Init(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.show() manus.show()
assert manus.docPreview.toPlainText().strip() == "" 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.clearSelection()
manus.buildList.setCurrentRow(0) manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged): with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click() manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() == allText 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 nwGUI.closeProject() # This should auto-close the manuscript tool
assert manus.isHidden()
# qtbot.stop() # qtbot.stop()
@pytest.mark.gui @pytest.mark.gui
def testManuscript_Builds(qtbot, nwGUI, projPath): def testToolManuscript_Builds(qtbot, nwGUI, projPath):
"""Test the handling of builds in the GuiManuscript dialog.""" """Test the handling of builds in the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath) nwGUI.openProject(projPath)
@@ -157,7 +145,7 @@ def testManuscript_Builds(qtbot, nwGUI, projPath):
@pytest.mark.gui @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.""" """Test other features of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath) nwGUI.openProject(projPath)
@@ -186,7 +174,6 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.show() manus.show()
manus.loadContent() manus.loadContent()
cacheFile = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
manus.buildList.setCurrentRow(0) manus.buildList.setCurrentRow(0)
build = manus._getSelectedBuild() build = manus._getSelectedBuild()
assert isinstance(build, BuildSettings) assert isinstance(build, BuildSettings)
@@ -199,17 +186,9 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
manus.btnPreview.click() manus.btnPreview.click()
qtbot.wait(200) # Should be enough to run the build qtbot.wait(200) # Should be enough to run the build
assert manus.docPreview.toPlainText().strip() == "" assert manus.docPreview.toPlainText().strip() == ""
assert cacheFile.exists() is False
manus._updateBuildsList() manus._updateBuildsList()
# Preview the first, but fail to save cache # Preview the first
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
first = manus.buildList.item(0) first = manus.buildList.item(0)
assert isinstance(first, QListWidgetItem) assert isinstance(first, QListWidgetItem)
build = manus._builds.getBuild(first.data(GuiManuscript.D_KEY)) 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) build.setValue("headings.fmtAltScene", nwHeadFmt.TITLE)
manus._builds.setBuild(build) manus._builds.setBuild(build)
# Preview again, and allow cache file to be created
manus.buildList.setCurrentRow(0) manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged): with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click() manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() != "" assert manus.docPreview.toPlainText().strip() != ""
assert cacheFile.exists() is True
# Check Outline # Check Outline
assert manus.buildOutline._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.maxTotalWords.text() == "25"
assert manus.docStats.maxTotalChars.text() == "117" 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 # Tests are too fast to trigger this one, so we trigger it manually to ensure it isn't failing
manus.docPreview._postUpdate() manus.docPreview._postUpdate()
@@ -299,7 +269,7 @@ def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
@pytest.mark.gui @pytest.mark.gui
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") @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.""" """Test the print feature of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath) nwGUI.openProject(projPath)
+6 -6
View File
@@ -40,7 +40,7 @@ from tests.tools import C, buildTestProject
@pytest.mark.gui @pytest.mark.gui
def testBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd): def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
"""Test the initialisation of the GuiBuildSettings dialog.""" """Test the initialisation of the GuiBuildSettings dialog."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath) nwGUI.openProject(projPath)
@@ -110,7 +110,7 @@ def testBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd):
@pytest.mark.gui @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.""" """Test the Filter Tab of the GuiBuildSettings dialog."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath) nwGUI.openProject(projPath)
@@ -314,7 +314,7 @@ def testBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd):
@pytest.mark.gui @pytest.mark.gui
def testBuildSettings_Headings(qtbot, nwGUI): def testToolBuildSettings_Headings(qtbot, nwGUI):
"""Test the Headings Tab of the GuiBuildSettings dialog.""" """Test the Headings Tab of the GuiBuildSettings dialog."""
build = BuildSettings() build = BuildSettings()
@@ -484,7 +484,7 @@ def testBuildSettings_Headings(qtbot, nwGUI):
@pytest.mark.gui @pytest.mark.gui
def testBuildSettings_Content(qtbot, nwGUI): def testToolBuildSettings_Content(qtbot, nwGUI):
"""Test the Content Tab of the GuiBuildSettings dialog.""" """Test the Content Tab of the GuiBuildSettings dialog."""
build = BuildSettings() build = BuildSettings()
@@ -544,7 +544,7 @@ def testBuildSettings_Content(qtbot, nwGUI):
@pytest.mark.gui @pytest.mark.gui
def testBuildSettings_Format(monkeypatch, qtbot, nwGUI): def testToolBuildSettings_Format(monkeypatch, qtbot, nwGUI):
"""Test the Format Tab of the GuiBuildSettings dialog.""" """Test the Format Tab of the GuiBuildSettings dialog."""
build = BuildSettings() build = BuildSettings()
@@ -650,7 +650,7 @@ def testBuildSettings_Format(monkeypatch, qtbot, nwGUI):
@pytest.mark.gui @pytest.mark.gui
def testBuildSettings_Output(qtbot, nwGUI): def testToolBuildSettings_Output(qtbot, nwGUI):
"""Test the Output Tab of the GuiBuildSettings dialog.""" """Test the Output Tab of the GuiBuildSettings dialog."""
build = BuildSettings() build = BuildSettings()