diff --git a/novelwriter/core/toqdoc.py b/novelwriter/core/toqdoc.py index f3e880a5..0f094016 100644 --- a/novelwriter/core/toqdoc.py +++ b/novelwriter/core/toqdoc.py @@ -138,35 +138,36 @@ class ToQTextDocument(Tokenizer): # ================= self._cText = QTextCharFormat() + self._cText.setBackground(QtTransparent) self._cText.setForeground(self._theme.text) - self._cHead = QTextCharFormat() + self._cHead = QTextCharFormat(self._cText) self._cHead.setForeground(self._theme.head) - self._cComment = QTextCharFormat() + self._cComment = QTextCharFormat(self._cText) self._cComment.setForeground(self._theme.comment) - self._cCommentMod = QTextCharFormat() + self._cCommentMod = QTextCharFormat(self._cText) self._cCommentMod.setForeground(self._theme.comment) self._cCommentMod.setFontWeight(self._bold) - self._cNote = QTextCharFormat() + self._cNote = QTextCharFormat(self._cText) self._cNote.setForeground(self._theme.note) - self._cCode = QTextCharFormat() + self._cCode = QTextCharFormat(self._cText) self._cCode.setForeground(self._theme.code) - self._cModifier = QTextCharFormat() + self._cModifier = QTextCharFormat(self._cText) self._cModifier.setForeground(self._theme.modifier) self._cModifier.setFontWeight(self._bold) - self._cKeyword = QTextCharFormat() + self._cKeyword = QTextCharFormat(self._cText) self._cKeyword.setForeground(self._theme.keyword) - self._cTag = QTextCharFormat() + self._cTag = QTextCharFormat(self._cText) self._cTag.setForeground(self._theme.tag) - self._cOptional = QTextCharFormat() + self._cOptional = QTextCharFormat(self._cText) self._cOptional.setForeground(self._theme.optional) self._init = True diff --git a/tests/test_core/test_core_toqdoc.py b/tests/test_core/test_core_toqdoc.py new file mode 100644 index 00000000..13a5d28f --- /dev/null +++ b/tests/test_core/test_core_toqdoc.py @@ -0,0 +1,571 @@ +""" +novelWriter – ToQTextDocument Class Tester +========================================== + +This file is a part of novelWriter +Copyright 2018–2024, 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 . +""" +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."